-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Compose and Add tests for ControlFlow (#224)
- Loading branch information
1 parent
e9bd270
commit 9cec7be
Showing
7 changed files
with
353 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.