dohrm/scalts

View on GitHub
src/Future.ts

Summary

Maintainability
F
4 days
Test Coverage
/* tslint:disable:no-use-before-declare */

import {Try, Success, Failure} from './Try';
import {Optional, Some, None} from './Optional';

import { Supplier } from './types'


// TODO review this code to use PartialFunction and other ...

/**
 * A `Future` represents a value which may or may not *currently* be available,
 * but will be available at some point, or an exception if that value could not be made available.
 */
export interface Future<A> {
    readonly promise: Promise<A>;
    /**
     * When this future is completed, either through an exception, or a value,
     * apply the provided function.
     *
     * If the future has already been completed,
     * this will either be applied immediately or be scheduled asynchronously.
     *
     * Note that the returned value of `f` will be discarded.
     */
    onComplete<B>(f: (t: Try<A>) => B): void;
    /**
     * Returns whether the future had already been completed with
     * a value or an exception.
     *
     * $nonDeterministic
     */
    isCompleted(): boolean;
    /**
     * The current value of this `Future`.
     *
     * $nonDeterministic
     *
     * If the future was not completed the returned value will be `None`.
     * If the future was completed the value will be `Some(Success(t))`
     * if it contained a valid result, or `Some(Failure(error))` if it contained
     * an exception.
     */
    value(): Optional<Try<A>>;
    /**
     * The returned `Future` will be successfully completed with the `Error` of the original `Future`
     * if the original `Future` fails.
     *
     * If the original `Future` is successful, the returned `Future` is failed with a `NoSuchElementException`.
     */
    failed(): Future<Error>;
    /**
     * Asynchronously processes the value in the future once the value becomes available.
     *
     * WARNING: Will not be called if this future is never completed or if it is completed with a failure.
     *
     * $swallowsExceptions
     */
    foreach<B>(f: (a: A) => B): void;
    /**
     * Creates a new Future by applying the specified function to the result
     * of this Future. If there is any non-fatal exception thrown when 'f'
     * is applied then that exception will be propagated to the resulting future.
     */
    transform<B>(f: (t: Try<A>) => Try<B>): Future<B>;
    /**
     * Creates a new future by applying the 's' function to the successful result of
     * this future, or the 'f' function to the failed result. If there is any non-fatal
     * exception thrown when 's' or 'f' is applied, that exception will be propagated
     * to the resulting future.
     */
    transform1<B>(s: (a: A) => B, f: (e: Error) => Error): Future<B>;
    /**
     * Creates a new Future by applying the specified function, which produces a Future, to the result
     * of this Future. If there is any non-fatal exception thrown when 'f'
     * is applied then that exception will be propagated to the resulting future.
     */
    transformWith<B>(f: (t: Try<A>) => Future<B>): Future<B>;
    /**
     * Creates a new future by applying a function to the successful result of
     * this future. If this future is completed with an exception then the new
     * future will also contain this exception.
     */
    map<B>(f: (a: A) => B): Future<B>;
    /**
     * Creates a new future by applying a function to the successful result of
     * this future, and returns the result of the function as the new future.
     * If this future is completed with an exception then the new future will
     * also contain this exception.
     */
    flatMap<B>(f: (a: A) => Future<B>): Future<B>;
    /**
     * Creates a new future by filtering the value of the current future with a predicate.
     *
     * If the current future contains a value which satisfies the predicate, the new future will also hold that value.
     * Otherwise, the resulting future will fail with a `NoSuchElementException`.
     *
     * If the current future fails, then the resulting future also fails.
     */
    filter(f: (a: A) => boolean): Future<A>;
    /**
     * Creates a new future that will handle any matching Error that this
     * future might contain. If there is no match, or if this future contains
     * a valid result then the new future will contain the same.
     * @param f
     */
    recover<B extends A>(f: (e: Error) => Optional<B>): Future<A>;
    /**
     * Creates a new future that will handle any matching throwable that this
     * future might contain by assigning it a value of another future.
     *
     * If there is no match, or if this future contains
     * a valid result then the new future will contain the same result.
     * @param f
     */
    recoverWith<B extends A>(f: (e: Error) => Optional<Future<B>>): Future<A>;
    /**
     * Zips the values of `this` and `that` future, and creates
     * a new future holding the tuple of their results.
     *
     * If `this` future fails, the resulting future is failed
     * with the throwable stored in `this`.
     * Otherwise, if `that` future fails, the resulting future is failed
     * with the throwable stored in `that`.
     */
    zip<B>(fu: Supplier< Future<B> >): Future<[A, B]>;
    /**
     * Zips the values of `this` and `that` future using a function `f`,
     * and creates a new future holding the result.
     *
     * If `this` future fails, the resulting future is failed
     * with the throwable stored in `this`.
     * Otherwise, if `that` future fails, the resulting future is failed
     * with the throwable stored in `that`.
     * If the application of `f` throws a throwable, the resulting future
     * is failed with that throwable if it is non-fatal.
     * @param f
     */
    zipWith<B, C>(fu: Supplier< Future<B> >, f: (a: A, b: B) => C): Future<C>;
    /**
     * Creates a new future which holds the result of this future if it was completed successfully, or, if not,
     * the result of the `that` future if `that` is completed successfully.
     * If both futures are failed, the resulting future holds the throwable object of the first future.
     *
     * Using this method will not cause concurrent programs to become nondeterministic.
     */
    fallbackTo<B extends A>(fu: Supplier< Future<B> >): Future<A>;
    /**
     * Applies the side-effecting function to the result of this future, and returns
     * a new future with the result of this future.
     *
     * This method allows one to enforce that the callbacks are executed in a
     * specified order.
     *
     * Note that if one of the chained `andThen` callbacks throws
     * an exception, that exception is not propagated to the subsequent `andThen`
     * callbacks. Instead, the subsequent `andThen` callbacks are given the original
     * value of this future.
     */
    andThen<B>(f: (t: Try<A>) => B): Future<A>;


    // add methods
    apply1<B, C>(ob: Supplier< Future<B> >, f: (a: A, b: B) => C): Future<C>;
    apply2<B, C, D>(ob: Supplier< Future<B> >, oc: Supplier< Future<C> >, f: (a: A, b: B, c: C) => D): Future<D>;
    chain<B>(ob: Supplier< Future<B> >): FutureBuilder1<A, B>;
}

export function Future<A>(f: Promise<A> | (() => A)): Future<A> {
    if (f instanceof Promise) {
        return new FutureImpl(f);
    } else {
        return new FutureImpl<A>(
            new Promise((resolve, reject) => {
                Try(<() => A>f).fold(e => reject(e), a => resolve(a));
            })
        );
    }
}

export namespace Future {
    export function fromPromise<A>(p: Promise<A>): Future<A> {
        return new FutureImpl(p);
    }

    export function unit(): Future<void> {
        return new FutureImpl(Promise.resolve<void>(undefined), Success<void>(undefined));
    }

    export function failed<A>(e: Error): Future<A> {
        return new FutureImpl<A>(<Promise<A>>Promise.reject<A>(e), Failure<A>(e));
    }

    export function successful<A>(a: A): Future<A> {
        return new FutureImpl(Promise.resolve(a), Success(a));
    }

    export function fromTry<A>(t: Try<A>): Future<A> {
        return t.fold((e) => this.failed(e), (a) => this.successful(a));
    }

    export function sequence<A>(fus: Array<Future<A>>): Future<Array<A>> {
        return new FutureImpl(Promise.all(fus.map(a => a.promise)));
    }

    export function firstCompletedOf<A>(fus: Array<Future<A>>): Future<A> {
        return new FutureImpl(Promise.race(fus.map(a => a.promise)));
    }

    export function find<A>(fus: Array<Future<A>>, f: (a: A) => boolean): Future<Optional<A>> {
        let searchRecursive = (fr: Array<Future<A>>): Future<Optional<A>> => {
            if (fr.length === 0) {
                return Future.successful<Optional<A>>(None);
            }
            const [fh, ...ft] = fr;
            return fh.transformWith(t =>
                t.fold(
                    e => searchRecursive(ft),
                    a => f(a) ? Future.successful(Optional.apply(a)) : searchRecursive(ft))
            );
        };
        return searchRecursive(fus);
    }

    export function foldLeft<A, B>(fu: Array<Future<A>>, zero: B, f: (b: B, a: A) => B): Future<B> {
        let recursive = (fr: Array<Future<A>>, acc: B): Future<B> => {
            if (fr.length === 0) {
                return Future.successful(acc);
            }
            const [fh, ...ft] = fr;
            return fh.flatMap(a => recursive(ft, f(acc, a)));
        };
        return recursive(fu, zero);
    }

    export function traverse<A, B>(fu: Array<A>, f: (a: A) => Future<B>): Future<Array<B>> {
        const fzero = Future.successful<Array<B>>([]);
        if (fu.length === 0) {
            return fzero;
        }
        return fu.reduce<Future<Array<B>>>((fbs, a) =>
                fbs.zipWith(() => f(a), (bs, fa) => {
                    bs.push(fa);
                    return bs;
                }),
            fzero);
    }
}

class FutureImpl<A> implements Future<A> {
    private completeValue: Optional<Try<A>> = None;

    constructor(private _promise: Promise<A>, already?: Try<A>) {
        if (already) {
            this.completeValue = Some(already);
        } else {
            _promise.then(
                a => this.completeValue = Some(Success(a)),
                e => this.completeValue = Some(Failure<A>(e))
            );
        }
    }

    get promise(): Promise<A> {
        return this._promise;
    }

    onComplete<B>(f: (t: Try<A>) => B): void {
        this.promise.then(a => f(Success(a)), e => f(Failure<A>(e)));
    }

    isCompleted(): boolean {
        return this.completeValue.nonEmpty;
    }

    value(): Optional<Try<A>> {
        return this.completeValue;
    }

    failed(): Future<Error> {
        return this.transform(t =>
            t.fold(
                e => Success(e),
                a => Failure<Error>(new Error('Future.failed not completed with a throwable.'))
            )
        );
    }

    foreach<B>(f: (a: A) => B): void {
        this.onComplete(t => t.foreach(f));
    }

    tryPromise<B>(f: () => Promise<B>): Promise<B> {
        try {
            return f();
        } catch (e) {
            return <Promise<B>>Promise.reject<B>(e);
        }
    }

    transform<B>(f: (t: Try<A>) => Try<B>): Future<B> {
        return new FutureImpl(
            this.promise.then<B>(
                (a:A) => this.tryPromise(() => f(Success(a)).fold((e: Error) => <Promise<B>>Promise.reject<B>(e), (b:B) => Promise.resolve(b))),
                e => this.tryPromise(() => f(Failure<A>(e)).fold((e: Error) => <Promise<B>>Promise.reject<B>(e), (b) => Promise.resolve(b)))
            )
        );
    }

    transform1<B>(fs: (a: A) => B, ff: (e: Error) => Error): Future<B> {
        return this.transform<B>(t =>
            t.fold(
                e => {
                    try {
                        return Failure<B>(ff(e));
                    } catch (e) {
                        return Failure<B>(e);
                    }
                },
                a => Try(() => fs(a))
            )
        );
    }

    transformWith<B>(f: (t: Try<A>) => Future<B>): Future<B> {
        return new FutureImpl(
            this.promise.then<B>(
                (a:A) => this.tryPromise(() => f(Success(a)).promise),
                e => this.tryPromise(() => f(Failure<A>(e)).promise)
            )
        );
    }

    map<B>(f: (a: A) => B): Future<B> {
        return this.transform(t => t.map(f));
    }

    flatMap<B>(f: (a: A) => Future<B>): Future<B> {
        return this.transformWith(t => t.fold(e => Future.failed<B>(e), a => f(a)));
    }

    filter(f: (a: A) => boolean): Future<A> {
        return this.map(a => {
            if (f(a)) {
                return a;
            } else {
                throw new Error('Future.filter predicate is not satisfied');
            }
        });
    }

    recover<B extends A>(f: (e: Error) => Optional<B>): Future<A> {
        return this.transform(t => t.recover(f));
    }

    recoverWith<B extends A>(f: (e: Error) => Optional<Future<B>>): Future<A> {
        return this.transformWith(t =>
            t.fold(
                e => f(e).fold(() => Future.failed<B>(e), a => a),
                a => Future.successful(a)
            )
        );
    }

    zip<B>(fu: Supplier< Future<B> >): Future<[A, B]> {
        return this.flatMap(a => fu().map<[A, B]>(b => [a, b]));
    }

    zipWith<B, C>(fu: Supplier< Future<B> >, f: (a: A, b: B) => C): Future<C> {
        return this.flatMap(a => fu().map(b => f(a, b)));
    }

    fallbackTo<B extends A>(fu: Supplier< Future<B> >): Future<A> {
        return this.recoverWith(e => Some(fu())).recoverWith(e => Some(this));
    }

    andThen<B>(f: (t: Try<A>) => B): Future<A> {
        return this.transform(t => {
            try {
                f(t);
            } catch (e) {
                if (typeof console !== 'undefined') {
                    console.error(e);
                }
            }
            return t;
        });
    }

    apply1<B, C>(ob: Supplier< Future<B> >, f: (a: A, b: B) => C): Future<C> {
        return this.zipWith(ob, f);
    }

    apply2<B, C, D>(ob: Supplier< Future<B> >, oc: Supplier< Future<C> >, f: (a: A, b: B, c: C) => D): Future<D> {
        return this.flatMap(a => ob().flatMap(b => oc().map(c => f(a, b, c))));
    }

    chain<B>(ob: Supplier< Future<B> >): FutureBuilder1<A, B> {
        return new FutureBuilder1(this, ob);
    }
}

/**
 * FutureBuilder.
 */
export class FutureBuilder1<A, B> {
    constructor(private oa: Future<A>, private ob: Supplier< Future<B> >) {
    }

    run<C>(f: (a: A, b: B) => C): Future<C> {
        return this.oa.flatMap(a => this.ob().map(b => f(a, b)));
    }

    chain<C>(oc: Supplier< Future<C> >): FutureBuilder2<A, B, C> {
        return new FutureBuilder2(this.oa, this.ob, oc);
    }
}

export class FutureBuilder2<A, B, C> {
    constructor(private oa: Future<A>, private ob: Supplier< Future<B> >, private oc: Supplier< Future<C> >) {
    }

    run<D>(f: (a: A, b: B, c: C) => D): Future<D> {
        return this.oa.flatMap(a => this.ob().flatMap(b => this.oc().map(c => f(a, b, c))));
    }

    chain<D>(od: Supplier< Future<D> >): FutureBuilder3<A, B, C, D> {
        return new FutureBuilder3(this.oa, this.ob, this.oc, od);
    }
}

export class FutureBuilder3<A, B, C, D> {
    constructor(private oa: Future<A>, private ob: Supplier< Future<B> >, private oc: Supplier< Future<C> >, private od: Supplier< Future<D> >) {
    }

    run<E>(f: (a: A, b: B, c: C, d: D) => E): Future<E> {
        return this.oa.flatMap(a => this.ob().flatMap(b => this.oc().flatMap(c =>
            this.od().map(d => f(a, b, c, d)))));
    }

    chain<E>(oe: Supplier< Future<E> >): FutureBuilder4<A, B, C, D, E> {
        return new FutureBuilder4(this.oa, this.ob, this.oc, this.od, oe);
    }
}

export class FutureBuilder4<A, B, C, D, E> {
    constructor(private oa: Future<A>, private ob: Supplier< Future<B> >, private oc: Supplier< Future<C> >,
                private od: Supplier< Future<D> >, private oe: Supplier< Future<E> >) {
    }

    run<F>(f: (a: A, b: B, c: C, d: D, e: E) => F): Future<F> {
        return this.oa.flatMap(a => this.ob().flatMap(b => this.oc().flatMap(c => this.od().flatMap(d =>
            this.oe().map(e => f(a, b, c, d, e))))));
    }

    chain<F>(of: Supplier< Future<F> >): FutureBuilder5<A, B, C, D, E, F> {
        return new FutureBuilder5(this.oa, this.ob, this.oc, this.od, this.oe, of);
    }
}

export class FutureBuilder5<A, B, C, D, E, F> {
    constructor(private oa: Future<A>, private ob: Supplier< Future<B> >, private oc: Supplier< Future<C> >,
                private od: Supplier< Future<D> >, private oe: Supplier< Future<E> >, private of: Supplier< Future<F> >) {
    }

    run<G>(f: (a: A, b: B, c: C, d: D, e: E, f: F) => G): Future<G> {
        return this.oa.flatMap(a => this.ob().flatMap(b => this.oc().flatMap(c => this.od().flatMap(d => this.oe().flatMap(e =>
            this.of().map(ff => f(a, b, c, d, e, ff)))))));
    }
}