/*! * Copyright (c) 2017-2018 by The Funfix Project Developers. * Some rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { HK, Monad } from "funland" import { Either, Throwable, coreInternals } from "funfix-core" import { IteratorLike, iteratorOf } from "./internals" /** * Eval is a monad which controls evaluation. * * This type wraps a value (or an expression that produces a value) * and can produce it on command via the {@link Eval.get get()} * method. * * There are three basic evaluation strategies: * * - {@link Eval.now}: for describing strict values, evaluated * immediately * - {@link Eval.once}: evaluated only once when the value is needed, * with the result memoized (cached) for subsequent evaluations * - [[Eval.always]]: evaluated every time the value is needed, * being equivalent to a function * * Eval supports stack-safe lazy computation via the {@link Eval.map .map} * and {@link Eval.flatMap .flatMap} methods, which use an internal * trampoline to avoid stack overflows. Computation done within `map` * and `flatMap` is always done lazily, even when applied to an * `Eval.now` instance. * * Use `map` and `flatMap` to chain computation, and use `get()` to * get the result when needed. It is also not good style to create * `Eval` instances whose computation involves calling `get()` on * another `Eval` instance -- this can defeat the trampolining and * lead to stack overflows. * * ```typescript * const rndInt = Eval.of(() => { * const nr = Math.random() * 1000000 * return nr & nr * }) * * const evenInt = () => * rndInt.flatMap(int => { * if (i % 2 == 0) * return Eval.now(i) * else // Retry until we have an even number! * return evenInt() * }) * * const cached = evenInt().memoize() * * // Nothing happens until now, this triggers the * // actual evaluation: * const n: number = cached.get() * ``` * * ## Versus IO * * For dealing with lazy evaluation, the other alternative is * the {@link IO} data type. * * Differences between `Eval` and `IO`: * * 1. `IO` is capable of describing asynchronous computations as well * 2. `IO` is capable of error handling (it implements `MonadError`), * whereas `Eval` does not provide error handling capabilities, * being meant to be used for pure expressions (it implements * `Comonad`, which is incompatible with `MonadError`) * * So if you need error handling capabilities * (i.e. `MonadError`), or if you need to describe * asynchronous processes, then {@link IO} is for you. `Eval` * is a simpler data type with the sole purpose of controlling the * evaluation of expressions (i.e. strict versus lazy). * * ## Credits * * This type is inspired by `cats.Eval` from * {@link http://typelevel.org/cats/|Typelevel Cats} * and by `monix.eval.Coeval` from {@link https://monix.io|Monix}. * * @final */ export class Eval implements HK<"funfix/eval", A> { /** * Evaluates the source `Eval` and returns the result. * * ```typescript * const ref = Eval.always(() => 100 * 2) * * ref.get() // 200 * ``` */ get(): A { return evalRunLoop(this) } /** * Returns a new `Eval` that applies the mapping function to the * successful result emitted by the source. * * ```typescript * Eval.now(111).map(_ => _ * 2).get() // 222 * ``` */ map(f: (a: A) => B): Eval { return new FlatMap(this, (a: A) => Eval.now(f(a))) } /** * Creates a new `Eval` by applying a function to the successful * result of the source, and returns a new instance equivalent to * the result of the function. * * ```typescript * const rndInt = Eval.of(() => { * const nr = Math.random() * 1000000 * return nr & nr * }) * * const evenInt = () => * rndInt.flatMap(int => { * if (i % 2 == 0) * return Eval.now(i) * else // Retry until we have an even number! * return evenInt() * }) * ``` */ flatMap(f: (a: A) => Eval): Eval { return new FlatMap(this, f) } /** Alias for {@link flatMap}. */ chain(f: (a: A) => Eval): Eval { return this.flatMap(f) } /** * `Applicative` apply operator. * * Resembles {@link map}, but the passed mapping function is * lifted in the `Either` context. */ ap(ff: Eval<(a: A) => B>): Eval { return ff.flatMap(f => this.map(f)) } /** * Memoizes (caches) the result of the source on the first * evaluation and reuses it on subsequent invocations of `get()`. * * The resulting `Eval` will be idempotent, meaning that * evaluating it multiple times will have the same effect * as evaluating it once. */ memoize(): Eval { switch (this._tag) { case "now": case "once": return this case "always": return new Once(this.get) default: return new Once(() => this.get()) } } /** * Returns a new `Eval` that upon evaluation will execute the given * function for the generated element, transforming the source into * an `Eval`. * * Similar in spirit with normal {@link Eval.forEach .forEach}, * but lazy, as obviously nothing gets executed at this point. */ forEachL(cb: (a: A) => void): Eval { return this.map(cb) } /** * Triggers the evaluation of the source, executing the given * function for the generated element. * * The application of this function has strict behavior, as the * coeval is immediately executed. */ forEach(cb: (a: A) => void): void { this.forEachL(cb).get() } /** * Identifies the `Eval` reference type, useful for debugging and * for pattern matching in the implementation. * * @hidden */ readonly _tag!: "now" | "always" | "once" | "suspend" | "flatMap" // Implements HK /** @hidden */ readonly _URI!: "funfix/eval" /** @hidden */ readonly _A!: A // Implements Constructor /** @hidden */ static readonly _Class: Eval /** * Alias for {@link Eval.always}. */ static of(thunk: () => A): Eval { return Eval.always(thunk) } /** * Lifts a value into the `Eval` context. * * Alias for {@link Eval.now}. */ static pure(value: A): Eval { return Eval.now(value) } /** * Returns an `Eval` that on execution is always successful, * emitting the given strict value. */ static now(value: A): Eval { return new Now(value) } /** * Shorthand for `now(undefined as void)`, always returning * the same reference as optimization. */ static unit(): Eval { return evalUnitRef } /** * Promote a `thunk` function to an `Eval`, catching exceptions in * the process. * * Note that since `Eval` is not memoized by global, this will * recompute the value each time the `Eval` is executed. */ static always(thunk: () => A): Eval { return new Always(thunk) } /** * Promote a `thunk` function to a `Coeval` that is memoized on the * first evaluation, the result being then available on subsequent * evaluations. * * Note this is equivalent with: * * ```typescript * Eval.always(thunk).memoize() * ``` */ static once(thunk: () => A): Eval { return new Once(thunk) } /** * Promote a `thunk` function generating `Eval` results to an `Eval` * of the same type. */ static suspend(thunk: () => Eval): Eval { return new Suspend(thunk) } /** * Promote a `thunk` function generating `Eval` results to an `Eval` * of the same type. * * Alias for {@link Eval.suspend}. */ static defer(thunk: () => Eval): Eval { return Eval.suspend(thunk) } /** * Keeps calling `f` until a `Right(b)` is returned. * * Based on Phil Freeman's * [Stack Safety for Free]{@link http://functorial.com/stack-safety-for-free/index.pdf}. * * Described in `FlatMap.tailRecM`. */ static tailRecM(a: A, f: (a: A) => Eval>): Eval { return f(a).flatMap(either => { if (either.isRight()) { return Eval.now(either.get()) } else { // Recursive call return Eval.tailRecM(either.swap().get(), f) } }) } /** * Transforms a list of `Eval` values into an `Eval` of a list. * * Sample: * * ```typescript * const io1 = Eval.of(() => 1) * const io2 = Eval.of(() => 2) * const io3 = Eval.of(() => 3) * * // Yields [1, 2, 3] * const all: Eval = Eval.sequence([f1, f2, f3]) * ``` */ static sequence(list: Eval[] | Iterable>): Eval { return evalSequence(list) } /** * Maps 2 `Eval` values by the mapping function, returning a new * `Eval` reference that completes with the result of mapping that * function to the successful values of the futures, or in failure in * case either of them fails. * * ```typescript * const fa1 = Eval.of(() => 1) * const fa2 = Eval.of(() => 2) * * * // Yields Success(3) * Eval.map2(fa1, fa2, (a, b) => a + b) * ``` * * This operation is the `Applicative.map2`. */ static map2( fa1: Eval, fa2: Eval, f: (a1: A1, a2: A2) => R): Eval { const fl: Eval = Eval.sequence([fa1, fa2] as any[]) return fl.map(lst => f(lst[0], lst[1])) } /** * Maps 3 `Eval` values by the mapping function, returning a new * `Eval` reference that completes with the result of mapping that * function to the successful values of the futures, or in failure in * case either of them fails. * * ```typescript * const fa1 = Eval.of(() => 1) * const fa2 = Eval.of(() => 2) * const fa3 = Eval.of(() => 3) * * * // Yields Success(6) * Eval.map3(fa1, fa2, fa3, (a, b, c) => a + b + c) * ``` */ static map3( fa1: Eval, fa2: Eval, fa3: Eval, f: (a1: A1, a2: A2, a3: A3) => R): Eval { const fl: Eval = Eval.sequence([fa1, fa2, fa3] as any[]) return fl.map(lst => f(lst[0], lst[1], lst[2])) } /** * Maps 4 `Eval` values by the mapping function, returning a new * `Eval` reference that completes with the result of mapping that * function to the successful values of the futures, or in failure in * case either of them fails. * * ```typescript * const fa1 = Eval.of(() => 1) * const fa2 = Eval.of(() => 2) * const fa3 = Eval.of(() => 3) * const fa4 = Eval.of(() => 4) * * // Yields Success(10) * Eval.map4(fa1, fa2, fa3, fa4, (a, b, c, d) => a + b + c + d) * ``` */ static map4( fa1: Eval, fa2: Eval, fa3: Eval, fa4: Eval, f: (a1: A1, a2: A2, a3: A3, a4: A4) => R): Eval { const fl: Eval = Eval.sequence([fa1, fa2, fa3, fa4] as any[]) return fl.map(lst => f(lst[0], lst[1], lst[2], lst[3])) } /** * Maps 5 `Eval` values by the mapping function, returning a new * `Eval` reference that completes with the result of mapping that * function to the successful values of the futures, or in failure in * case either of them fails. * * ```typescript * const fa1 = Eval.of(() => 1) * const fa2 = Eval.of(() => 2) * const fa3 = Eval.of(() => 3) * const fa4 = Eval.of(() => 4) * const fa5 = Eval.of(() => 5) * * // Yields Success(15) * Eval.map5(fa1, fa2, fa3, fa4, fa5, * (a, b, c, d, e) => a + b + c + d + e * ) * ``` */ static map5( fa1: Eval, fa2: Eval, fa3: Eval, fa4: Eval, fa5: Eval, f: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => R): Eval { const fl: Eval = Eval.sequence([fa1, fa2, fa3, fa4, fa5] as any[]) return fl.map(lst => f(lst[0], lst[1], lst[2], lst[3], lst[4])) } /** * Maps 6 `Eval` values by the mapping function, returning a new * `Eval` reference that completes with the result of mapping that * function to the successful values of the futures, or in failure in * case either of them fails. * * ```typescript * const fa1 = Eval.of(() => 1) * const fa2 = Eval.of(() => 2) * const fa3 = Eval.of(() => 3) * const fa4 = Eval.of(() => 4) * const fa5 = Eval.of(() => 5) * const fa6 = Eval.of(() => 6) * * // Yields Success(21) * Eval.map6( * fa1, fa2, fa3, fa4, fa5, fa6, * (a, b, c, d, e, f) => a + b + c + d + e + f * ) * ``` */ static map6( fa1: Eval, fa2: Eval, fa3: Eval, fa4: Eval, fa5: Eval, fa6: Eval, f: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6) => R): Eval { const fl: Eval = Eval.sequence([fa1, fa2, fa3, fa4, fa5, fa6] as any[]) return fl.map(lst => f(lst[0], lst[1], lst[2], lst[3], lst[4], lst[5])) } } /** * `Now` is an internal `Eval` state that wraps any strict * value in an `Eval` reference. Returned by [[Eval.now]]. * * @private */ class Now extends Eval { readonly _tag: "now" = "now" /** * @param value is the value that's going to be returned * when `get()` is called. */ constructor(public readonly value: A) { super() } get(): A { return this.value } toString(): string { return `Eval.now(${JSON.stringify(this.value)})` } } /** * Reusable reference, to use in {@link Eval.unit}. * * @private */ const evalUnitRef: Now = new Now(undefined) /** * `Once` is an internal `Eval` state that executes the given `thunk` * only once, upon calling `get()` and then memoize its result for * subsequent invocations. * * Returned by [[Eval.once]]. * * @private */ class Once extends Eval { readonly _tag: "once" = "once" private _thunk: () => A private _cache?: Throwable | A private _isError?: boolean constructor(thunk: () => A) { super() this._thunk = thunk } get(): A { if (this._thunk) { try { this._cache = this._thunk() this._isError = false } catch (e) { this._cache = e this._isError = true } // GC purposes delete this._thunk } if (this._isError) throw this._cache return this._cache as A } toString(): string { return `Eval.once([thunk])` } } /** * `Always` is an internal `Eval` state that executes the given `thunk` * every time the call to `get()` happens. Returned by [[Eval.always]]. * * @private */ class Always extends Eval { readonly _tag: "always" = "always" constructor(thunk: () => A) { super() this.get = thunk } toString(): string { return `Eval.always([thunk])` } } /** * `Suspend` is an internal `Eval` state that represents a factory of * `Eval` values. Returned by [[Eval.suspend]]. * * @private */ class Suspend extends Eval { readonly _tag: "suspend" = "suspend" constructor(public readonly thunk: () => Eval) { super() } toString(): string { return `Eval.suspend([thunk])` } } /** * `FlatMap` is an internal `Eval` state that represents a * [[Eval.flatMap .flatMap]], [[Eval.map .map]] operations, * all of them being expressed with this state. * * @private */ class FlatMap extends Eval { readonly _tag: "flatMap" = "flatMap" constructor( public readonly source: Eval, public readonly f: (a: A) => Eval) { super() } toString(): string { return `Eval#FlatMap(${String(this.source)}, [function])` } } /** * Type enumerating the type classes implemented by `Eval`. */ export type EvalTypes = Monad<"funfix/eval"> /** * Type-class implementations, compatible with the `static-land` * specification. */ export const EvalModule: EvalTypes = { // Functor map: (f: (a: A) => B, fa: Eval) => fa.map(f), // Apply ap: (ff: Eval<(a: A) => B>, fa: Eval): Eval => fa.ap(ff), // Applicative of: Eval.pure, // Chain chain: (f: (a: A) => Eval, fa: Eval): Eval => fa.flatMap(f), // ChainRec chainRec: (f: (next: (a: A) => C, done: (b: B) => C, a: A) => Eval, a: A): Eval => Eval.tailRecM(a, a => f(Either.left as any, Either.right as any, a)) } // Registers Fantasy-Land compatible symbols coreInternals.fantasyLandRegister(Eval, EvalModule) /** @hidden */ type Current = Eval /** @hidden */ type Bind = ((a: any) => Eval) /** @hidden */ type CallStack = Array /** @hidden */ function _popNextBind(bFirst: Bind | null, bRest: CallStack | null): Bind | undefined | null { if (bFirst) return bFirst if (bRest && bRest.length > 0) return bRest.pop() return null } /** @hidden */ function evalRunLoop(start: Eval): A { let current: Current = start let bFirst: Bind | null = null let bRest: CallStack | null = null while (true) { switch (current._tag) { case "now": const now = current as Now const bind = _popNextBind(bFirst, bRest) if (!bind) return now.value bFirst = null current = bind(now.value) break case "always": case "once": current = new Now(current.get()) break case "suspend": current = (current as Suspend).thunk() break case "flatMap": if (bFirst) { if (!bRest) bRest = [] bRest.push(bFirst) } const fm = current as FlatMap bFirst = fm.f current = fm.source break } } } /** * Implementation for `Eval.sequence`. * @hidden */ function evalSequence(list: Eval[] | Iterable>): Eval { return Eval.of(() => iteratorOf(list)) .flatMap(cursor => evalSequenceLoop([], cursor)) } /** * Recursive loop that goes through the given `cursor`, element by * element, gathering the results of all generated `Eval` elements. * * @hidden */ function evalSequenceLoop(acc: A[], cursor: IteratorLike>): Eval { while (true) { const elem = cursor.next() const isDone = elem.done if (elem.value) { const io: Eval = elem.value return io.flatMap(a => { acc.push(a) if (isDone) return Eval.pure(acc) return evalSequenceLoop(acc, cursor) }) } else { /* istanbul ignore else */ if (isDone) return Eval.pure(acc) } } }