dohrm/scalts

View on GitHub
src/Optional.ts

Summary

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

import 'reflect-metadata';

import { Supplier, Predicate, Mapper, Func } from './types'

/**
 * Represents optional values. Instances of `Optional`
 *  are either an instance of $some or the object $none.
 *
 *  The most idiomatic way to use an $optional instance is to treat it
 *  as a collection or monad and use `map`,`flatMap`, `filter`, or
 *  `foreach`:
 */
export abstract class Optional<A> {
    /** Returns true if the option is $none, false otherwise. */
    isEmpty: boolean;
    /** Returns true if the option is an instance of $some, false otherwise. */
    nonEmpty: boolean;

    /**
     * Returns the option's value.
     * @note The option must be nonEmpty.
     */
    abstract get(): A;

    /**
     * Returns the option's value if the option is nonempty, otherwise
     * return the result of evaluating `default`.
     *
     * @param a the default expression.
     */
    getOrElse<B extends A>( a: Supplier< B > ): A {
        return this.isEmpty ? a() : this.get();
    }

    /**
     * Returns a $some containing the result of applying $f to this $option's value if this $option is nonempty.
     * Otherwise return $none.
     *
     * @note This is similar to `flatMap` except here, $f does not need to wrap its result in an $option.
     *
     * @param f the function to apply
     * @see flatMap
     * @see foreach
     */
    map<B>( f: Mapper< A, B > ): Optional<B> {
        return this.isEmpty ? None : Some( f( this.get() ) );
    }

    /**
     * Returns the result of applying $f to this $option's
     * value if the $option is nonempty.  Otherwise, evaluates
     * expression `ifEmpty`.
     *
     * @note This is equivalent to `$option map f getOrElse ifEmpty`.
     *
     * @param ifEmpty the expression to evaluate if empty.
     * @param f       the function to apply if nonempty.
     */
    fold<B>( ifEmpty: Supplier< B >, f: Mapper< A, B > ): B {
        return this.isEmpty ? ifEmpty() : f( this.get() );
    }

    /**
     * Returns the result of applying $f to this $option's value if this $option is nonempty.
     * Returns $none if this $option is empty.
     * Slightly different from `map` in that $f is expected to return an $option (which could be $none).
     *
     * @param f the function to apply
     * @see map
     * @see foreach
     */
    flatMap<B>( f: Mapper< A, Optional< B > > ): Optional<B> {
        return this.isEmpty ? None : f( this.get() );
    }

    /**
     * Returns this $option if it is nonempty '''and''' applying the predicate $F to
     * this $option's value returns true. Otherwise, return $none.
     *
     * @param f the predicate used for testing.
     */
    filter( f: Predicate< A > ): Optional<A> {
        return this.isEmpty || f( this.get() ) ? this : None;
    }

    /**
     * Tests whether the option contains a given value as an element.
     *
     * @param b the element to test.
     * @return `true` if the option has an element that is equal (as determined by `==`) to `elem`, `false` otherwise.
     */
    contains<B extends A>( b: B ): boolean {
        return this.nonEmpty && this.get() === b;
    }

    /**
     * Returns true if this option is nonempty '''and''' the predicate $f returns true when applied to this $option's value.
     * Otherwise, returns false.
     *
     * @param f the predicate to test
     */
    exists( f: Predicate< A > ): boolean {
        return this.nonEmpty && f( this.get() );
    }

    /**
     * Returns true if this option is empty '''or''' the predicate $f returns true when applied to this $option's value.
     *
     * @param f the predicate to test
     */
    forall( f: Predicate< A > ): boolean {
        return this.isEmpty || f( this.get() );
    }

    /**
     * Apply the given procedure $f to the option's value, if it is nonempty. Otherwise, do nothing.
     *
     * @param f the procedure to apply.
     * @see map
     * @see flatMap
     */
    foreach( f: Func< A > ): void {
        if ( this.nonEmpty ) {
            f( this.get() );
        }
    }

    /**
     * Returns this $option if it is nonempty, otherwise return the result of evaluating `alternative`.
     *
     * @param ob the alternative expression.
     */
    orElse<B extends A>( ob: Supplier< Optional<B> > ): Optional<A> {
        return this.isEmpty ? ob() : this;
    }

    // add methods
    apply1<B, C>( ob: Supplier< Optional<B> >, f: ( a: A, b: B ) => C ): Optional<C> {
        return this.flatMap( a => ob().map( b => f( a, b ) ) );
    }

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

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

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

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

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

    static apply<A>( a: A | null | undefined ): Optional<A> {
        return (a !== undefined && a !== null) ? new SomeImpl<A>( a ) : None;
    }
}

/**
 * Class `Some[A]` represents existing values of type `A`.
 */
export function Some<A>( a: A ): Optional<A> {
    return new SomeImpl( a );
}

//------------------------------------
//
// Implementation section.
//
//------------------------------------

class SomeImpl<A> extends Optional<A> {
    isEmpty: boolean = false;
    nonEmpty: boolean = true;

    get(): A {
        return this.value;
    }

    constructor( private value: A ) {
        super();
    }

    toString(): string {
        return 'Some(' + this.value + ')';
    }
}

class NoneImpl extends Optional<any> {
    isEmpty: boolean = true;
    nonEmpty: boolean = false;

    get(): any {
        throw new TypeError( 'None can not #get' );
    }

    toString(): string {
        return 'None';
    }
}

/**
 * This object represents non-existent values.
 */
export const None: Optional<any> = new NoneImpl();

// Builders

export class OptionalBuilder1<A, B> {
    constructor( private oa: Optional<A>,
                 private ob: Supplier< Optional<B> > ) {
    }

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

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

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

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

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

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

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

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

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

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

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

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

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