From 3ca9e2c08f1d1313081df9b81c5e980f21740f51 Mon Sep 17 00:00:00 2001 From: Thomas Reynolds Date: Sat, 2 May 2020 13:26:18 -0700 Subject: [PATCH] feat: add Maybe --- src/index.ts | 439 +++++++++++++++++++++++++++++ src/monads/Maybe.ts | 60 ++++ src/monads/__tests__/Maybe.spec.ts | 98 +++++++ src/monads/index.ts | 1 + 4 files changed, 598 insertions(+) create mode 100644 src/monads/Maybe.ts create mode 100644 src/monads/__tests__/Maybe.spec.ts create mode 100644 src/monads/index.ts diff --git a/src/index.ts b/src/index.ts index 86307b7..d054c7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,441 @@ export * from "./core/index" export * from "./memo/index" +export * from "./monads/index" + +// interface Setoid { +// equals(other: Setoid): boolean +// } + +// interface Ord extends Setoid { +// lte(other: Ord): boolean +// } + +// interface Semigroupoid { +// compose(other: Semigroupoid): Semigroupoid +// } + +// interface Category extends Semigroupoid { +// id(): Category +// } + +// interface Semigroup { +// concat(other: Semigroup): Semigroup +// } + +// interface Monoid extends Semigroup { +// empty(): Monoid +// } + +// interface Group extends Monoid { +// invert(): Group +// } + +// interface Filterable { +// filter(fn: (a: T) => boolean): Filterable +// } + +// interface Functor { +// map(fn: (a: A) => B): Functor +// } + +// interface Contravariant { +// contramap(fn: (b: B) => A): Contravariant +// } + +// interface Apply extends Functor { +// ap any>(fn: (a: Apply) => Apply): Apply +// } + +// interface Applicative extends Apply { +// of(a: A): Applicative +// } + +// interface Alt extends Functor { +// alt(a: Alt): Alt +// } + +// interface Plus extends Alt { +// zero(): Plus +// } + +// interface Alternative extends Applicative, Plus {} + +// interface Foldable { +// reduce(fn: (sum: T, item: A) => T, initial: T): T +// } + +// interface Traversable extends Functor, Foldable { +// traverse(a: Applicative, fn: () => B): B +// } + +// interface Chain extends Apply { +// chain(fn: (v: A) => A): Chain +// } + +// interface ChainRec extends Chain { +// chainRec( +// fn: (next: (a: T) => B, done: (a: T) => B, value: T) => B, +// initial: T, +// ): Chain +// } + +// interface Monad extends Applicative, Chain {} + +// interface Extend extends Functor { +// extend(fn: (a: A) => A): Extend +// } + +// interface Comonad extends Extend { +// extract(): A +// } + +// interface Bifunctor extends Functor { +// bimap(f: (a: A) => B, g: (a: A) => C): Bifunctor +// } + +// interface Profunctor extends Functor { +// promap(f: (a: A) => B, g: (a: A) => C): Profunctor +// } + +// class Num +// implements Ord, Chain, Semigroupoid, Category, Semigroup, Monoid, Group { +// static of = (v: number) => new Num(v) + +// constructor(public value: number) {} + +// // Setoid +// equals = (other: Num) => this.value === other.value + +// // Ord +// lte = (other: Num) => this.value <= other.value + +// // Semigroupoid +// compose = (other: Num) => Num.of(this.value + other.value) + +// // Category +// id = () => Num.of(0) + +// // Semigroup +// concat = (other: Num) => Num.of(this.value + other.value) + +// // Monoid +// empty = () => Num.of(0) + +// // Group +// invert = () => Num.of(-this.value) + +// chain = (fn: (v: number) => Num) => fn(this.value) +// } + +// class List implements Filterable, Functor { +// static of = (value: T[]) => new List(value) + +// constructor(public value: T[]) {} + +// // Filterable +// filter = (pred: (a: T) => boolean): List => +// List.of(this.value.filter(pred)) + +// // Functor +// map = (fn: (a: T) => B): List => List.of(this.value.map(fn)) +// } + +// class Fn implements Contravariant { +// static of = (value: (a: A) => T) => new Fn(value) + +// constructor(public value: (a: A) => T) {} + +// // Contravariant +// contramap = (fn: (b: B) => A) => Fn.of((b: B) => this.run(fn(b))) + +// run = (arg: A) => this.value(arg) +// } + +// Fn.of((name: string) => name) +// .contramap((obj: { name: string }) => obj.name) +// .run({ name: "test" }) + +// // Setoid +// export const equals = (a: Setoid, b: Setoid) => a.equals(b) + +// // Ord +// export const lte = (a: Ord, b: Ord) => a.lte(b) +// export const gt = (a: Ord, b: Ord) => !lte(a, b) +// export const gte = (a: Ord, b: Ord) => gt(a, b) || a.equals(b) +// export const lt = (a: Ord, b: Ord) => !gte(a, b) + +// // Semigroupoid +// export const compose = (a: A, b: A): A => +// a.compose(b) as A + +// // Category +// export const id = (a: A): A => a.id() as A + +// // Semigroup +// export const concat = (a: A, b: A): A => a.concat(b) as A + +// // Monoid +// export const empty = (a: A): A => a.empty() as A + +// // Group +// export const invert = (a: A): A => a.invert() as A + +// // Filterable +// export const filter = < +// A extends Filterable, +// T = A extends Filterable ? T : never +// >( +// pred: (a: T) => boolean, +// a: A, +// ) => a.filter(pred) as A + +// // Functor +// export const map = < +// A extends Functor, +// B, +// T = A extends Functor ? T : never +// >( +// fn: (a: T) => B, +// a: A, +// ) => a.map(fn) + +// // Contravariant +// export const contramap = < +// A extends Contravariant, +// B, +// T = A extends Contravariant ? T : never +// >( +// fn: (a: B) => T, +// a: A, +// ) => a.contramap(fn) + +// const eleven = concat(Num.of(5), Num.of(6)) +// eleven.id() + +// const nums = List.of([1, 2, 3]) +// const result = filter(a => a < 2, nums) +// const result2 = map(a => a + 1, nums) + +// const result = contramap( +// (obj: { name: string }) => obj.name, +// Fn.of((name: string) => name), +// ) + +// interface Setoid { +// equals(other: Setoid): boolean +// } + +// interface Ord extends Setoid { +// lte(other: Ord): boolean +// } + +// interface Semigroupoid { +// compose(other: Semigroupoid): Semigroupoid +// } + +// interface Category extends Semigroupoid { +// id(): Category +// } + +// interface Semigroup { +// concat(other: Semigroup): Semigroup +// } + +// interface Monoid extends Semigroup { +// empty(): Monoid +// } + +// interface Group extends Monoid { +// invert(): Group +// } + +// interface Filterable { +// filter(fn: (a: T) => boolean): Filterable +// } + +// interface Functor { +// map(fn: (a: A) => B): Functor +// } + +// interface Contravariant { +// contramap(fn: (b: B) => A): Contravariant +// } + +// interface Apply extends Functor { +// ap any>(fn: (a: Apply) => Apply): Apply +// } + +// interface Applicative extends Apply { +// of(a: A): Applicative +// } + +// interface Alt extends Functor { +// alt(a: Alt): Alt +// } + +// interface Plus extends Alt { +// zero(): Plus +// } + +// interface Alternative extends Applicative, Plus {} + +// interface Foldable { +// reduce(fn: (sum: T, item: A) => T, initial: T): T +// } + +// interface Traversable extends Functor, Foldable { +// traverse(a: Applicative, fn: () => B): B +// } + +// interface Chain extends Apply { +// chain(fn: (v: A) => A): Chain +// } + +// interface ChainRec extends Chain { +// chainRec( +// fn: (next: (a: T) => B, done: (a: T) => B, value: T) => B, +// initial: T, +// ): Chain +// } + +// interface Monad extends Applicative, Chain {} + +// interface Extend extends Functor { +// extend(fn: (a: A) => A): Extend +// } + +// interface Comonad extends Extend { +// extract(): A +// } + +// interface Bifunctor extends Functor { +// bimap(f: (a: A) => B, g: (a: A) => C): Bifunctor +// } + +// interface Profunctor extends Functor { +// promap(f: (a: A) => B, g: (a: A) => C): Profunctor +// } + +// class Num +// implements Ord, Chain, Semigroupoid, Category, Semigroup, Monoid, Group { +// static of = (v: number) => new Num(v) + +// constructor(public value: number) {} + +// // Setoid +// equals = (other: Num) => this.value === other.value + +// // Ord +// lte = (other: Num) => this.value <= other.value + +// // Semigroupoid +// compose = (other: Num) => Num.of(this.value + other.value) + +// // Category +// id = () => Num.of(0) + +// // Semigroup +// concat = (other: Num) => Num.of(this.value + other.value) + +// // Monoid +// empty = () => Num.of(0) + +// // Group +// invert = () => Num.of(-this.value) + +// chain = (fn: (v: number) => Num) => fn(this.value) +// } + +// class List implements Filterable, Functor { +// static of = (value: T[]) => new List(value) + +// constructor(public value: T[]) {} + +// // Filterable +// filter = (pred: (a: T) => boolean): List => +// List.of(this.value.filter(pred)) + +// // Functor +// map = (fn: (a: T) => B): List => List.of(this.value.map(fn)) +// } + +// class Fn implements Contravariant { +// static of = (value: (a: A) => T) => new Fn(value) + +// constructor(public value: (a: A) => T) {} + +// // Contravariant +// contramap = (fn: (b: B) => A) => Fn.of((b: B) => this.run(fn(b))) + +// run = (arg: A) => this.value(arg) +// } + +// Fn.of((name: string) => name) +// .contramap((obj: { name: string }) => obj.name) +// .run({ name: "test" }) + +// // Setoid +// export const equals = (a: Setoid, b: Setoid) => a.equals(b) + +// // Ord +// export const lte = (a: Ord, b: Ord) => a.lte(b) +// export const gt = (a: Ord, b: Ord) => !lte(a, b) +// export const gte = (a: Ord, b: Ord) => gt(a, b) || a.equals(b) +// export const lt = (a: Ord, b: Ord) => !gte(a, b) + +// // Semigroupoid +// export const compose = (a: A, b: A): A => +// a.compose(b) as A + +// // Category +// export const id = (a: A): A => a.id() as A + +// // Semigroup +// export const concat = (a: A, b: A): A => a.concat(b) as A + +// // Monoid +// export const empty = (a: A): A => a.empty() as A + +// // Group +// export const invert = (a: A): A => a.invert() as A + +// // Filterable +// export const filter = < +// A extends Filterable, +// T = A extends Filterable ? T : never +// >( +// pred: (a: T) => boolean, +// a: A, +// ) => a.filter(pred) as A + +// // Functor +// export const map = < +// A extends Functor, +// B, +// T = A extends Functor ? T : never +// >( +// fn: (a: T) => B, +// a: A, +// ) => a.map(fn) + +// // Contravariant +// export const contramap = < +// A extends Contravariant, +// B, +// T = A extends Contravariant ? T : never +// >( +// fn: (a: B) => T, +// a: A, +// ) => a.contramap(fn) + +// const eleven = concat(Num.of(5), Num.of(6)) +// eleven.id() + +// const nums = List.of([1, 2, 3]) +// const result = filter(a => a < 2, nums) +// const result2 = map(a => a + 1, nums) + +// const result = contramap( +// (obj: { name: string }) => obj.name, +// Fn.of((name: string) => name), +// ) diff --git a/src/monads/Maybe.ts b/src/monads/Maybe.ts new file mode 100644 index 0000000..5d9e2cd --- /dev/null +++ b/src/monads/Maybe.ts @@ -0,0 +1,60 @@ +import { identity, pipe } from "../core/index" + +export interface Just { + readonly type: "Just" + readonly value: T +} + +export interface Nothing { + readonly type: "Nothing" +} + +export type Maybe = Just | Nothing + +export const Just = (value: T): Maybe => ({ type: "Just", value }) +export const Nothing = (): Maybe => ({ type: "Nothing" }) + +// Monoid +export const empty = Nothing + +// Applicative +export const of = Just + +export const fromNullable = (valueOrNull: T | null | undefined) => + valueOrNull ? Just(valueOrNull) : Nothing() + +export const cata = (handlers: { + Nothing: () => U + Just: (v: T) => U +}) => (maybe: Maybe): U => { + switch (maybe.type) { + case "Just": + return handlers.Just(maybe.value) + + case "Nothing": + return handlers.Nothing() + } +} + +export const fold = (justFn: (v: T) => U, nothingFn: () => U) => + cata({ Just: justFn, Nothing: nothingFn }) + +// Bimap +export const bimap = (justFn: (v: T) => U, nothingFn: () => U) => + pipe(fold(justFn, nothingFn), Just) + +// Chain +export const chain = (fn: (a: T) => Maybe) => + fold>(fn, Nothing) + +// Functor +export const map = (fn: (a: T) => U) => chain(pipe(fn, Just)) + +// Error handling +export const orElse = (fn: () => T) => bimap(identity, fn) + +export const isJust = (maybe: Maybe): maybe is Just => + maybe.type === "Just" + +export const isNothing = (maybe: Maybe): maybe is Nothing => + maybe.type === "Nothing" diff --git a/src/monads/__tests__/Maybe.spec.ts b/src/monads/__tests__/Maybe.spec.ts new file mode 100644 index 0000000..a930df6 --- /dev/null +++ b/src/monads/__tests__/Maybe.spec.ts @@ -0,0 +1,98 @@ +import { + Just, + Nothing, + cata, + fold, + empty, + of, + fromNullable, + bimap, + map, + chain, + orElse, + isJust, + isNothing, +} from "../Maybe" + +describe("Maybe", () => { + test("Just", () => { + const J = jest.fn() + const N = jest.fn() + + fold(J, N)(Just(5)) + + expect(J).toBeCalledWith(5) + expect(N).not.toHaveBeenCalled() + }) + + test("Nothing", () => { + const J = jest.fn() + const N = jest.fn() + + fold(J, N)(Nothing()) + + expect(J).not.toHaveBeenCalled() + expect(N).toHaveBeenCalled() + }) + + test("empty", () => expect(empty()).toEqual(Nothing())) + test("of", () => expect(of(5)).toEqual(Just(5))) + + test("fromNullable(null)", () => + expect(fromNullable(null)).toEqual(Nothing())) + test("fromNullable(5)", () => expect(fromNullable(5)).toEqual(Just(5))) + + test("cata(Just)", () => { + const J = jest.fn() + const N = jest.fn() + + cata({ Just: J, Nothing: N })(Just(5)) + + expect(J).toBeCalledWith(5) + expect(N).not.toHaveBeenCalled() + }) + + test("cata(Nothing)", () => { + const J = jest.fn() + const N = jest.fn() + + cata({ Just: J, Nothing: N })(Nothing()) + + expect(J).not.toHaveBeenCalled() + expect(N).toHaveBeenCalled() + }) + + test("bimap(Just)", () => { + const N = jest.fn() + + expect(bimap((v: number) => v * 2, N)(Just(5))).toEqual(Just(10)) + + expect(N).not.toHaveBeenCalled() + }) + + test("bimap(Nothing)", () => { + const J = jest.fn() + + expect(bimap(J, () => 10)(Nothing())).toEqual(Just(10)) + + expect(J).not.toHaveBeenCalled() + }) + + test("map(Just)", () => + expect(map((v: number) => v * 2)(Just(5))).toEqual(Just(10))) + test("map(Nothing)", () => + expect(map(jest.fn())(Nothing())).toEqual(Nothing())) + + test("chain(Just)", () => + expect(chain((v: number) => Just(v * 2))(Just(5))).toEqual(Just(10))) + test("chain(Nothing)", () => + expect(chain(jest.fn())(Nothing())).toEqual(Nothing())) + + test("orElse", () => expect(orElse(() => 10)(Nothing())).toEqual(Just(10))) + + test("isJust(Just)", () => expect(isJust(Just(5))).toBe(true)) + test("isJust(Nothing)", () => expect(isJust(Nothing())).toBe(false)) + + test("isNothing(Just)", () => expect(isNothing(Just(5))).toBe(false)) + test("isNothing(Nothing)", () => expect(isNothing(Nothing())).toBe(true)) +}) diff --git a/src/monads/index.ts b/src/monads/index.ts new file mode 100644 index 0000000..ca8e3fb --- /dev/null +++ b/src/monads/index.ts @@ -0,0 +1 @@ +export * as Maybe from "./Maybe"