diff --git a/examples/fn/fn_async_either.ts b/examples/fn/fn_async_either.ts new file mode 100644 index 0000000..9f1e709 --- /dev/null +++ b/examples/fn/fn_async_either.ts @@ -0,0 +1,41 @@ +import * as F from "../../fn.ts"; +import * as FAE from "../../fn_async_either.ts"; + +const add1 = await F.pipe( + FAE.ask(), + FAE.map((n) => n + 1), + FAE.match(() => 0, F.identity), +); + +add1(2).then((n) => console.log(n)); // 3 + +const failable = F.pipe( + FAE.tryCatch( + (n: number) => n % 2 === 0 ? Promise.resolve("even") : Promise.reject("odd"), + () => "fail: odd", + ), + FAE.match(() => "fail: even", F.identity), +); + +failable(1).then((n) => console.log(n)); // odd; + +const askN = FAE.ask(); + +const divide = ([dividend, divisor]: [number, number]) => dividend / divisor; +const onError = (error: unknown, [[dividend, divisor]]: [[number, number]]) => + `Error dividing ${dividend} by ${divisor}`; +const safeDivide = FAE.tryCatch(divide, onError); + +safeDivide([10, 2]); // returns Right(5) +safeDivide([10, 0]); // returns Left("Error dividing 10 by 0") + +const _fetch = FAE.tryCatch( + fetch, + (error, args) => ({ message: "Fetch Error", error, args }), +); + +const t1 = await _fetch("blah")(); +const t2 = await _fetch("https://deno.land/")(); + +const computation = FAE.left(1); +const result = await computation()(); diff --git a/fn_async_either.ts b/fn_async_either.ts new file mode 100644 index 0000000..d6943ad --- /dev/null +++ b/fn_async_either.ts @@ -0,0 +1,389 @@ +import type { Kind, Out } from "./kind.ts"; +import type { Alt } from "./alt.ts"; +import type { Bifunctor } from "./bifunctor.ts"; +import type { Either } from "./either.ts"; +import type { Monad } from "./monad.ts"; +import type { Async } from "./async.ts"; +import type { AsyncEither } from "./async_either.ts"; + +import * as A from "./async.ts"; +import * as AE from "./async_either.ts"; +import * as E from "./either.ts"; +import * as F from "./fn.ts"; +import { Fn, pipe } from "./fn.ts"; + +/** + * The FnAsyncEither type can best be thought of as an asynchronous function that + * takes dependencies (d: D) and returns an AsyncEither. + * (...d: D[]) => AsyncEither + * Builds on AsyncEither and enables injecting an environment into an async computation + * + * @since 2.0.0 + */ +export type FnAsyncEither = + | (() => AsyncEither) + | ((...d: D[]) => AsyncEither); + +export interface URI extends Kind { + readonly kind: FnAsyncEither, Out, Out>; +} + +/** + * Constructs a FnAsyncEither from a value and wraps it in an inner *Left*. + * Traditionally signaling a failure + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * import * as E from "./either.ts"; + * + * const computation = FAE.left(1); + * const result = await computation()(); + * + * assertEquals(result, E.left(1)); + * ``` + * + * @since 2.0.0 + */ +export function left( + left: B, +): FnAsyncEither { + return F.of(AE.left(left)); +} + +/** + * Constructs an FnAsyncEither from a value and wraps it in an inner *Right*. + * Traditionally signaling a successful computation + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * import * as E from "./either.ts"; + * + * const computation = FAE.right(1); + * const result = await computation()(); + * + * assertEquals(result, E.right(1)); + * ``` + * + * @since 2.0.0 + */ +export function right( + right: A, +): FnAsyncEither { + return F.of(AE.right(right)); +} + +/** + * Wraps a Async of A in a try-catch block which upon failure returns B instead. + * Upon success returns a *Right* and *Left* for a failure. + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * + * const _fetch = FAE.tryCatch( + * fetch, + * (error, args) => ({ message: "Fetch Error", error, args }) + * ); + * + * const t1 = await _fetch("blah")(); + * assertEquals(t1.tag, "Left"); + * + * const t2 = await _fetch("https://deno.land/")(); + * assertEquals(t2.tag, "Right"); + * ``` + * + * @since 2.0.0 + */ +export function tryCatch( + fasr: (d: D) => A | PromiseLike, + onThrow: (e: unknown, as: [D]) => B, +): FnAsyncEither { + return (d: D) => AE.tryCatch(fasr, onThrow)(d); +} + +/** + * Lift an always succeeding computation (Fn) into an FnAsyncEither + */ +export function fromFn(fa: Fn): FnAsyncEither { + return (d: D) => of(fa(d))(d); +} + +/** + * Lift an always succeeding async computation (Async) into a FnAsyncEither + */ +export function fromAsync( + ta: Async, +): FnAsyncEither { + return F.of(() => ta().then(E.right)); +} + +/** + * Lifts an Either into a FnAsyncEither + */ +export function fromEither(ta: Either): FnAsyncEither { + return F.pipe(ta, A.of, F.of); +} + +/** + * Lifts an AsyncEither into an FnAsyncEither + */ +export function fromAsyncEither( + ta: AE.AsyncEither, +): FnAsyncEither { + return F.of(ta); +} + +/** + * Pointed constructor of(A) => FnAsyncEither + */ +export function of(a: A): FnAsyncEither { + return right(a); +} + +/** + * Pointed constructor throwError(B) => FnAsyncEither + */ +export function throwError( + b: B, +): FnAsyncEither { + return left(b); +} + +/** + * A dual map function that maps over both *Left* and *Right* side of + * an FnAsyncEither. + */ +export function bimap( + fbj: (b: B) => J, + fai: (a: A) => I, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta) => pipe(ta, F.map(A.map(E.bimap(fbj, fai)))); +} + +/** + * Map a function over the *Right* side of an FnAsyncEither + */ +export function map( + fai: (a: A) => I, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta) => pipe(ta, F.map(AE.map(fai))); +} + +/** + * Map a function over the *Left* side of an FnAsyncEither + */ +export function mapLeft( + fbj: (b: B) => J, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta) => pipe(ta, F.map(A.map(E.mapLeft(fbj)))); +} + +/** + * Apply an argument to a function under the *Right* side. + */ +export function apParallel( + ua: FnAsyncEither, +): ( + ufai: FnAsyncEither I>, +) => FnAsyncEither { + return (ufai: FnAsyncEither I>) => (d: K | D) => AE.apParallel(ua(d as D))(ufai(d as K)); +} + +/** + * Sequentially apply arguments + */ +export function apSequential( + ua: FnAsyncEither, +): ( + ufai: FnAsyncEither I>, +) => FnAsyncEither { + return (ufai: FnAsyncEither I>) => (d: K | D) => AE.apSequential(ua(d as D))(ufai(d as K)); +} + +/** + * Chain AsyncEither based computations together in a pipeline + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * import * as E from "./either.ts"; + * import { pipe } from "./fn.ts"; + * + * const ta = pipe( + * FAE.of(1), + * FAE.chain(n => FAE.of(n*2)), + * FAE.chain(n => FAE.of(n**2)) + * ) + * + * assertEquals(await ta()(), E.right(4)) + * ``` + */ +export function chain( + fati: (a: A) => FnAsyncEither, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta: FnAsyncEither) => (d: K | D) => async () => { + const ea = await ta(d as D)(); + if (E.isLeft(ea)) { + return ea; + } else { + return await fati(ea.right)(d as K)(); + } + }; +} + +export function chainFirst( + fati: (a: A) => FnAsyncEither, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta) => (d: D) => async () => { + const ea = await ta(d)(); + if (E.isLeft(ea)) { + return ea; + } else { + const ei = await fati(ea.right)(d)(); + return E.isLeft(ei) ? ei : ea; + } + }; +} + +/** + * Chain FnAsyncEither based failures, *Left* sides, useful for recovering + * from error conditions. + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * import * as E from "./either.ts"; + * import { pipe } from "./fn.ts"; + * + * const ta = pipe( + * FAE.throwError(1), + * FAE.chainLeft(n => FAE.of(n*2)), + * FAE.chain(n => FAE.of(n**2)) + * ) + * + * assertEquals(await ta(), E.right(4)) + * ``` + */ +export function chainLeft( + fbtj: (b: B) => FnAsyncEither, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta) => (d: D) => async () => { + const ea = await ta(d)(); + return E.isLeft(ea) ? fbtj(ea.left)(d)() : ea; + }; +} + +/** + * Flatten an FnAsyncEither wrapped in an FnAsyncEither + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * import * as E from "./either.ts"; + * import { pipe } from "./fn.ts"; + * + * const ta = pipe( + * TE.of(1), + * TE.map(n => TE.of(n*2)), + * TE.join, + * TE.chain(n => TE.of(n**2)) + * ) + * + * assertEquals(await ta(), E.right(4)) + * ``` + */ +export function join( + tta: FnAsyncEither>, +): FnAsyncEither { + return pipe(tta, chain(F.identity)); +} + +/** + * Provide an alternative for a failed computation. + * Useful for implementing defaults. + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as FAE from "./fn_async_either.ts"; + * import * as E from "./either.ts"; + * import { pipe } from "./fn.ts"; + * + * const ta = pipe( + * TE.throwError(1), + * TE.alt(TE.of(2)), + * ) + * + * assertEquals(await ta(), E.right(2)) + * ``` + */ +export function alt( + ti: FnAsyncEither, +): (ta: FnAsyncEither) => FnAsyncEither { + return (ta) => (d: D) => async () => { + const ea = await ta(d)(); + return E.isLeft(ea) ? ti(d)() : ea; + }; +} + +/** + * Fold away the inner Either from the `FnAsyncEither` leaving us with the + * result of our computation in the form of a `Async` + * + * ```ts + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import * as TE from "./async_either.ts"; + * import * as T from "./async.ts"; + * import { flow, identity } from "./fn.ts"; + * + * const hello = flow( + * TE.match(() => "World", identity), + * T.map((name) => `Hello ${name}!`), + * ); + * + * assertEquals(await hello(TE.right("Functional!"))(), "Hello Functional!!"); + * assertEquals(await hello(TE.left(Error))(), "Hello World!"); + * ``` + */ +export function match( + onLeft: (left: L) => B, + onRight: (right: R) => B, +): (ta: FnAsyncEither) => (d: D) => Promise { + return (ta) => (d: D) => ta(d)().then(E.match(onLeft, onRight)); +} + +export function id(): FnAsyncEither { + return fromFn(F.identity); +} +export function ask() { + return id(); +} +export const asks = (f: (d: D) => A) => (d: D) => right(f(d)); +export const local = (f: (d: D) => D) => (ta: FnAsyncEither) => (d: D) => ta(f(d)); +export const askFn = (ta: FnAsyncEither) => (d: D) => ta(d); + +// This leaks async ops so we cut it for now. +//export const timeout = (ms: number, onTimeout: () => E) => +// (ta: AsyncEither): AsyncEither => +// () => Promise.race([ta(), wait(ms).then(flow(onTimeout, E.left))]); + +export const BifunctorAsyncEither: Bifunctor = { bimap, mapLeft }; + +export const MonadAsyncEitherParallel: Monad = { + of, + ap: apParallel, + map, + join, + chain, +}; + +export const AltAsyncEither: Alt = { alt, map }; + +export const MonadAsyncEitherSequential: Monad = { + of, + ap: apSequential, + map, + join: join, + chain: chain, +}; diff --git a/testing/fn_async_either.test.ts b/testing/fn_async_either.test.ts new file mode 100644 index 0000000..25f84c6 --- /dev/null +++ b/testing/fn_async_either.test.ts @@ -0,0 +1,174 @@ +import { assertEquals } from "https://deno.land/std@0.103.0/testing/asserts.ts"; + +import * as FAE from "../fn_async_either.ts"; +import * as AE from "../async_either.ts"; +import * as E from "../either.ts"; +import * as F from "../fn.ts"; +import { pipe } from "../fn.ts"; +import { then, wait } from "../promise.ts"; + +const add = (n: number) => n + 1; + +const assertEqualsT = async ( + a: FAE.FnAsyncEither, + b: FAE.FnAsyncEither, +) => assertEquals(await a({})(), await b({})()); + +function throwSync(n: number): number { + if (n % 2 === 0) { + throw new Error(`Number '${n}' is divisible by 2`); + } + return n; +} + +async function throwAsync(n: number): Promise { + await wait(200); + if (n % 2 === 0) { + return Promise.reject(`Number '${n}' is divisible by 2`); + } + return n; +} + +Deno.test("FnAsyncEither left", async () => { + await assertEqualsT(F.of(AE.left(1)), FAE.left(1)); +}); + +Deno.test("FnAsyncEither right", async () => { + await assertEqualsT(() => AE.right(1), FAE.right(1)); +}); + +Deno.test("FnAsyncEither tryCatch", async (t) => { + await t.step("Sync", async () => { + const computation = FAE.tryCatch(throwSync, () => "Bad"); + await assertEqualsT( + () => computation(1), + FAE.right(1), + ); + await assertEqualsT( + () => computation(2), + FAE.left("Bad"), + ); + }); + await t.step("Async", async () => { + const computation = FAE.tryCatch(throwAsync, () => "Bad"); + await assertEqualsT( + () => computation(1), + FAE.right(1), + ); + await assertEqualsT( + () => computation(2), + FAE.left("Bad"), + ); + }); +}); + +Deno.test("FnAsyncEither fromEither", async () => { + await assertEqualsT(FAE.fromEither(E.left(1)), FAE.left(1)); + await assertEqualsT(FAE.fromEither(E.right(1)), FAE.right(1)); +}); + +Deno.test("FnAsyncEither then", async () => { + assertEquals( + await pipe(Promise.resolve(1), then(add)), + await Promise.resolve(2), + ); +}); + +Deno.test("FnAsyncEither of", async () => { + await assertEqualsT(() => AE.of(1), FAE.of(1)); +}); + +Deno.test("FnAsyncEither map", async () => { + await assertEqualsT(pipe(FAE.of(1), FAE.map(add)), FAE.right(2)); + await assertEqualsT(pipe(FAE.left(1), FAE.map(add)), FAE.left(1)); +}); + +Deno.test("FnAsyncEither join", async () => { + await assertEqualsT(FAE.join(FAE.right(FAE.right(1))), FAE.right(1)); + await assertEqualsT(FAE.join(FAE.right(FAE.left(1))), FAE.left(1)); + await assertEqualsT(FAE.join(FAE.left(1)), FAE.left(1)); +}); + +Deno.test("FnAsyncEither chain", async () => { + const chain = FAE.chain( + (n: number) => n === 0 ? FAE.left(0) : FAE.right(1), + ); + await assertEqualsT(chain(FAE.right(0)), FAE.left(0)); + await assertEqualsT(chain(FAE.right(1)), FAE.right(1)); + await assertEqualsT(chain(FAE.left(1)), FAE.left(1)); +}); + +Deno.test("FnAsyncEither bimap", async () => { + const bimap = FAE.bimap(add, add); + await assertEqualsT(bimap(FAE.right(1)), FAE.right(2)); + await assertEqualsT(bimap(FAE.left(1)), FAE.left(2)); +}); + +Deno.test("FnAsyncEither mapLeft", async () => { + await assertEqualsT(pipe(FAE.of(1), FAE.mapLeft(add)), FAE.right(1)); + await assertEqualsT(pipe(FAE.left(1), FAE.mapLeft(add)), FAE.left(2)); +}); + +Deno.test("FnAsyncEither apSequential", async () => { + await assertEqualsT( + pipe(FAE.of(add), FAE.apSequential(FAE.of(1))), + FAE.right(2), + ); + await assertEqualsT( + pipe(FAE.of(add), FAE.apSequential(FAE.left(1))), + FAE.left(1), + ); +}); + +Deno.test("FnAsyncEither apParallel", async () => { + await assertEqualsT( + pipe(FAE.of(add), FAE.apParallel(FAE.right(1))), + FAE.right(2), + ); + await assertEqualsT( + pipe(FAE.of(add), FAE.apParallel(FAE.left(1))), + FAE.left(1), + ); +}); + +Deno.test("FnAsyncEither alt", async () => { + await assertEqualsT(pipe(FAE.left(1), FAE.alt(FAE.left(2))), FAE.left(2)); + await assertEqualsT(pipe(FAE.left(1), FAE.alt(FAE.right(2))), FAE.right(2)); + await assertEqualsT(pipe(FAE.right(1), FAE.alt(FAE.left(2))), FAE.right(1)); + await assertEqualsT(pipe(FAE.right(1), FAE.alt(FAE.right(2))), FAE.right(1)); +}); + +Deno.test("FnAsyncEither chainLeft", async () => { + const chainLeft = FAE.chainLeft(( + n: number, + ): FAE.FnAsyncEither => n === 0 ? FAE.right(n) : FAE.left(n)); + await assertEqualsT(chainLeft(FAE.right(0)), FAE.right(0)); + await assertEqualsT(chainLeft(FAE.left(0)), FAE.right(0)); + await assertEqualsT(chainLeft(FAE.left(1)), FAE.left(1)); +}); + +Deno.test("FnAsyncEither match", async () => { + const match = FAE.match((l: string) => l, String); + + assertEquals(await match(FAE.right(1))({}), "1"); + assertEquals(await match(FAE.left("asdf"))({}), "asdf"); +}); + +// Deno.test("FnAsyncEither Do, bind, bindTo", () => { +// assertEqualsT( +// pipe( +// FAE.Do(), +// FAE.bind("one", () => FAE.right(1)), +// FAE.bind("two", ({ one }) => FAE.right(one + one)), +// FAE.map(({ one, two }) => one + two), +// ), +// FAE.right(3), +// ); +// assertEqualsT( +// pipe( +// FAE.right(1), +// FAE.bindTo("one"), +// ), +// FAE.right({ one: 1 }), +// ); +// });