Skip to content

Commit

Permalink
feat: Add Compose and Add tests for ControlFlow (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikuroXina authored May 19, 2024
1 parent e9bd270 commit 9cec7be
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 11 deletions.
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * as Bool from "./src/bool.ts";
export * as Cat from "./src/cat.ts";
export * as Cofree from "./src/cofree.ts";
export * as ComonadCofree from "./src/cofree/comonad.ts";
export * as Compose from "./src/compose.ts";
export * as Const from "./src/const.ts";
export * as Cont from "./src/cont.ts";
export * as MonadCont from "./src/cont/monad.ts";
Expand Down
30 changes: 19 additions & 11 deletions src/array.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Get1, Hkt1 } from "./hkt.ts";
import { andThen, map, type Option, some } from "./option.ts";
import { andThen, map as mapOption, type Option, some } from "./option.ts";
import { and, type Ordering } from "./ordering.ts";
import {
type Decoder,
Expand Down Expand Up @@ -36,7 +36,7 @@ export const partialCmp =
foldR((
next: Option<Ordering>,
): (acc: Option<Ordering>) => Option<Ordering> =>
andThen((a: Ordering) => map(and(a))(next))
andThen((a: Ordering) => mapOption(and(a))(next))
)(some(Math.sign(l.length - r.length) as Ordering))(
l.map((left, i) => order.partialCmp(left, r[i])),
);
Expand All @@ -58,6 +58,19 @@ export interface ArrayHkt extends Hkt1 {
readonly type: readonly this["arg1"][];
}

export const map =
<T, U>(fn: (t: T) => U) => (src: readonly T[]): readonly U[] => src.map(fn);

export const pure = <T>(item: T): readonly T[] => [item];

export const apply =
<T, U>(fns: readonly ((t: T) => U)[]) => (ts: readonly T[]): readonly U[] =>
fns.flatMap((fn) => ts.map((t) => fn(t)));

export const flatMap =
<T, U>(fn: (t: T) => readonly U[]) => (src: readonly T[]): readonly U[] =>
src.flatMap(fn);

export const foldR: <A, B>(
folder: (next: A) => (acc: B) => B,
) => (init: B) => (data: readonly A[]) => B = (folder) => (init) => (data) =>
Expand All @@ -76,16 +89,11 @@ export const traverse =
return res;
};

export const functor: Functor<ArrayHkt> = {
map: (fn) => (t) => t.map(fn),
};
export const functor: Functor<ArrayHkt> = { map };

export const monad: Monad<ArrayHkt> = {
map: (fn) => (t) => t.map(fn),
pure: (t) => [t],
apply: (fns) => (ts) => fns.flatMap((fn) => ts.map((t) => fn(t))),
flatMap: (fn) => (t) => t.flatMap(fn),
};
export const applicative: Applicative<ArrayHkt> = { map, pure, apply };

export const monad: Monad<ArrayHkt> = { map, pure, apply, flatMap };

export const foldable: Foldable<ArrayHkt> = { foldR };

Expand Down
82 changes: 82 additions & 0 deletions src/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* This module provides a composing functor, `Compose<F, G, _>`.
*
* @packageDocumentation
* @module
*/

import type { Apply2Only, Apply3Only, Get1, Hkt3 } from "./hkt.ts";
import { type Applicative, liftA2 } from "./type-class/applicative.ts";
import type { Foldable } from "./type-class/foldable.ts";
import type { Functor } from "./type-class/functor.ts";
import type { Traversable } from "./type-class/traversable.ts";

/**
* Right to left composition of `F` and `G` functors.
*/
export type Compose<F, G, T> = Get1<F, Get1<G, T>>;

export interface ComposeHkt extends Hkt3 {
readonly type: Compose<this["arg3"], this["arg2"], this["arg1"]>;
}

/**
* Composes the two functors into a new one.
*
* @param f - The `Functor` instance for `F`.
* @param g - The `Functor` instance for `G`.
* @returns The composed functor.
*/
export const functor =
<F>(f: Functor<F>) =>
<G>(g: Functor<G>): Functor<Apply2Only<Apply3Only<ComposeHkt, F>, G>> => ({
map: (fn) => f.map(g.map(fn)),
});

/**
* Composes the two applicative functors into a new one.
*
* @param f - The `Applicative` instance for `F`.
* @param g - The `Applicative` instance for `G`.
* @returns The composed applicative functor.
*/
export const applicative = <F>(f: Applicative<F>) =>
<G>(
g: Applicative<G>,
): Applicative<Apply2Only<Apply3Only<ComposeHkt, F>, G>> => ({
pure: (t) => f.pure(g.pure(t)),
map: (fn) => f.map(g.map(fn)),
apply: liftA2(f)(g.apply),
});

/**
* Composes the two composing operators into a new one.
*
* @param f - The `Foldable` instance for `F`.
* @param g - The `Foldable` instance for `G`.
* @returns The composed folding operator.
*/
export const foldable = <F>(f: Foldable<F>) =>
<G>(
g: Foldable<G>,
): Foldable<Apply2Only<Apply3Only<ComposeHkt, F>, G>> => ({
foldR: <A, B>(folder: (next: A) => (acc: B) => B) =>
f.foldR((ga: Get1<G, A>) => (acc: B) => g.foldR(folder)(acc)(ga)),
});

/**
* Composes the two traversable functors into a new one.
*
* @param f - The `Traversable` instance for `F`.
* @param g - The `Traversable` instance for `G`.
* @returns The composed traversable functor.
*/
export const traversable = <F>(f: Traversable<F>) =>
<G>(
g: Traversable<G>,
): Traversable<Apply2Only<Apply3Only<ComposeHkt, F>, G>> => ({
map: (fn) => f.map(g.map(fn)),
foldR: <A, B>(folder: (next: A) => (acc: B) => B) =>
f.foldR((ga: Get1<G, A>) => (acc: B) => g.foldR(folder)(acc)(ga)),
traverse: (app) => (visitor) => f.traverse(app)(g.traverse(app)(visitor)),
});
226 changes: 226 additions & 0 deletions src/control-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { assertEquals } from "../deps.ts";
import { Array, Compose } from "../mod.ts";
import {
apply,
biMap,
breakValue,
continueValue,
type ControlFlow,
dec,
enc,
flatten,
foldR,
functor,
isBreak,
isContinue,
mapBreak,
mapContinue,
monad,
newBreak,
newContinue,
traversable,
} from "./control-flow.ts";
import { applicative as applicativeIdentity } from "./identity.ts";
import {
applicative as applicativeOption,
none,
type Option,
some,
} from "./option.ts";
import { unwrap } from "./result.ts";
import {
decU32Be,
decUtf8,
encU32Be,
encUtf8,
runCode,
runDecoder,
} from "./serial.ts";

const cases = [newContinue("foo"), newBreak("bar")] as const;

Deno.test("type assertion", () => {
const actual = cases.map((
c: ControlFlow<string, string>,
) => [isContinue(c), isBreak(c)]);
assertEquals(actual, [[true, false], [false, true]]);
});

Deno.test("extract", () => {
const actual = cases.map((c) => [continueValue(c), breakValue(c)]);
assertEquals(actual, [
[some("foo"), none()],
[none(), some("bar")],
]);
});

Deno.test("map", () => {
const actual = cases.map((
c,
) => [
mapContinue((x: string) => x.toUpperCase())(c),
mapBreak((x: string) => x.toUpperCase())(c),
]);
assertEquals(actual, [
[newContinue("FOO"), newContinue("foo")],
[newBreak("bar"), newBreak("BAR")],
]);
});

Deno.test("biMap", () => {
const actual = cases.map(
biMap((x: string) => x + "!")((x: string) => x + "?"),
);
assertEquals(actual, [
newContinue("foo?"),
newBreak("bar!"),
]);
});

Deno.test("flatten", () => {
assertEquals(flatten(newBreak("hoge")), newBreak("hoge"));
assertEquals(flatten(newContinue(newBreak("foo"))), newBreak("foo"));
assertEquals(flatten(newContinue(newContinue("bar"))), newContinue("bar"));
});

Deno.test("functor laws", () => {
const f = functor<never[]>();
const data = newContinue(2);
// identity
assertEquals(f.map((x: number) => x)(data), data);

// composition
const add = (x: number) => x + 3;
const mul = (x: number) => x * 2;
assertEquals(
f.map((x: number) => mul(add(x)))(data),
f.map(mul)(f.map(add)(data)),
);
});

Deno.test("applicative laws", () => {
const a = monad<never[]>();
const data = newContinue("2");
// identity
assertEquals(apply(a.pure((x: string) => x))(data), data);

// composition
const strLen = a.pure((x: string) => x.length);
const question = a.pure((x: string) => x + "?");
assertEquals(
apply(
apply(
apply(
a.pure(
(f: (x: string) => number) =>
(g: (x: string) => string) =>
(i: string) => f(g(i)),
),
)(strLen),
)(question),
)(data),
apply(strLen)(apply(question)(data)),
);

// homomorphism
assertEquals(
apply(a.pure((x: string) => x + "!"))(a.pure("foo")),
a.pure("foo!"),
);

// interchange
assertEquals(
apply(strLen)(a.pure("boo")),
apply(a.pure((f: (x: string) => number) => f("boo")))(strLen),
);
});

Deno.test("monad laws", () => {
const m = monad<number>();
const continuing = (x: string): ControlFlow<number, string> =>
newContinue(x);
const breaking = (x: string): ControlFlow<number, string> =>
newBreak(x.length);
const cases = [continuing, breaking];

// left identity
for (const c of cases) {
assertEquals(m.flatMap(c)(m.pure("baz")), c("baz"));
}

// right identity
assertEquals(m.flatMap(m.pure)(newContinue("a")), newContinue("a"));
assertEquals(m.flatMap(m.pure)(newBreak(2)), newBreak(2));

// associativity
for (const data of [newContinue("a"), newBreak(2)]) {
assertEquals(
m.flatMap(breaking)(m.flatMap(continuing)(data)),
m.flatMap((x: string) => m.flatMap(breaking)(continuing(x)))(data),
);
assertEquals(
m.flatMap(continuing)(m.flatMap(breaking)(data)),
m.flatMap((x: string) => m.flatMap(continuing)(breaking(x)))(data),
);
}
});

Deno.test("foldR", () => {
{
const actual = foldR((next: string) => (acc: string) => next + acc)("")(
cases[0],
);
assertEquals(actual, "foo");
}
{
const actual = foldR((next: string) => (acc: string) => next + acc)("")(
cases[1],
);
assertEquals(actual, "");
}
});

Deno.test("traversable laws", () => {
const t = traversable<string>();
// naturality
const first = <T>(
x: readonly T[],
): Option<T> => 0 in x ? some(x[0]) : none();
const dup = (x: string): readonly string[] => [x + "0", x + "1"];
const data = newContinue("fever");
assertEquals(
first(t.traverse(Array.applicative)(dup)(data)),
t.traverse(applicativeOption)((item: string) => first(dup(item)))(
data,
),
);

// identity
for (const c of cases) {
assertEquals(t.traverse(applicativeIdentity)((x: string) => x)(c), c);
}

// composition
const firstCh = (x: string): Option<string> =>
x.length > 0 ? some(x.charAt(0)) : none();
assertEquals(
t.traverse(Compose.applicative(Array.applicative)(applicativeOption))(
(x: string) => Array.map(firstCh)(dup(x)),
)(data),
Array.map(t.traverse(applicativeOption)(firstCh))(
t.traverse(Array.applicative)(dup)(data),
),
);
});

Deno.test("encode then decode", async () => {
const data: readonly ControlFlow<string, number>[] = [
newContinue(42),
newBreak("foo"),
];
for (const datum of data) {
const code = await runCode(enc(encUtf8)(encU32Be)(datum));
const decoded = unwrap(runDecoder(dec(decUtf8())(decU32Be()))(code));
assertEquals(datum, decoded);
}
});
10 changes: 10 additions & 0 deletions src/identity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { flip, id } from "./func.ts";
import type { Hkt1 } from "./hkt.ts";
import type { Applicative } from "./type-class/applicative.ts";
import type { Comonad } from "./type-class/comonad.ts";
import type { Distributive } from "./type-class/distributive.ts";
import type { Functor } from "./type-class/functor.ts";
Expand Down Expand Up @@ -30,6 +31,15 @@ export const functor: Functor<IdentityHkt> = {
map: id,
};

/**
* The `Applicative` instance for `Identity`.
*/
export const applicative: Applicative<IdentityHkt> = {
pure: id,
map: id,
apply: id,
};

/**
* The instance of `Monad` for `Identity`.
*/
Expand Down
Loading

0 comments on commit 9cec7be

Please sign in to comment.