/** * FnEither is also known as ReaderEither. In essence a FnEither is * a function that returns an either. This pattern can be used as * a validation, a failable computation, a computation resulting * in a "Choice", and many other things. * * @module FnEither */ import type { In, Kind, Out } from "./kind.ts"; import type { Applicable } from "./applicable.ts"; import type { Bimappable } from "./bimappable.ts"; import type { Combinable } from "./combinable.ts"; import type { Composable } from "./composable.ts"; import type { Either } from "./either.ts"; import type { Failable, Tap } from "./failable.ts"; import type { Bind, Flatmappable } from "./flatmappable.ts"; import type { Fn } from "./fn.ts"; import type { BindTo, Mappable } from "./mappable.ts"; import type { Predicate } from "./predicate.ts"; import type { Premappable } from "./premappable.ts"; import type { Refinement } from "./refinement.ts"; import type { Wrappable } from "./wrappable.ts"; import * as E from "./either.ts"; import * as F from "./fn.ts"; import { createTap } from "./failable.ts"; import { createBind } from "./flatmappable.ts"; import { createBindTo } from "./mappable.ts"; /** * A FnEither, also known as ReaderEither, is a type over a variadic * javascript function that returns an Either. * ie. (a: number, b: string) => Either can be * a FnEither. As an algebraic data type, the associated type class * instances for Fn are limited to single variable inputs so they will * look like (a: number) => Either, with only one * argument. The purposes of a FnEither are many and varied, some * common purposes are: failable computation, reading values from a * shared environment, and sub-computations in a modified * environment. * * Currently, there is no implementation of Chain recursion or * trampolining for FnEIther implemented, but it is likely to be a future * feature. Once implemented Fn will gain some much needed stack safety. * * @since 2.0.0 */ export type FnEither = Fn>; /** * A FnEither type over any, useful for constraining generics that * take or return FnEithers. * * @since 2.0.0 */ // deno-lint-ignore no-explicit-any export type AnyFnEither = FnEither; /** * Specifies FnEither as a Higher Kinded Type, with * covariant parameter A and B corresponding to the 0th * and 1st indices of any Substitutions and a contravariant * parameter D corresponding to the 0th index of * any Substititions. The FnEither KindFnEither is unique in that * it constrains the FnEither type to taking a single * argument for the purposes of type substitution * while the implementations of FnEither combinators such * as map, flatmap, etc are mostly variadic (multiple * arguments). * * @since 2.0.0 */ export interface KindFnEither extends Kind { readonly kind: FnEither, Out, Out>; } /** * Specifies FnEither as a Higher Kinded Type, with * covariant parameter A corresponding to the 0th * index of any Substitutions and a contravariant * parameter D corresponding to the 0th index of * any Substititions. KindRightFnEither curries the Left parameter * of the output Either. This is useful when one * needs to Fix the Left output with a Combinable or * some other collection algebraic structure. * * @since 2.0.0 */ export interface KindRightFnEither extends Kind { readonly kind: FnEither, B, Out>; } /** * Wrap any function in a try catch block. The returned * function will lazily call and handle any throwing of * the wrapped function. Non-throwing calls will be * returned in a Right, and throwing calls will have * their error and arguments passed to the onThrow function * before being returned in a Left. * * @example * ```ts * import { tryCatch } from "./fn_either.ts"; * import { todo } from "./fn.ts"; * * const throws = tryCatch(todo, () => "Failed!"); * const returns = tryCatch((n: number) => n, () => "Failed!"); * * const result1 = throws()(0); // Left("Failed!"); * const result2 = returns(1)(0); // Right(1); * ``` * * @since 2.0.0 */ export function tryCatch( ua: (...d: D) => A, onThrow: (e: unknown, d: D) => B, ): (...d: D) => FnEither { return F.handleThrow( ua, right, (e, d) => left(onThrow(e, d)), ); } /** * Create a FnEither that always returns a Left(B). * * @example * ```ts * import { left } from "./fn_either.ts"; * * const leftNumber = left(1); * * const result = leftNumber(0); // Left(1); * ``` * * @since 2.0.0 */ export function left( left: B, ): FnEither { return F.wrap(E.left(left)); } /** * Create a FnEither that always returns a Right(A). * * @example * ```ts * import { right } from "./fn_either.ts"; * * const rightNumber = right(1); * * const result = rightNumber(null); // Right(1); * ``` * * @since 2.0.0 */ export function right( right: A, ): FnEither { return F.wrap(E.right(right)); } /** * Turn an Either into a FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import * as E from "./either.ts"; * * const left = E.left(1); * const right = E.right(1); * * const fnLeft = FE.fromEither(left); * const fnRight = FE.fromEither(right); * * const result1 = fnLeft(null); // Left(1); * const result2 = fnRight(null); // Right(1); * ``` * * @since 2.0.0 */ export function fromEither( ua: Either, ): FnEither { return F.wrap(ua); } /** * Lift a Fn into FnEither<[D], never, A> * * @example * ```ts * import { fromFn } from "./fn_either.ts"; * * const double = (first: number) => first + first; * const lifted = fromFn(double); * * const result = lifted(1); // Right(2) * ``` * * @since 2.0.0 */ export function fromFn( fda: Fn, ): FnEither { return F.flow(fda, E.right); } /** * Create a FnEither from a Predicate or a Refinement. * If the Predicate or Refinement returns true then the * FnEither returns Right, otherwise it returns Left. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const isPositive = (n: number) => n > 0; * const computation = FE.fromPredicate(isPositive); * * const result1 = computation(0); // Left(0) * const result2 = computation(1); // Right(1) * ``` * * @since 2.0.0 */ export function fromPredicate( refinement: Refinement, ): FnEither; export function fromPredicate( predicate: Predicate, ): FnEither; export function fromPredicate( predicate: Predicate, ): FnEither { return (a) => predicate(a) ? E.right(a) : E.left(a); } /** * An alias for right. Creates a FnEither from a value. The created * FnEither does not require any arguments, but can widen when used * in a flatmap. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const computation = pipe( * FE.id(), // Ask for a number * FE.map(n => n + 1), // Add one * FE.flatmap(_ => FE.wrap("Hello")), // Forget about the number * ); * * const result = computation(1); // Right("Hello") * ``` * * @since 2.0.0 */ export function wrap( a: A, ): FnEither { return right(a); } /** * @since 2.0.0 */ export function fail( b: B, ): FnEither { return left(b); } /** * Given a FnEither returning a function A => I and a FnEither returning * a value A, combine them into a FnEither returning an I. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * type Person = { name: string; age: number }; * * const person = (name: string) => (age: number): Person => ({ name, age }); * * const result = pipe( * FE.wrap(person), * FE.apply(FE.wrap("Brandon")), * FE.apply(FE.wrap(37)), * ); // FnEither<[], never, Person> * ``` * * @since 2.0.0 */ export function apply( ua: FnEither, ): (ufai: FnEither I>) => FnEither { return (ufai) => (d) => F.pipe(ufai(d), E.apply(ua(d))); } /** * Provide an alternative FnEither in the event that an original * FnEither returns Left. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * FE.left("Oh no I broke!"), * FE.alt(FE.right("But I work")), * )(null); // Right("But I work") * ``` * * @since 2.0.0 */ export function alt( ub: FnEither, ): (ua: FnEither) => FnEither { return (ua) => (d) => { const e = ua(d); return E.isLeft(e) ? ub(d) : e; }; } /** * Map over the left and right return values of a FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import * as E from "./either.ts"; * import { pipe } from "./fn.ts"; * * const boundedValue = (n: number) => n > 10 || n < 0 ? E.left(n) : E.right(n); * * const log = pipe( * boundedValue, * FE.bimap(n => `Out of bounds: ${n}`, n => `Received a good value: ${n}`), * ); * * const result1 = log(1); // Right("Received a good value: 1") * const result2 = log(20); // Left("Out of bounds: 20") * ``` * * @since 2.0.0 */ export function bimap( fbj: (b: B) => J, fai: (a: A) => I, ): ( ua: FnEither, ) => FnEither { return F.map(E.bimap(fbj, fai)); } /** * Map over the right return value of a FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * FE.id(), * FE.map(n => n + 1), * )(0); // Right(1) * ``` * * @since 2.0.0 */ export function map( fai: (a: A) => I, ): ( ua: FnEither, ) => FnEither { return F.map(E.map(fai)); } /** * Map over the left return value of a FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const result1 = pipe( * FE.id(), * FE.mapSecond((n: number) => n + 1), * )(0); // Right(0) * * const result2 = pipe( * FE.idLeft(), * FE.mapSecond(n => n + 1), * )(0); // Left(1) * ``` * * @since 2.0.0 */ export function mapSecond( fbj: (b: B) => J, ): ( ua: FnEither, ) => FnEither { return F.map(E.mapSecond(fbj)); } /** * Flatten nested FnEithers with the same input and * left types. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * FE.right(FE.right(1)), * FE.join, * )(null); // Right(1) * ``` * * @since 2.0.0 */ export function join( tua: FnEither>, ): FnEither { return (d) => F.pipe(tua(d), E.flatmap((fn) => fn(d))); } /** * Chain the right result of one FnEither into another * FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * FE.id(), * FE.flatmap(s => FE.right(s.length)), * )("Hello"); // Right(5) * ``` * * @since 2.0.0 */ export function flatmap( fati: (a: A) => FnEither, ): ( ua: FnEither, ) => FnEither { return (ua) => (d) => { const e = ua(d); return E.isLeft(e) ? e : fati(e.right)(d); }; } /** * Chain the left result of one FnEither into another * FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * FE.id(), * FE.flatmap(s => FE.left(s.length)), * FE.recover(n => FE.right(String(n))), * )("Hello"); // Right("5") * ``` * * @since 2.0.0 */ export function recover( fbtj: (b: B) => FnEither, ): ( ua: FnEither, ) => FnEither { return (ua) => (d) => { const e = ua(d); return E.isRight(e) ? e : fbtj(e.left)(d); }; } /** * Map over the input value of a FnEither. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * // This has type FnEither<[Date], never, number> * const computation = pipe( * FE.id(), * FE.premap((d: Date) => d.valueOf()), * ); * * const result = computation(new Date(0)); // Right(0) * ``` * * @since 2.0.0 */ export function premap( fld: (l: L) => D, ): (ua: FnEither) => FnEither { return (ua) => F.flow(fld, ua); } /** * Map over the input of a FnEither contravariantly and the * right result of a FnEither covariantly. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * // This has type FnEither<[Date], never, string> * const computation = pipe( * FE.id(), * FE.dimap( * (d: Date) => d.valueOf(), * String, * ), * ); * * const result = computation(new Date(0)); // Right('0') * ``` * * @since 2.0.0 */ export function dimap( fld: (l: L) => D, fai: (a: A) => I, ): (ua: FnEither) => FnEither { return F.flow(premap(fld), map(fai)); } /** * Perform the same function as Reader ask. Given a type A * (and optionally a type B), return a FnEither<[A], B, A>. * This is useful for starting a FnEither flatmap. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const computation = FE.id(); * * const result1 = computation(1); // Right(1); * const result2 = computation(2); // Right(2); * ``` * * @since 2.0.0 */ export function id(): FnEither { return E.right; } /** * Perform the same function as Reader askLeft. Given a type B * (and optionally a type A), return a FnEither<[B], B, A>. * This is useful for starting a FnEither flatmap with a left value. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const computation = FE.idLeft(); * * const result1 = computation(1); // Left(1); * const result2 = computation(2); // Left(2); * ``` * * @since 2.0.0 */ export function idLeft(): FnEither { return E.left; } /** * Compose two FnEithers, passing the right value of the first * into the second. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { pipe } from "./fn.ts"; * * const isPositive = (n: number) => n > 0; * const isInteger = (n: number) => Number.isInteger(n); * * const isPositiveInteger = pipe( * FE.fromPredicate(isPositive), * FE.compose(FE.fromPredicate(isInteger)), * ); * * const result1 = isPositiveInteger(0); // Left(0) * const result2 = isPositiveInteger(1); // Right(1) * const result3 = isPositiveInteger(1.1); // Left(1.1) * ``` * * @since 2.0.0 */ export function compose( second: FnEither, ): ( first: FnEither, ) => FnEither { return (first) => F.flow(first, E.flatmap(second)); } /** * Create a Flatmappable for FnEither where left values are combined using the * supplied Combinable. * * @example * ```ts * import * as FE from "./fn_either.ts"; * import { InitializableNumberSum } from "./number.ts"; * import { pipe } from "./fn.ts"; * * const { apply } = FE.getRightFlatmappable(InitializableNumberSum); * * const result1 = pipe( * FE.left(1), * apply(FE.left(1)), * ); // Left(2) * ``` * * @since 2.0.0 */ export function getRightFlatmappable( { combine }: Combinable, ): Flatmappable> { return ({ wrap, apply: (ua) => (ufai) => (c) => { const efai = ufai(c); const ea = ua(c); return E.isLeft(efai) ? (E.isLeft(ea) ? E.left(combine(efai.left)(ea.left)) : efai) : (E.isLeft(ea) ? ea : E.right(efai.right(ea.right))); }, map, flatmap, }); } /** * @since 2.0.0 */ export const ApplicableFnEither: Applicable = { apply, map, wrap, }; /** * The canonical implementation of Bimappable for FnEither. It contains * the methods bimap and mapSecond. * * @since 2.0.0 */ export const BimappableFnEither: Bimappable = { map, mapSecond, }; /** * @since 2.0.0 */ export const ComposableFnEither: Composable = { compose, id }; /** * The canonical implementation of Flatmappable for FnEither. It contains * the methods wrap, apply, map, and flatmap. * * @since 2.0.0 */ export const FlatmappableFnEither: Flatmappable = { wrap, apply, map, flatmap, }; /** * @since 2.0.0 */ export const FailableFnEither: Failable = { apply, flatmap, map, wrap, fail, alt, recover, }; /** * @since 2.0.0 */ export const MappableFnEither: Mappable = { map }; /** * The canonical implementation of Premappable for FnEither. It contains * the method premap. * * @since 2.0.0 */ export const PremappableFnEither: Premappable = { premap, map, dimap, }; /** * @since 2.0.0 */ export const WrappableFnEither: Wrappable = { wrap }; /** * @since 2.0.0 */ export const tap: Tap = createTap(FailableFnEither); /** * @since 2.0.0 */ export const bind: Bind = createBind(FlatmappableFnEither); /** * @since 2.0.0 */ export const bindTo: BindTo = createBindTo(MappableFnEither);