/** * The [[Either]] type represents an alternative between two value types. * A "left" value which is also conceptually tied to a failure, * or a "right" value which is conceptually tied to success. * * The code is organized through the class [[Left]], the class [[Right]], * and the type alias [[Either]] (Left or Right). * * Finally, "static" functions on Option are arranged in the class * [[EitherStatic]] and are accessed through the global constant Either. * * Examples: * * Either.right(5); * Either.left(2); * Either.right(5).map(x => x*2); * * Left has the extra [[Left.getLeft]] method that [[Right]] doesn't have. * Right has the extra [[Right.get]] method that [[Left]] doesn't have. */ import { Value } from "./Value"; import { Option } from "./Option"; import { LinkedList } from "./LinkedList"; import { Vector } from "./Vector"; import { Function0 } from "./Function"; import { WithEquality, areEqual, hasTrueEquality, getHashCode } from "./Comparison"; import { contractTrueEquality} from "./Contract"; /** * Holds the "static methods" for [[Either]] */ export class EitherStatic { /** * Constructs an Either containing a left value which you give. */ left(val: L): Either { return new Left(val); } /** * Constructs an Either containing a right value which you give. */ right(val: R): Either { return new Right(val); } /** * Curried type guard for Either * Sometimes needed also due to https://github.com/Microsoft/TypeScript/issues/20218 * * Vector.of(Either.right(2), Either.left(1)) * .filter(Either.isLeft) * .map(o => o.getLeft()) * => Vector.of(1) */ isLeft(e: Either): e is Left { return e.isLeft(); } /** * Curried type guard for Either * Sometimes needed also due to https://github.com/Microsoft/TypeScript/issues/20218 * * Vector.of(Either.right(2), Either.left(1)) * .filter(Either.isRight) * .map(o => o.get()) * => Vector.of(2) */ isRight(e: Either): e is Right { return e.isRight(); } /** * Turns a list of eithers in an either containing a list of items. * Useful in many contexts. * * Either.sequence(Vector.of( * Either.right(1), * Either.right(2))); * => Either.right(Vector.of(1,2)) * * But if a single element is Left, everything is discarded: * * Either.sequence(Vector.of( * Either.right(1), * Either.left(2), * Either.left(3))); * => Either.left(2) * * Also see [[EitherStatic.traverse]] */ sequence(elts:Iterable>): Either> { return Either.traverse(elts, x=>x); } /** * Takes a list, a function that can transform list elements * to eithers, then return an either containing a list of * the transformed elements. * * const getUserById: (x:number)=>Either = x => x > 0 ? * Either.right("user" + x.toString()) : Either.left("invalid id!"); * Either.traverse([4, 3, 2], getUserById); * => Either.right(Vector.of("user4", "user3", "user2")) * * But if a single element results in Left, everything is discarded: * * const getUserById: (x:number)=>Either = x => x > 0 ? * Either.right("user" + x.toString()) : Either.left("invalid id!"); * Either.traverse([4, -3, 2], getUserById); * => Either.left("invalid id!") * * Also see [[EitherStatic.sequence]] */ traverse(elts:Iterable, fn: (x:T)=>Either): Either> { let r = Vector.empty(); const iterator = elts[Symbol.iterator](); let curItem = iterator.next(); while (!curItem.done) { const v = fn(curItem.value); if (v.isLeft()) { return v; } r = r.append(v.get()); curItem = iterator.next(); } return Either.right>(r); } /** * Turns a list of eithers in an either containing a list of items. * Compared to [[EitherStatic.sequence]], sequenceAcc 'accumulates' * the errors, instead of short-circuiting on the first error. * * Either.sequenceAcc(Vector.of( * Either.right(1), * Either.right(2))); * => Either.right(Vector.of(1,2)) * * But if a single element is Left, you get all the lefts: * * Either.sequenceAcc(Vector.of( * Either.right(1), * Either.left(2), * Either.left(3))); * => Either.left(Vector.of(2,3)) */ sequenceAcc(elts:Iterable>): Either,Vector> { const [lefts,rights] = Vector.ofIterable(elts).partition(Either.isLeft); if (lefts.isEmpty()) { return Either.right,Vector>(rights.map(r => r.getOrThrow())); } return Either.left,Vector>(lefts.map(l => l.getLeft())); } /** * Applicative lifting for Either. * Takes a function which operates on basic values, and turns it * in a function that operates on eithers of these values ('lifts' * the function). The 2 is because it works on functions taking two * parameters. * * const lifted = Either.liftA2( * (x:number,y:number) => x+y, {} as string); * lifted( * Either.right(5), * Either.right(6)); * => Either.right(11) * * const lifted = Either.liftA2( * (x:number,y:number) => x+y, {} as string); * lifted( * Either.right(5), * Either.left("bad")); * => Either.left("bad") * * @param R1 the first right type * @param R2 the second right type * @param L the left type * @param V the new right type as returned by the combining function. */ liftA2(fn:(v1:R1,v2:R2)=>V, leftWitness?: L) : (p1:Either, p2:Either) => Either { return (p1,p2) => p1.flatMap(a1 => p2.map(a2 => fn(a1,a2))); } /** * Applicative lifting for Either. 'p' stands for 'properties'. * * Takes a function which operates on a simple JS object, and turns it * in a function that operates on the same JS object type except which each field * wrapped in an Either ('lifts' the function). * It's an alternative to [[EitherStatic.liftA2]] when the number of parameters * is not two. * * const fn = (x:{a:number,b:number,c:number}) => x.a+x.b+x.c; * const lifted = Either.liftAp(fn, {} as number); * lifted({ * a: Either.right(5), * b: Either.right(6), * c: Either.right(3)}); * => Either.right(14) * * const lifted = Either.liftAp( * x => x.a+x.b); * lifted({ * a: Either.right(5), * b: Either.left(2)}); * => Either.left(2) * * @param L the left type * @param A the object property type specifying the parameters for your function * @param B the type returned by your function, returned wrapped in an either by liftAp. */ liftAp(fn:(x:A)=>B, leftWitness?: L): (x: {[K in keyof A]: Either;}) => Either { return x => { const copy:A = {}; for (let p in x) { if (x[p].isLeft()) { return >x[p]; } copy[p] = x[p].getOrThrow(); } return Either.right(fn(copy)); } } /** * Applicative lifting for Either. 'p' stands for 'properties'. * Compared to [[EitherStatic.liftAp]], liftApAcc 'accumulates' * the errors, instead of short-circuiting on the first error. * * Takes a function which operates on a simple JS object, and turns it * in a function that operates on the same JS object type except which each field * wrapped in an Either ('lifts' the function). * It's an alternative to [[EitherStatic.liftA2]] when the number of parameters * is not two. * * const fn = (x:{a:number,b:number,c:number}) => x.a+x.b+x.c; * const lifted = Either.liftApAcc(fn, {} as number); * lifted({ * a: Either.right(5), * b: Either.right(6), * c:Either.right(3)}); * => Either.right(14) * * const fn = (x:{a:number,b:number,c:number}) => x.a+x.b+x.c; * const lifted = Either.liftApAcc(fn, {} as number); * lifted({ * a: Either.right(5), * b: Either.left(2), * c: Either.left(6)}); * => Either.left(Vector.of(2, 6)) * * @param L the left type * @param A the object property type specifying the parameters for your function * @param B the type returned by your function, returned wrapped in an either by liftAp. */ liftApAcc(fn:(x:A)=>B, leftWitness?: L): (x: {[K in keyof A]: Either;}) => Either,B> { const leftErrs: L[] = []; return x => { const copy:A = {}; for (let p in x) { const field = x[p]; if (field.isLeft()) { leftErrs.push(field.getLeft()); } else { copy[p] = x[p].getOrThrow(); } } if (leftErrs.length === 0) { return Either.right,B>(fn(copy)); } else { return Either.left,B>(Vector.ofIterable(leftErrs)); } } } /** * Take a partial function (may return undefined or throw), * and lift it to return an [[Either]] instead. * * Note that unlike the [[OptionStatic.lift]] version, if * the function returns undefined, the Either.lift version will throw * (the Option.lift version returns None()): if you want to do * pure side-effects which may throw, you're better off just using * javascript try blocks. * * When using typescript, to help the compiler infer the left type, * you can either pass a second parameter like `{} as `, or * call with `lift(...)`. * * const add = Either.lift((x:number,y:number) => x+y, {} as string); * add(1,2); * => Either.right(3) * * const undef = Either.lift((x:number,y:number,z:number) => undefined); * undef(1,2,3); * => throws * * const throws = Either.lift(() => {throw "x"}); * throws(); * => Either.left("x") */ lift(fn: (...args: T)=>U, witness?: L): (...args:T)=>Either { return (...args:T) => { try { const r = fn(...args); if (r !== undefined) { return Either.right(r); } } catch (err) { return Either.left(err); } throw new Error("liftEither got undefined!"); }; } /** * Take a no-parameter partial function (may return undefined or throw), * call it, and return an [[Either]] instead. * * Note that unlike the [[OptionStatic.try_]] version, if * the function returns undefined, this function will throw * (the Option.try_ version returns None()): if you want to do * pure side-effects which may throw, you're better off just using * javascript try blocks. * * When using typescript, to help the compiler infer the left type, * you can either pass a second parameter like `{} as `, or * call with `try_(...)`. * * Either.try_(Math.random, {} as string); * => Either.right(0.49884723907769635) * * Either.try_(() => undefined); * => throws * * Either.try_(() => {throw "x"}); * => Either.left("x") * * Also see [[Function0.liftEither]], [[OptionStatic.try_]], * [[OptionStatic.tryNullable]] */ try_(fn:()=>T, witness?: L): Either { return Either.lift<[],L,T>(fn)(); } } /** * The Either constant allows to call the either "static" methods */ export const Either = new EitherStatic(); /** * Either represents an alternative between two value types. * A "left" value which is also conceptually tied to a failure, * or a "right" value which is conceptually tied to success. * "static methods" available through [[EitherStatic]] */ export type Either = Left | Right; /** * Represents an [[Either]] containing a left value, * conceptually tied to a failure. * "static methods" available through [[EitherStatic]] * @param L the "left" item type 'failure' * @param R the "right" item type 'success' */ export class Left implements Value { constructor(private value: L) {} /** * @hidden */ readonly className: "Left" = undefined; // https://stackoverflow.com/a/47841595/516188 /** * Returns true since this is a Left */ isLeft(): this is Left { return true; } /** * Returns false since this is a Left */ isRight(): this is Right { return false; } /** * Returns true if this is either is a right and contains the value you give. */ contains(val: R&WithEquality): boolean { return false; } /** * If this either is a right, applies the function you give * to its contents and build a new right either, otherwise return this. */ map(fn: (x:R)=>U): Either { return this; } /** * If this either is a right, call the function you give with * the contents, and return what the function returns, else * returns this. * This is the monadic bind. */ flatMap(fn: (x:R)=>Either): Either { return this; } /** * If this either is a left, call the function you give with * the left value and return a new either left with the result * of the function, else return this. */ mapLeft(fn: (x:L)=>U): Either { return new Left(fn(this.value)); } /** * Map the either: you give a function to apply to the value, * a function in case it's a left, a function in case it's a right. */ bimap(fnL: (x:L)=>S,fnR: (x:R)=>T): Either { return new Left(fnL(this.value)); } /** * "filter" the either. If it was a Left, it stays a Left. * If it was a Right and the predicate you pass returns * true for its value, return the either unchanged. * But if it was a left and the predicate returns false, * return a Left with the value returned by the function * passed as second parameter. * * Either.right(-3) * .filter(x => x >= 0, v => "got negative value: " + v); * => Either.left("got negative value: -3") */ filter(p: (x:R)=>boolean, filterVal: (x:R)=>L): Either { return this; } /** * Combines two eithers. If this either is a right, returns it. * If it's a left, returns the other one. */ orElse(other: Either): Either { return other; } /** * Execute a side-effecting function if the either * is a right; returns the either. */ ifRight(fn: (x:R)=>void): Either { return this; } /** * Execute a side-effecting function if the either * is a left; returns the either. */ ifLeft(fn: (x:L)=>void): Either { fn(this.value); return this; } /** * Handle both branches of the either and return a value * (can also be used for side-effects). * This is the catamorphism for either. * * Either.right(5).match({ * Left: x => "left " + x, * Right: x => "right " + x * }); * => "right 5" */ match(cases: {Left: (v:L)=>U, Right: (v:R)=>U}): U { return cases.Left(this.value); } /** * If this either is a right, return its value, else throw * an exception. * You can optionally pass a message that'll be used as the * exception message, or an Error object. */ getOrThrow(errorInfo?: Error|string): R { if (typeof errorInfo === 'string') { throw new Error(errorInfo || "Left.getOrThrow called!"); } throw errorInfo || new Error("Left.getOrThrow called!"); } /** * If this either is a right, return its value, else return * the value you give. */ getOrElse(other: R): R { return other; } /** * Get the value contained in this left. * NOTE: we know it's there, since this method * belongs to Left, not Either. */ getLeft(): L { return this.value; } /** * If this either is a left, return its value, else throw * an exception. * You can optionally pass a message that'll be used as the * exception message. */ getLeftOrThrow(message?: string): L { return this.value; } /** * If this either is a left, return its value, else return * the value you give. */ getLeftOrElse(other: L): L { return this.value; } /** * Convert this either to an option, conceptually dropping * the left (failing) value. */ toOption(): Option { return Option.none(); } /** * Convert to a vector. If it's a left, it's the empty * vector, if it's a right, it's a one-element vector with * the contents of the either. */ toVector(): Vector { return Vector.empty(); } /** * Convert to a list. If it's a left, it's the empty * list, if it's a right, it's a one-element list with * the contents of the either. */ toLinkedList(): LinkedList { return LinkedList.empty(); } /** * Transform this value to another value type. * Enables fluent-style programming by chaining calls. */ transform(converter:(x:Either)=>U): U { return converter(this); } hasTrueEquality(): boolean { return (this.value && (this.value).hasTrueEquality) ? (this.value).hasTrueEquality() : hasTrueEquality(this.value); } /** * Get a number for that object. Two different values * may get the same number, but one value must always get * the same number. The formula can impact performance. */ hashCode(): number { return getHashCode(this.value); } /** * Two objects are equal if they represent the same value, * regardless of whether they are the same object physically * in memory. */ equals(other: Either): boolean { if (other === this) { return true; } if ((!other) || (!other.isRight) || other.isRight()) { return false; } const leftOther = >other; contractTrueEquality("Either.equals", this, leftOther); return areEqual(this.value, leftOther.value); } /** * Get a human-friendly string representation of that value. */ toString(): string { return "Left(" + this.value + ")"; } /** * Used by the node REPL to display values. */ inspect(): string { return this.toString(); } } /** * Represents an [[Either]] containing a success value, * conceptually tied to a success. * "static methods" available through [[EitherStatic]] * @param L the "left" item type 'failure' * @param R the "right" item type 'success' */ export class Right implements Value { constructor(private value: R) {} /** * @hidden */ readonly className: "Right" = undefined; // https://stackoverflow.com/a/47841595/516188 /** * Returns false since this is a Right */ isLeft(): this is Left { return false; } /** * Returns true since this is a Right */ isRight(): this is Right { return true; } /** * Returns true if this is either is a right and contains the value you give. */ contains(val: R&WithEquality): boolean { return areEqual(this.value, val); } /** * If this either is a right, applies the function you give * to its contents and build a new right either, otherwise return this. */ map(fn: (x:R)=>U): Either { return new Right(fn(this.value)); } /** * If this either is a right, call the function you give with * the contents, and return what the function returns, else * returns this. * This is the monadic bind. */ flatMap(fn: (x:R)=>Either): Either { return fn(this.value); } /** * If this either is a left, call the function you give with * the left value and return a new either left with the result * of the function, else return this. */ mapLeft(fn: (x:L)=>U): Either { return this; } /** * Map the either: you give a function to apply to the value, * a function in case it's a left, a function in case it's a right. */ bimap(fnL: (x:L)=>S,fnR: (x:R)=>T): Either { return new Right(fnR(this.value)); } /** * "filter" the either. If it was a Left, it stays a Left. * If it was a Right and the predicate you pass returns * true for its value, return the either unchanged. * But if it was a left and the predicate returns false, * return a Left with the value returned by the function * passed as second parameter. * * Either.right(-3) * .filter(x => x >= 0, v => "got negative value: " + v); * => Either.left("got negative value: -3") */ filter(p: (x:R)=>boolean, filterVal: (x:R)=>L): Either { if (p(this.value)) { return this; } return new Left(filterVal(this.value)); } /** * Combines two eithers. If this either is a right, returns it. * If it's a left, returns the other one. */ orElse(other: Either): Either { return this; } /** * Execute a side-effecting function if the either * is a right; returns the either. */ ifRight(fn: (x:R)=>void): Either { fn(this.value); return this; } /** * Execute a side-effecting function if the either * is a left; returns the either. */ ifLeft(fn: (x:L)=>void): Either { return this; } /** * Handle both branches of the either and return a value * (can also be used for side-effects). * This is the catamorphism for either. * * Either.right(5).match({ * Left: x => "left " + x, * Right: x => "right " + x * }); * => "right 5" */ match(cases: {Left: (v:L)=>U, Right: (v:R)=>U}): U { return cases.Right(this.value); } /** * Get the value contained in this right. * NOTE: we know it's there, since this method * belongs to Right, not Either. */ get(): R { return this.value; } /** * If this either is a right, return its value, else throw * an exception. * You can optionally pass a message that'll be used as the * exception message, or an Error object. */ getOrThrow(errorInfo?: Error|string): R { return this.value; } /** * If this either is a right, return its value, else return * the value you give. */ getOrElse(other: R): R { return this.value; } /** * If this either is a left, return its value, else throw * an exception. * You can optionally pass a message that'll be used as the * exception message. */ getLeftOrThrow(message?: string): L { throw message || "Left.getOrThrow called!"; } /** * If this either is a left, return its value, else return * the value you give. */ getLeftOrElse(other: L): L { return other; } /** * Convert this either to an option, conceptually dropping * the left (failing) value. */ toOption(): Option { return Option.of(this.value); } /** * Convert to a vector. If it's a left, it's the empty * vector, if it's a right, it's a one-element vector with * the contents of the either. */ toVector(): Vector { return Vector.of(this.value); } /** * Convert to a list. If it's a left, it's the empty * list, if it's a right, it's a one-element list with * the contents of the either. */ toLinkedList(): LinkedList { return LinkedList.of(this.value); } /** * Transform this value to another value type. * Enables fluent-style programming by chaining calls. */ transform(converter:(x:Either)=>U): U { return converter(this); } hasTrueEquality(): boolean { return (this.value && (this.value).hasTrueEquality) ? (this.value).hasTrueEquality() : hasTrueEquality(this.value); } /** * Get a number for that object. Two different values * may get the same number, but one value must always get * the same number. The formula can impact performance. */ hashCode(): number { return getHashCode(this.value); } /** * Two objects are equal if they represent the same value, * regardless of whether they are the same object physically * in memory. */ equals(other: Either): boolean { if (other === this) { return true; } if ((!other) || (!other.isRight) || (!other.isRight())) { return false; } const rightOther = >other; contractTrueEquality("Either.equals", this, rightOther); return areEqual(this.value, rightOther.value); } /** * Get a human-friendly string representation of that value. */ toString(): string { return "Right(" + this.value + ")"; } /** * Used by the node REPL to display values. */ inspect(): string { return this.toString(); } }