diff --git a/src/tuple.test.ts b/src/tuple.test.ts new file mode 100644 index 00000000..aba0fe77 --- /dev/null +++ b/src/tuple.test.ts @@ -0,0 +1,399 @@ +import { assertEquals } from "../deps.ts"; +import { Array, Lazy, Number, Option, Tuple } from "../mod.ts"; +import { compose, id } from "./func.ts"; +import { equal, greater, less, type Ordering } from "./ordering.ts"; +import { unwrap } from "./result.ts"; +import { + decU32Be, + decUtf8, + encU32Be, + encUtf8, + runCode, + runDecoder, +} from "./serial.ts"; +import { stringEq } from "./type-class/eq.ts"; +import { maxMonoid } from "./type-class/monoid.ts"; +import { stringOrd } from "./type-class/ord.ts"; +import { nonNanOrd } from "./type-class/ord.ts"; +import { semiGroupSymbol } from "./type-class/semi-group.ts"; + +Deno.test("partial equality", () => { + const equality = Tuple.partialEq({ + equalityA: stringEq, + equalityB: Number.partialOrd, + }); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("bar")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("foo")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("bar")(3), Tuple.make("bar")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("bar")(3)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(4), Tuple.make("bar")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("bar")(4)), + false, + ); + + assertEquals( + equality.eq(Tuple.make("foo")(NaN), Tuple.make("foo")(NaN)), + false, + ); + + assertEquals(equality.eq(Tuple.make("foo")(3), Tuple.make("foo")(3)), true); +}); +Deno.test("equality", () => { + const equality = Tuple.eq({ + equalityA: stringEq, + equalityB: nonNanOrd, + }); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("bar")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("foo")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("bar")(3), Tuple.make("bar")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("bar")(3)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(4), Tuple.make("bar")(4)), + false, + ); + assertEquals( + equality.eq(Tuple.make("foo")(3), Tuple.make("bar")(4)), + false, + ); + + assertEquals(equality.eq(Tuple.make("foo")(3), Tuple.make("foo")(3)), true); +}); +Deno.test("partial order", () => { + const order = Tuple.partialOrd({ + ordA: nonNanOrd, + ordB: Number.partialOrd, + }); + + assertEquals( + order.partialCmp( + Tuple.make(5)(NaN), + Tuple.make(5)(NaN), + ), + Option.none(), + ); + assertEquals( + order.partialCmp( + Tuple.make(5)(NaN), + Tuple.make(6)(NaN), + ), + Option.none(), + ); + assertEquals( + order.partialCmp( + Tuple.make(6)(NaN), + Tuple.make(6)(NaN), + ), + Option.none(), + ); + + assertEquals( + order.partialCmp( + Tuple.make(5)(4), + Tuple.make(5)(4), + ), + Option.some(equal as Ordering), + ); + assertEquals( + order.partialCmp( + Tuple.make(5)(4), + Tuple.make(5)(5), + ), + Option.some(less as Ordering), + ); + assertEquals( + order.partialCmp( + Tuple.make(5)(5), + Tuple.make(5)(4), + ), + Option.some(greater as Ordering), + ); +}); +Deno.test("total order", () => { + const order = Tuple.ord({ + ordA: stringOrd, + ordB: nonNanOrd, + }); + + assertEquals(order.cmp(Tuple.make("a")(64), Tuple.make("a")(64)), equal); + assertEquals(order.cmp(Tuple.make("a")(64), Tuple.make("abc")(64)), less); + assertEquals( + order.cmp(Tuple.make("abc")(64), Tuple.make("a")(64)), + greater, + ); + assertEquals(order.cmp(Tuple.make("a")(64), Tuple.make("a")(65)), less); + assertEquals(order.cmp(Tuple.make("a")(65), Tuple.make("a")(64)), greater); +}); + +Deno.test("first", () => { + assertEquals(Tuple.first(Tuple.make(1)("4")), 1); + assertEquals(Tuple.first(Tuple.make("4")(1)), "4"); +}); +Deno.test("second", () => { + assertEquals(Tuple.second(Tuple.make(1)("4")), "4"); + assertEquals(Tuple.second(Tuple.make("4")(1)), 1); +}); +Deno.test("assocL", () => { + assertEquals( + Tuple.assocL(Tuple.make(1)(Tuple.make(2)(3))), + Tuple.make(Tuple.make(1)(2))(3), + ); +}); +Deno.test("assocR", () => { + assertEquals( + Tuple.assocR(Tuple.make(Tuple.make(1)(2))(3)), + Tuple.make(1)(Tuple.make(2)(3)), + ); +}); +Deno.test("curry", () => { + const product = ([a, b]: Tuple.Tuple): number => a * b; + const curried = Tuple.curry(product); + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + assertEquals(curried(x)(y), product([x, y])); + } + } +}); +Deno.test("uncurry", () => { + const product = (a: number) => (b: number): number => a * b; + const uncurried = Tuple.uncurry(product); + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + assertEquals(uncurried([x, y]), product(x)(y)); + } + } +}); +Deno.test("swap", () => { + assertEquals(Tuple.swap(Tuple.make(1)("4")), Tuple.make("4")(1)); +}); +Deno.test("bind", () => { + assertEquals( + Tuple.bind({ + combine: (l: number, r: number) => l + r, + [semiGroupSymbol]: true, + })(Tuple.make(4)(5))((x: number) => Tuple.make(x + 1)("y")), + Tuple.make(10)("y"), + ); +}); +Deno.test("defer", () => { + const unzipped = Tuple.defer(Lazy.known(Tuple.make(4)(2))); + assertEquals(Lazy.force(unzipped[0]), 4); + assertEquals(Lazy.force(unzipped[1]), 2); +}); +Deno.test("foldR", () => { + assertEquals( + Tuple.foldR((x: number) => (y: number) => x ^ y)(1)(Tuple.make(2)(4)), + 7, + ); +}); +Deno.test("bifoldR", () => { + assertEquals( + Tuple.bifoldable.bifoldR((a: number) => (c: string) => + a.toString() + c + )((b: boolean) => (c: string) => b.toString() + c)("")([5, false]), + "5false", + ); +}); +Deno.test("bitraverse", () => { + assertEquals( + Tuple.bitraversable.bitraverse(Array.applicative)(( + x: string, + ) => ["ERROR", x])((x: number) => [x, x + 1])(Tuple.make("")(2)), + [ + Tuple.make("ERROR")(2), + Tuple.make("ERROR")(3), + Tuple.make("")(2), + Tuple.make("")(3), + ], + ); +}); + +Deno.test("functor laws", () => { + const f = Tuple.functor(); + // identity + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals(f.map((x: number) => x)(t), t); + } + } + + // composition + const mul2 = (x: number) => x * 2; + const add3 = (x: number) => x + 3; + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals( + f.map((x: number) => add3(mul2(x)))(t), + f.map(add3)(f.map(mul2)(t)), + ); + } + } +}); +Deno.test("applicative functor laws", () => { + const app = Tuple.applicative(maxMonoid(-Infinity)); + const mul2 = app.pure((x: number) => x * 2); + const add3 = app.pure((x: number) => x + 3); + + // identity + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals(app.apply(app.pure((i: number) => i))(t), t); + } + } + + // composition + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + app.apply( + app.apply( + app.apply(app.pure( + (f: (x: number) => number) => + (g: (x: number) => number) => + (i: number) => f(g(i)), + ))(add3), + )(mul2), + )(t), app.apply(add3)(app.apply(mul2)(t)); + } + } + + // homomorphism + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals( + app.apply(app.pure(Tuple.swap))(app.pure(t)), + app.pure(Tuple.swap(t)), + ); + } + } + + // interchange + for (let x = -100; x <= 100; ++x) { + assertEquals( + app.apply(add3)(app.pure(x)), + app.apply(app.pure((i: (x: number) => number) => i(x)))(add3), + ); + } +}); +Deno.test("monad laws", () => { + const m = Tuple.monad(maxMonoid(-Infinity)); + + const halfDouble = ( + x: number, + ): Tuple.Tuple => [x / 2, x * 2]; + const minusPlus = ( + x: number, + ): Tuple.Tuple => [x - 1, x + 1]; + + // left identity + for (let x = -100; x <= 100; ++x) { + assertEquals(m.flatMap(halfDouble)(m.pure(x)), halfDouble(x)); + } + + // right identity + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals(m.flatMap(m.pure)(t), t); + } + } + + // associativity + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals( + m.flatMap(halfDouble)(m.flatMap(minusPlus)(t)), + m.flatMap((x: number) => m.flatMap(halfDouble)(minusPlus(x)))( + t, + ), + ); + } + } +}); +Deno.test("bifunctor laws", () => { + const f = Tuple.bifunctor; + + // identity + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals(f.biMap(id)(id)(t), t); + } + } + + // composition + const succ = (x: number) => x + 1; + const invert = (x: number) => ~x; + const mul4 = (x: number) => x * 4; + const square = (x: number) => x ** 2; + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + assertEquals( + f.biMap(compose(succ)(invert))(compose(mul4)(square))(t), + compose(f.biMap(succ)(mul4))(f.biMap(invert)(square))(t), + ); + } + } +}); +Deno.test("comonad laws", () => { + const c = Tuple.comonad(); + + for (let x = -100; x <= 100; ++x) { + for (let y = -100; y <= 100; ++y) { + const t = Tuple.make(x)(y); + // duplicate then extract + assertEquals(c.extract(c.duplicate(t)), t); + + // extract as identity of map + assertEquals(c.map(c.extract)(c.duplicate(t)), t); + + // duplicate as identity of map + assertEquals( + c.duplicate(c.duplicate(t)), + c.map(c.duplicate)(c.duplicate(t)), + ); + } + } +}); + +Deno.test("encode then decode", () => { + const data = Tuple.make(1024)("wow"); + const serial = runCode(Tuple.enc(encU32Be)(encUtf8)(data)); + assertEquals( + unwrap(runDecoder(Tuple.dec(decU32Be())(decUtf8()))(serial)), + data, + ); +}); diff --git a/src/tuple.ts b/src/tuple.ts index 70621202..ec98541f 100644 --- a/src/tuple.ts +++ b/src/tuple.ts @@ -12,9 +12,10 @@ import { type Applicative, liftA2 } from "./type-class/applicative.ts"; import { type Bifoldable, fromBifoldMap } from "./type-class/bifoldable.ts"; import type { Bifunctor } from "./type-class/bifunctor.ts"; import type { Bitraversable } from "./type-class/bitraversable.ts"; +import type { Comonad } from "./type-class/comonad.ts"; import { type Eq, fromEquality } from "./type-class/eq.ts"; import type { Functor } from "./type-class/functor.ts"; -import { liftM2 } from "./type-class/monad.ts"; +import { liftM2, type Monad } from "./type-class/monad.ts"; import type { Monoid } from "./type-class/monoid.ts"; import { fromCmp, type Ord } from "./type-class/ord.ts"; import { @@ -194,6 +195,19 @@ export const bind = return [sg.combine(a1, a2), c]; }; +/** + * Maps and flattens the tuple by a function `f` which maps from `B` to `Tuple`. The first element is combined by the `SemiGroup` instance for `A`. + * + * @param sg - The `SemiGroup` instance for `A`. + * @param tuple - The tuple to be mapped. + * @param f - The function which takes the second element and returns the new tuple. + * @returns The composed tuple. + */ +export const flatMap = + (sg: SemiGroup) => + (f: (b: B) => Tuple) => + (tuple: Tuple): Tuple => bind(sg)(tuple)(f); + export const extend = (f: (tuple: Tuple) => B) => (tuple: Tuple): Tuple => [tuple[0], f(tuple)]; @@ -202,6 +216,16 @@ export const extend = */ export const extract = second; +/** + * Duplicates the first element of tuple. + * + * @param t - The tuple to be duplicated. + * @returns The nested tuple. + */ +export const duplicate = ( + t: Tuple, +): Tuple> => [t[0], t]; + /** * Distributes `Tuple` in `Lazy`. * @@ -287,6 +311,21 @@ export const bitraverse: ( ) => (data: Tuple) => Get1> = (app) => (f) => (g) => ([a, b]) => liftA2(app)(make)(f(a))(g(b)); +/** + * Folds the tuple data into a `Monoid` value with mapping functions `aMap` and `bMap`. + * + * @param m - The `Monoid` instance for `M`. + * @param aMap - The mapping function from `A`. + * @param bMap - The mapping function from `B`. + * @param data - The data to be mapped. + * @returns Folded value of type `M`. + */ +export const bifoldMap = + (m: Monoid) => + (aMap: (a: A) => M) => + (bMap: (b: B) => M) => + ([a, b]: Tuple): M => m.combine(aMap(a), bMap(b)); + export interface TupleHkt extends Hkt2 { readonly type: Tuple; } @@ -300,6 +339,31 @@ export interface TupleDHkt extends Hkt1 { readonly type: Tuple; } +/** + * @param monoid - The `Monoid` instance for the first element `A`. + * @returns The `Applicative` instance for `Tuple`. + */ +export const applicative = ( + monoid: Monoid, +): Applicative> => ({ + map, + pure: pure(monoid), + apply: apply(monoid), +}); + +/** + * @param monoid - The `Monoid` instance for the first element `A`. + * @returns The `Monad` instance for `Tuple`. + */ +export const monad = ( + monoid: Monoid, +): Monad> => ({ + map, + pure: pure(monoid), + apply: apply(monoid), + flatMap: flatMap(monoid), +}); + /** * The instance of `Functor` for `Tuple<_, _>`. */ @@ -314,7 +378,7 @@ export const bifunctor: Bifunctor = { biMap }; * The instance of `Bitraversal` for `Tuple<_, _>`. */ export const bifoldable: Bifoldable = fromBifoldMap( - (m) => (aMap) => (bMap) => ([a, b]) => m.combine(aMap(a), bMap(b)), + bifoldMap, ); /** @@ -326,6 +390,15 @@ export const bitraversable: Bitraversable = { bitraverse, }; +/** + * The `Comonad` instance for `Tuple`. + */ +export const comonad = (): Comonad> => ({ + map, + extract, + duplicate, +}); + export const enc = (encA: Encoder) => (encB: Encoder): Encoder> =>