diff --git a/map.ts b/map.ts index 4db76f2..b34ba71 100644 --- a/map.ts +++ b/map.ts @@ -102,8 +102,8 @@ export const getMonoid = ( pipe( lookupKey(bk)(a), O.fold( - ([ak, aa]) => r.set(ak, SA.concat(aa)(ba)), () => r.set(bk, ba), + ([ak, aa]) => r.set(ak, SA.concat(aa)(ba)), ), ); } diff --git a/option.ts b/option.ts index 6700ec4..2195489 100644 --- a/option.ts +++ b/option.ts @@ -26,6 +26,10 @@ export type Some = { tag: "Some"; value: V }; */ export type Option = Some | None; +/******************************************************************************* + * Kind Registration + ******************************************************************************/ + export const URI = "Option"; export type URI = typeof URI; @@ -115,10 +119,8 @@ export const tryCatch = (f: Lazy): Option => { * const a = toNumber(some(1)); // 1 * const b = toNumber(none); // 0 */ -export const fold = (onSome: (a: A) => B, onNone: () => B) => - ( - ta: Option, - ): B => (isNone(ta) ? onNone() : onSome(ta.value)); +export const fold = (onNone: () => B, onSome: (a: A) => B) => + (ta: Option): B => (isNone(ta) ? onNone() : onSome(ta.value)); /** * getOrElse operates like a simplified fold. One supplies a thunk that returns a default @@ -136,7 +138,7 @@ export const getOrElse = (onNone: () => B) => * toNullable returns either null or the inner value of an Option. This is useful for * interacting with code that handles null but has no concept of the Option type. */ -export const toNullable = (ma: Option): A | null => +export const toNull = (ma: Option): A | null => isNone(ma) ? null : ma.value; /** @@ -179,66 +181,6 @@ export const isNone = (m: Option): m is None => m.tag === "None"; */ export const isSome = (m: Option): m is Some => m.tag === "Some"; -/******************************************************************************* - * Module Getters - ******************************************************************************/ - -/** - * Generates a Show module for an option with inner type of A. - * - * @example - * const Show = getShow({ show: (n: number) => n.toString() }); // Show> - * const a = Show.show(some(1)); // "Some(1)" - * const b = Show.show(none); // "None" - */ -export const getShow = ({ show }: TC.Show): TC.Show> => ({ - show: (ma) => (isNone(ma) ? "None" : `${"Some"}(${show(ma.value)})`), -}); - -/** - * Generates a Setoid module for an option with inner type of A. - * - * @example - * const Setoid = getSetoid({ equals: (a: number, b: number) => a === b }); - * const a = Setoid.equals(some(1), some(2)); // false - * const b = Setoid.equals(some(1), some(1)); // true - * const c = Setoid.equals(none, none); // true - * const d = Setoid.equals(some(1), none); // false - */ -export const getSetoid = (S: TC.Setoid): TC.Setoid> => ({ - equals: (a) => - (b) => - a === b || isNone(a) - ? isNone(b) - : (isNone(b) ? false : S.equals(a.value)(b.value)), -}); - -export const getOrd = (O: TC.Ord): TC.Ord> => ({ - ...getSetoid(O), - lte: (a) => - (b) => - a === b || isNone(a) - ? isNone(b) - : (isNone(b) ? false : O.lte(a.value)(b.value)), -}); - -export const getSemigroup = ( - S: TC.Semigroup, -): TC.Semigroup> => ({ - concat: (x) => - (y) => isNone(x) ? y : isNone(y) ? x : of(S.concat(x.value)(y.value)), -}); - -export const getMonoid = (M: TC.Monoid): TC.Monoid> => ({ - ...getSemigroup(M), - empty: constNone, -}); - -export const getGroup = (G: TC.Group): TC.Group> => ({ - ...getMonoid(G), - invert: (ta) => isNone(ta) ? ta : some(G.invert(ta.value)), -}); - /******************************************************************************* * Modules ******************************************************************************/ @@ -324,6 +266,70 @@ export const Traversable: TC.Traversable = { isNone(ta) ? A.of(constNone()) : pipe(favi(ta.value), A.map(some)), }; +/******************************************************************************* + * Module Getters + ******************************************************************************/ + +/** + * Generates a Show module for an option with inner type of A. + * + * @example + * const Show = getShow({ show: (n: number) => n.toString() }); // Show> + * const a = Show.show(some(1)); // "Some(1)" + * const b = Show.show(none); // "None" + */ +export const getShow = ({ show }: TC.Show): TC.Show> => ({ + show: (ma) => (isNone(ma) ? "None" : `${"Some"}(${show(ma.value)})`), +}); + +/** + * Generates a Setoid module for an option with inner type of A. + * + * @example + * const Setoid = getSetoid({ equals: (a: number, b: number) => a === b }); + * const a = Setoid.equals(some(1), some(2)); // false + * const b = Setoid.equals(some(1), some(1)); // true + * const c = Setoid.equals(none, none); // true + * const d = Setoid.equals(some(1), none); // false + */ +export const getSetoid = (S: TC.Setoid): TC.Setoid> => ({ + equals: (a) => + (b) => + a === b || + ((isSome(a) && isSome(b)) + ? S.equals(a.value)(b.value) + : (isNone(a) && isNone(b))), +}); + +export const getOrd = (O: TC.Ord): TC.Ord> => ({ + ...getSetoid(O), + lte: (a) => + (b) => { + if (a === b) { + return true; + } + if (isNone(a)) { + return true; + } + if (isNone(b)) { + return false; + } + return O.lte(a.value)(b.value); + }, +}); + +export const getSemigroup = ( + S: TC.Semigroup, +): TC.Semigroup> => ({ + concat: (x) => + (y) => isNone(x) ? y : isNone(y) ? x : of(S.concat(x.value)(y.value)), +}); + +export const getMonoid = (M: TC.Monoid): TC.Monoid> => ({ + ...getSemigroup(M), + empty: constNone, +}); + /******************************************************************************* * Pipeables ******************************************************************************/ diff --git a/testing/fns.test.ts b/testing/fns.test.ts index 851901c..b1d8758 100644 --- a/testing/fns.test.ts +++ b/testing/fns.test.ts @@ -122,7 +122,7 @@ Deno.test("fns wait", async () => { const within = (high: number, low: number) => (value: number): boolean => value >= low && value <= high; const target = 100; - const high = 200; + const high = 900; // github actions on macos tend to drag const low = 50; const test = within(high, low); diff --git a/testing/io.test.ts b/testing/io.test.ts index aeca676..b813442 100644 --- a/testing/io.test.ts +++ b/testing/io.test.ts @@ -54,7 +54,7 @@ Deno.test("IO reduce", () => { }); Deno.test("IO traverse", () => { - const fold = O.fold((n: I.IO) => n(), () => -1); + const fold = O.fold(() => -1, (n: I.IO) => n()); const t0 = I.traverse(O.Applicative); const t1 = t0((n: number) => n === 0 ? O.none : O.some(n)); const t2 = fold(t1(I.of(0))); diff --git a/testing/option.test.ts b/testing/option.test.ts new file mode 100644 index 0000000..43a15bf --- /dev/null +++ b/testing/option.test.ts @@ -0,0 +1,342 @@ +import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + +import * as AS from "./assert.ts"; + +import type * as HKT from "../hkt.ts"; + +import * as O from "../option.ts"; +import { setoidNumber } from "../setoid.ts"; +import { monoidSum } from "../monoid.ts"; +import { ordNumber } from "../ord.ts"; +import { semigroupSum } from "../semigroup.ts"; +import { _, pipe } from "../fns.ts"; + +Deno.test("Option none", () => { + assertEquals(O.none, { tag: "None" }); +}); + +Deno.test("Option some", () => { + assertEquals(O.some(1), { tag: "Some", value: 1 }); +}); + +Deno.test("Option constNone", () => { + assertEquals(O.constNone(), O.none); +}); + +Deno.test("Option fromNullable", () => { + assertEquals(O.fromNullable(undefined), O.none); + assertEquals(O.fromNullable(null), O.none); + assertEquals(O.fromNullable(1), O.some(1)); +}); + +Deno.test("Option fromPredicate", () => { + const fromPredicate = O.fromPredicate((n: number) => n > 0); + assertEquals(fromPredicate(0), O.none); + assertEquals(fromPredicate(1), O.some(1)); +}); + +Deno.test("Option tryCatch", () => { + assertEquals(O.tryCatch(_), O.none); + assertEquals(O.tryCatch(() => 1), O.some(1)); +}); + +Deno.test("Option fold", () => { + const fold = O.fold(() => 0, (n: number) => n); + assertEquals(fold(O.none), 0); + assertEquals(fold(O.some(1)), 1); +}); + +Deno.test("Option getOrElse", () => { + const getOrElse = O.getOrElse(() => 0); + assertEquals(getOrElse(O.none), 0); + assertEquals(getOrElse(O.some(1)), 1); +}); + +Deno.test("Option toNull", () => { + assertEquals(O.toNull(O.none), null); + assertEquals(O.toNull(O.some(1)), 1); +}); + +Deno.test("Option toUndefined", () => { + assertEquals(O.toUndefined(O.none), undefined); + assertEquals(O.toUndefined(O.some(1)), 1); +}); + +Deno.test("Option mapNullable", () => { + const mapNullable = O.mapNullable((n: number[]) => + n.length === 1 ? n[0] : n[10] + ); + assertEquals(mapNullable(O.some([0])), O.some(0)); + assertEquals(mapNullable(O.some([1, 2])), O.none); + assertEquals(mapNullable(O.none), O.none); +}); + +Deno.test("Option isNone", () => { + assertEquals(O.isNone(O.none), true); + assertEquals(O.isNone(O.some(1)), false); +}); + +Deno.test("Option isSome", () => { + assertEquals(O.isSome(O.none), false); + assertEquals(O.isSome(O.some(1)), true); +}); + +Deno.test("Option Functor", () => { + AS.assertFunctor(O.Functor, { ta: O.some(1), fai: AS.add, fij: AS.multiply }); +}); + +Deno.test("Option Apply", () => { + AS.assertApply(O.Apply, { + ta: O.some(1), + fai: AS.add, + fij: AS.multiply, + tfai: O.some(AS.add), + tfij: O.some(AS.multiply), + }); +}); + +Deno.test("Option Applicative", () => { + AS.assertApplicative(O.Applicative, { + a: 1, + ta: O.some(1), + fai: AS.add, + fij: AS.multiply, + tfai: O.some(AS.add), + tfij: O.some(AS.multiply), + }); +}); + +Deno.test("Option Chain", () => { + AS.assertChain(O.Chain, { + a: 1, + ta: O.some(1), + fai: AS.add, + fij: AS.multiply, + tfai: O.some(AS.add), + tfij: O.some(AS.multiply), + fati: (n: number) => O.some(n + 1), + fitj: (n: number) => O.some(n * n), + }); +}); + +Deno.test("Option Monad", () => { + AS.assertMonad(O.Monad, { + a: 1, + ta: O.some(1), + fai: AS.add, + fij: AS.multiply, + tfai: O.some(AS.add), + tfij: O.some(AS.multiply), + fati: (n: number) => O.some(n + 1), + fitj: (n: number) => O.some(n * n), + }); +}); + +Deno.test("Option Alt", () => { + AS.assertAlt(O.Alt, { + ta: O.some(1), + tb: O.some(2), + tc: O.some(3), + fai: AS.add, + fij: AS.multiply, + }); + AS.assertAlt(O.Alt, { + ta: O.none, + tb: O.some(2), + tc: O.some(3), + fai: AS.add, + fij: AS.multiply, + }); +}); + +Deno.test("Option Alternative", () => { + AS.assertAlternative(O.Alternative, { + a: 1, + ta: O.some(1), + tb: O.some(2), + tc: O.some(3), + fai: AS.add, + fij: AS.multiply, + tfai: O.some(AS.add), + tfij: O.some(AS.multiply), + }); +}); + +Deno.test("Option Extends", () => { + AS.assertExtend(O.Extends, { + ta: O.some(1), + fai: AS.add, + fij: AS.multiply, + ftai: O.getOrElse(() => 0), + ftij: O.getOrElse(() => -1), + }); +}); + +Deno.test("Option Filterable", () => { + AS.assertFilterable(O.Filterable, { + a: O.some(0), + b: O.some(1), + f: (n: number) => n > 0, + g: (n: number) => n < 2, + }); +}); + +Deno.test("Option Foldable", () => { + AS.assertFoldable(O.Foldable, { + a: 0, + tb: O.some(1), + faia: (n: number, o: number) => n + o, + }); +}); + +Deno.test("Option Plus", () => { + AS.assertPlus(O.Plus, { + ta: O.some(1), + tb: O.some(2), + tc: O.some(3), + fai: AS.add, + fij: AS.multiply, + }); +}); + +Deno.test("Option getShow", () => { + const { show } = O.getShow({ show: (n: number) => n.toString() }); + assertEquals(show(O.none), "None"); + assertEquals(show(O.some(1)), "Some(1)"); +}); + +Deno.test("Option getSetoid", () => { + const Setoid = O.getSetoid(setoidNumber); + AS.assertSetoid(Setoid, { + a: O.some(1), + b: O.some(1), + c: O.some(1), + z: O.none, + }); +}); + +Deno.test("Option getOrd", () => { + const Ord = O.getOrd(ordNumber); + AS.assertOrd(Ord, { a: O.none, b: O.some(1) }); + AS.assertOrd(Ord, { a: O.some(2), b: O.some(1) }); +}); + +Deno.test("Option getSemigroup", () => { + const Semigroup = O.getSemigroup(semigroupSum); + AS.assertSemigroup(Semigroup, { a: O.some(1), b: O.some(2), c: O.some(3) }); +}); + +Deno.test("Option getMonoid", () => { + const Monoid = O.getMonoid(monoidSum); + AS.assertMonoid(Monoid, { a: O.some(1), b: O.some(2), c: O.some(3) }); +}); + +Deno.test("Option of", () => { + assertEquals(O.of(1), O.some(1)); +}); + +Deno.test("Option ap", () => { + assertEquals(pipe(O.some(1), O.ap(O.some(AS.add))), O.some(2)); + assertEquals(pipe(O.some(1), O.ap(O.none)), O.none); + assertEquals(pipe(O.none, O.ap(O.some(AS.add))), O.none); + assertEquals(pipe(O.none, O.ap(O.none)), O.none); +}); + +Deno.test("Option map", () => { + assertEquals(pipe(O.some(1), O.map(AS.add)), O.some(2)); + assertEquals(pipe(O.none, O.map(AS.add)), O.none); +}); + +Deno.test("Option join", () => { + assertEquals(O.join(O.some(O.some(1))), O.some(1)); + assertEquals(O.join(O.some(O.none)), O.none); + assertEquals(O.join(O.none), O.none); +}); + +Deno.test("Option chain", () => { + const fati = (n: number) => n === 0 ? O.none : O.some(n); + assertEquals(pipe(O.some(0), O.chain(fati)), O.none); + assertEquals(pipe(O.some(1), O.chain(fati)), O.some(1)); + assertEquals(pipe(O.none, O.chain(fati)), O.none); +}); + +Deno.test("Option reduce", () => { + const reduce = O.reduce((n: number, o: number) => n + o, 0); + assertEquals(reduce(O.some(1)), 1); + assertEquals(reduce(O.none), 0); +}); + +Deno.test("Option traverse", () => { + const t1 = O.traverse(O.Applicative); + const t2 = t1((n: number) => n === 0 ? O.none : O.some(1)); + assertEquals(t2(O.none), O.some(O.none)); + assertEquals(t2(O.some(0)), O.none); + assertEquals(t2(O.some(1)), O.some(O.some(1))); +}); + +Deno.test("Option zero", () => { + assertEquals(O.zero(), O.none); +}); + +Deno.test("Option alt", () => { + assertEquals(pipe(O.some(0), O.alt(O.some(1))), O.some(0)); + assertEquals(pipe(O.some(0), O.alt(O.constNone())), O.some(0)); + assertEquals(pipe(O.none, O.alt(O.some(1))), O.some(1)); + assertEquals(pipe(O.none, O.alt(O.none)), O.none); +}); + +Deno.test("Option filter", () => { + const filter = O.filter((n: number) => n > 0); + assertEquals(filter(O.some(0)), O.none); + assertEquals(filter(O.some(1)), O.some(1)); + assertEquals(filter(O.none), O.none); +}); + +Deno.test("Option extend", () => { + const extend = O.extend(O.fold(() => -1, (n: number) => n + 1)); + assertEquals(extend(O.some(0)), O.some(1)); + assertEquals(extend(O.none), O.some(-1)); +}); + +Deno.test("Option sequenceTuple", () => { + assertEquals(O.sequenceTuple(O.some(0), O.some(1)), O.some([0, 1])); + assertEquals(O.sequenceTuple(O.some(0), O.none), O.none); + assertEquals(O.sequenceTuple(O.none, O.some(1)), O.none); + assertEquals(O.sequenceTuple(O.none, O.none), O.none); +}); + +Deno.test("Option sequenceStruct", () => { + assertEquals( + O.sequenceStruct({ a: O.some(0), b: O.some(1) }), + O.some({ a: 0, b: 1 }), + ); + assertEquals(O.sequenceStruct({ a: O.some(0), b: O.none }), O.none); + assertEquals(O.sequenceStruct({ a: O.none, b: O.some(1) }), O.none); + assertEquals(O.sequenceStruct({ a: O.none, b: O.none }), O.none); +}); + +Deno.test("Option exists", () => { + const exists = O.exists((n: number) => n > 0); + assertEquals(exists(O.some(0)), false); + assertEquals(exists(O.some(1)), true); + assertEquals(exists(O.none), false); +}); + +Deno.test("Option Do, bind, bindTo", () => { + assertEquals( + pipe( + O.Do(), + O.bind("one", () => O.some(1)), + O.bind("two", ({ one }) => O.some(one + one)), + O.map(({ one, two }) => one + two), + ), + O.some(3), + ); + assertEquals( + pipe( + O.some(1), + O.bindTo("one"), + ), + O.some({ one: 1 }), + ); +});