From a989187229fb0bfe431ec565000315e18a89367a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikuro=E3=81=95=E3=81=84=E3=81=AA?= Date: Tue, 23 Jul 2024 12:36:43 +0900 Subject: [PATCH] fix: Remove Promise from Encoder and Enforce tests for Graph (#243) --- src/graph.test.ts | 340 ++++++++++++++++++++++++++++++++++++++++- src/graph.ts | 181 ++++++++++++++-------- src/list.test.ts | 7 + src/list.ts | 66 ++++++-- src/serial.ts | 100 ++++++------ src/type-class.ts | 1 + src/type-class/hash.ts | 192 +++++++++++++++++++++++ 7 files changed, 757 insertions(+), 130 deletions(-) create mode 100644 src/type-class/hash.ts diff --git a/src/graph.test.ts b/src/graph.test.ts index 90408f5e..394f5c10 100644 --- a/src/graph.test.ts +++ b/src/graph.test.ts @@ -1,10 +1,348 @@ -import { dijkstra, fromEdges } from "./graph.ts"; +import { + adjsFrom, + bounds, + build, + canReach, + connectedComponents, + dijkstra, + type Edge, + edges, + fromEdges, + inDegree, + isCyclic, + outDegree, + postOrder, + preOrder, + reachableVertices, + reversedEdges, + stronglyConnectedComponents, + topologicalSort, + toReversed, + toUndirected, + type Vertex, + vertices, +} from "./graph.ts"; import { nonNanOrd } from "./type-class/ord.ts"; import { assertEquals } from "../deps.ts"; import { unwrap } from "./option.ts"; import { addMonoid } from "./type-class/monoid.ts"; +import { fromIterable, toArray } from "./list.ts"; +import { err, ok } from "./result.ts"; + +/* + 0 --> 1 + | \ ^ + | \ | + V \ | + 2 --> 3 +*/ +const simpleGraph = build([0 as Vertex, 3 as Vertex])(fromIterable([ + [0 as Vertex, 1 as Vertex], + [0 as Vertex, 3 as Vertex], + [0 as Vertex, 2 as Vertex], + [2 as Vertex, 3 as Vertex], + [3 as Vertex, 0 as Vertex], + [3 as Vertex, 1 as Vertex], +])); + +/* + downward arrows: + 0 + / \ + 1 2 + / \ | + 3 4 5 +*/ +const tree = build([0 as Vertex, 5 as Vertex])(fromIterable([ + [0 as Vertex, 1 as Vertex], + [0 as Vertex, 2 as Vertex], + [1 as Vertex, 3 as Vertex], + [1 as Vertex, 4 as Vertex], + [2 as Vertex, 5 as Vertex], +])); + +Deno.test("adjsFrom", () => { + assertEquals(toArray(adjsFrom(0 as Vertex)(simpleGraph)).toSorted(), [ + 1, + 2, + 3, + ]); + assertEquals(toArray(adjsFrom(1 as Vertex)(simpleGraph)).toSorted(), []); + assertEquals(toArray(adjsFrom(2 as Vertex)(simpleGraph)).toSorted(), [3]); + assertEquals(toArray(adjsFrom(3 as Vertex)(simpleGraph)).toSorted(), [ + 0, + 1, + ]); +}); + +Deno.test("vertices", () => { + assertEquals(toArray(vertices(simpleGraph)), [0, 1, 2, 3]); +}); + +Deno.test("edges", () => { + const actual = edges(simpleGraph); + assertEquals(toArray(actual), [ + [0 as Vertex, 2 as Vertex], + [0 as Vertex, 3 as Vertex], + [0 as Vertex, 1 as Vertex], + [2 as Vertex, 3 as Vertex], + [3 as Vertex, 1 as Vertex], + [3 as Vertex, 0 as Vertex], + ]); +}); + +Deno.test("bounds", () => { + assertEquals(bounds(simpleGraph), [0 as Vertex, 3 as Vertex]); +}); + +Deno.test("outDegree", () => { + assertEquals(outDegree(simpleGraph), [3, 0, 1, 2]); +}); + +Deno.test("inDegree", () => { + assertEquals(inDegree(simpleGraph), [1, 2, 1, 2]); +}); + +Deno.test("reversedEdges", () => { + const reversed = reversedEdges(tree); + assertEquals(toArray(reversed), [ + [2 as Vertex, 0 as Vertex], + [1 as Vertex, 0 as Vertex], + [4 as Vertex, 1 as Vertex], + [3 as Vertex, 1 as Vertex], + [5 as Vertex, 2 as Vertex], + ]); +}); + +Deno.test("toReversed", () => { + /* + upward arrows: + 0 + / \ + 1 2 + / \ | + 3 4 5 + */ + const reversed = toReversed(tree); + assertEquals(toArray(vertices(reversed)), [0, 1, 2, 3, 4, 5]); + assertEquals(toArray(edges(reversed)), [ + [1 as Vertex, 0 as Vertex], + [2 as Vertex, 0 as Vertex], + [3 as Vertex, 1 as Vertex], + [4 as Vertex, 1 as Vertex], + [5 as Vertex, 2 as Vertex], + ]); +}); + +Deno.test("preOrder", () => { + /* + 0 + / \ + 1 2 + / \ | + 3 4 5 + */ + assertEquals(preOrder(0 as Vertex)(tree), [0, 1, 3, 4, 2, 5]); +}); +Deno.test("postOrder", () => { + /* + 0 + / \ + 2 1 + | / \ + 5 4 3 + */ + assertEquals(postOrder(0 as Vertex)(tree), [5, 2, 4, 3, 1, 0]); +}); + +Deno.test("resolve dependencies", () => { + { + /* + 0 --------> 3 + | \ | + | \ | + V V V + 1 --> 2 --> 4 + */ + const { graph } = fromEdges(nonNanOrd)([ + ["0", 0, [1, 2, 3]], + ["1", 1, [2]], + ["2", 2, [4]], + ["3", 3, [4]], + ["4", 4, []], + ]); + const actual = topologicalSort(graph); + assertEquals( + actual, + ok([ + 0 as Vertex, + 3 as Vertex, + 1 as Vertex, + 2 as Vertex, + 4 as Vertex, + ]), + ); + } + { + /* + 0 <-------- 3 + | \ ^ + | \ | + V V | + 1 --> 2 --> 4 + */ + const { graph } = fromEdges(nonNanOrd)([ + ["0", 0, [1, 2]], + ["1", 1, [2]], + ["2", 2, [4]], + ["3", 3, [0]], + ["4", 4, [3]], + ]); + const actual = topologicalSort(graph); + assertEquals( + actual, + err({ at: [3 as Vertex, 0 as Vertex] as Edge }), + ); + } +}); + +Deno.test("isCyclic", () => { + assertEquals(isCyclic(simpleGraph), true); + { + /* + 0 --------> 3 + | \ | + | \ | + V V V + 1 --> 2 --> 4 + */ + const { graph } = fromEdges(nonNanOrd)([ + ["0", 0, [1, 2, 3]], + ["1", 1, [2]], + ["2", 2, [4]], + ["3", 3, [4]], + ["4", 4, []], + ]); + assertEquals(isCyclic(graph), false); + } + { + /* + 0 <-------- 3 + | \ ^ + | \ | + V V | + 1 --> 2 --> 4 + */ + const { graph } = fromEdges(nonNanOrd)([ + ["0", 0, [1, 2]], + ["1", 1, [2]], + ["2", 2, [4]], + ["3", 3, [0]], + ["4", 4, [3]], + ]); + assertEquals(isCyclic(graph), true); + } +}); + +Deno.test("toUndirected", () => { + /* + Expected: + 0 --- 1 + | \ | + | \ | + | \ | + 2 --- 3 + */ + const undirected = toUndirected(simpleGraph); + assertEquals(toArray(edges(undirected)), [ + [0 as Vertex, 1 as Vertex], + [0 as Vertex, 3 as Vertex], + [0 as Vertex, 2 as Vertex], + [1 as Vertex, 3 as Vertex], + [1 as Vertex, 0 as Vertex], + [2 as Vertex, 0 as Vertex], + [2 as Vertex, 3 as Vertex], + [3 as Vertex, 2 as Vertex], + [3 as Vertex, 0 as Vertex], + [3 as Vertex, 1 as Vertex], + ]); +}); + +Deno.test("weakly connected components", () => { + const cc = connectedComponents(simpleGraph); + assertEquals(cc.map((component) => [...component].toSorted()), [[ + 0, + 1, + 2, + 3, + ]]); +}); + +Deno.test("strongly connected components", () => { + /* + 0 --------> 3 + | ^ ^ + | \ | + V \ V + 1 --> 2 --> 4 + */ + const { graph, indexVertex } = fromEdges(nonNanOrd)([ + ["0", 0, [1, 3]], + ["1", 1, [2]], + ["2", 2, [0, 4]], + ["3", 3, [4]], + ["4", 4, [3]], + ]); + const scc = stronglyConnectedComponents(graph); + assertEquals(scc.map((component) => [...component].toSorted()), [ + [ + unwrap(indexVertex(0)), + unwrap(indexVertex(1)), + unwrap(indexVertex(2)), + ], + [ + unwrap(indexVertex(3)), + unwrap(indexVertex(4)), + ], + ]); +}); + +Deno.test("reachableVertices", () => { + assertEquals([...reachableVertices(0 as Vertex)(tree)], [0, 1, 3, 4, 2, 5]); + assertEquals([...reachableVertices(1 as Vertex)(tree)], [1, 3, 4]); + assertEquals([...reachableVertices(2 as Vertex)(tree)], [2, 5]); + assertEquals([...reachableVertices(3 as Vertex)(tree)], [3]); + assertEquals([...reachableVertices(4 as Vertex)(tree)], [4]); + assertEquals([...reachableVertices(5 as Vertex)(tree)], [5]); +}); + +Deno.test("canReach", () => { + assertEquals(canReach(0 as Vertex)(0 as Vertex)(simpleGraph), true); + assertEquals(canReach(0 as Vertex)(1 as Vertex)(simpleGraph), true); + assertEquals(canReach(0 as Vertex)(2 as Vertex)(simpleGraph), true); + assertEquals(canReach(0 as Vertex)(3 as Vertex)(simpleGraph), true); + assertEquals(canReach(1 as Vertex)(0 as Vertex)(simpleGraph), false); + assertEquals(canReach(1 as Vertex)(1 as Vertex)(simpleGraph), true); + assertEquals(canReach(1 as Vertex)(2 as Vertex)(simpleGraph), false); + assertEquals(canReach(1 as Vertex)(3 as Vertex)(simpleGraph), false); + assertEquals(canReach(2 as Vertex)(0 as Vertex)(simpleGraph), true); + assertEquals(canReach(2 as Vertex)(1 as Vertex)(simpleGraph), true); + assertEquals(canReach(2 as Vertex)(2 as Vertex)(simpleGraph), true); + assertEquals(canReach(2 as Vertex)(3 as Vertex)(simpleGraph), true); + assertEquals(canReach(3 as Vertex)(0 as Vertex)(simpleGraph), true); + assertEquals(canReach(3 as Vertex)(1 as Vertex)(simpleGraph), true); + assertEquals(canReach(3 as Vertex)(2 as Vertex)(simpleGraph), true); + assertEquals(canReach(3 as Vertex)(3 as Vertex)(simpleGraph), true); +}); Deno.test("dijkstra", () => { + /* + 0 <-------> 3 + ^ ^ ^ + | \ | + V V V + 1 <-> 2 --> 4 + */ const { graph, indexVertex } = fromEdges(nonNanOrd)([ ["0", 0, [1, 2, 3]], ["1", 1, [0, 2]], diff --git a/src/graph.ts b/src/graph.ts index 8ad52427..057cbb51 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -26,17 +26,21 @@ import { plus, range, toIterator, + unique, } from "./list.ts"; import { none, type Option, some, unwrap } from "./option.ts"; import { equal, greater, less } from "./ordering.ts"; import { err, isErr, ok, type Result } from "./result.ts"; import { BinaryHeap } from "../mod.ts"; import type { Monoid } from "./type-class/monoid.ts"; -import { fromProjection, type Ord } from "./type-class/ord.ts"; +import { fromProjection, nonNanOrd, type Ord } from "./type-class/ord.ts"; import type { Apply2Only, Hkt1 } from "./hkt.ts"; import type { HasInf } from "./type-class/has-inf.ts"; import { doMut, type Mut, type MutHkt, type MutRef } from "./mut.ts"; import { mapMIgnore } from "./type-class/foldable.ts"; +import { fromEncoder } from "./type-class/hash.ts"; +import { enc, eq } from "./tuple.ts"; +import { encU32Le } from "./serial.ts"; declare const vertexNominal: unique symbol; /** @@ -175,7 +179,12 @@ export const fromEdges = * @returns The new graph. */ export const build = ([start, end]: Bounds) => (edges: List): Graph => { - const graph = [] as List[]; + if (!(start <= end)) { + throw new Error("`start` must be less than or equals to `end`"); + } + const graph = [...new Array(end + 1)].map(() => empty()) as List< + Vertex + >[]; for (const [from, to] of toIterator(edges)) { if (!(start <= from && from <= end)) { throw new Error("`from` is out of bounds"); @@ -183,9 +192,6 @@ export const build = ([start, end]: Bounds) => (edges: List): Graph => { if (!(start <= to && to <= end)) { throw new Error("`to` is out of bounds"); } - if (!(from in graph)) { - graph[from] = empty(); - } graph[from] = appendToHead(to)(graph[from]); } return graph; @@ -279,31 +285,42 @@ export type CycleError = Readonly<{ * @returns The topological order vertices of `Ok`, or a `CycleError` on `Err` only if there is a cycle. */ export const topologicalSort = (graph: Graph): Result => { - const nodes = [] as Vertex[]; - const visited = new Set(); - const visit = (visiting: Vertex): Result => { - visited.add(visiting); - for (const next of toIterator(adjsFrom(visiting)(graph))) { - if (visited.has(next)) { - return err({ at: [visiting, next] }); - } - const res = visit(next); - if (isErr(res)) { - return res; + function dfs( + vertex: Vertex, + graph: Graph, + visited: boolean[], + res: Vertex[], + ): void { + visited[vertex] = true; + for (const next of toIterator(graph[vertex]!)) { + if (!visited[next]) { + dfs(next, graph, visited, res); } } - nodes.push(visiting); - return ok([]); - }; - for (let start = 0 as Vertex; start < graph.length; ++start) { - if (!visited.has(start)) { - const res = visit(start); - if (isErr(res)) { - return res; + res.push(vertex); + } + const res = [] as Vertex[]; + const visited = new Array(graph.length).fill(false); + for (let vertex = 0 as Vertex; vertex < graph.length; ++vertex) { + if (vertex in graph && !visited[vertex]) { + dfs(vertex, graph, visited, res); + } + } + res.reverse(); + + const posByVertex = new Map(res.map((vertex, i) => [vertex, i])); + for (let from = 0 as Vertex; from < graph.length; ++from) { + if (from in graph) { + for (const to of toIterator(graph[from]!)) { + if ( + !((posByVertex.get(from) ?? 0) < (posByVertex.get(to) ?? 0)) + ) { + return err({ at: [from, to] }); + } } } } - return ok(nodes.toReversed()); + return ok(res); }; /** @@ -312,31 +329,8 @@ export const topologicalSort = (graph: Graph): Result => { * @param graph - To be checked, * @returns Whether a cycle exists. */ -export const isCyclic = (graph: Graph): boolean => { - const visited = new Set(); - const visit = (visiting: Vertex): boolean => { - visited.add(visiting); - for (const next of toIterator(adjsFrom(visiting)(graph))) { - if (visited.has(next)) { - return true; - } - const hasCycle = visit(next); - if (hasCycle) { - return hasCycle; - } - } - return false; - }; - for (let start = 0 as Vertex; start < graph.length; ++start) { - if (!visited.has(start)) { - const hasCycle = visit(start); - if (hasCycle) { - return hasCycle; - } - } - } - return false; -}; +export const isCyclic = (graph: Graph): boolean => + isErr(topologicalSort(graph)); /** * Makes the graph undirected. It duplicates edges into opposite ones. @@ -345,7 +339,13 @@ export const isCyclic = (graph: Graph): boolean => { * @returns The naive undirected graph. */ export const toUndirected = (graph: Graph): Graph => - build(bounds(graph))(plus(edges(graph))(reversedEdges(graph))); + build(bounds(graph))( + unique( + fromEncoder(eq({ equalityA: nonNanOrd, equalityB: nonNanOrd }))( + enc(encU32Le)(encU32Le), + ), + )(plus(edges(graph))(reversedEdges(graph))), + ); /** * Decomposes the graph into connected components. @@ -355,7 +355,34 @@ export const toUndirected = (graph: Graph): Graph => */ export const connectedComponents = (graph: Graph): Set[] => { const undirected = toUndirected(graph); - const components = [] as Set[]; + /* + * `unionFind[idx]` means: + * - negative value of its tree size if negative, + * - parent index if positive. + */ + const unionFind = new Array(graph.length).fill(-1); + const repr = (idx: number): number => { + if (unionFind[idx] < 0) { + return idx; + } + const parent = repr(unionFind[idx]); + unionFind[idx] = parent; + return parent; + }; + const union = (a: number, b: number) => { + a = repr(a); + b = repr(b); + if (a === b) { + return; + } + if (unionFind[a] > unionFind[b]) { + [a, b] = [b, a]; + } + const greaterRoot = unionFind[b]; + unionFind[a] += greaterRoot; + unionFind[b] = a; + }; + for (let start = 0 as Vertex; start < undirected.length; ++start) { const visited = new Set(); const stack = [start]; @@ -364,13 +391,21 @@ export const connectedComponents = (graph: Graph): Set[] => { visited.add(visiting); for (const next of toIterator(adjsFrom(visiting)(undirected))) { if (!visited.has(next)) { + union(visiting, next); stack.push(next); } } } - components.push(visited); } - return components; + const components = [] as (Set | undefined)[]; + for (let idx = 0; idx < unionFind.length; ++idx) { + const parentPos = repr(idx); + if (!components[parentPos]) { + components[parentPos] = new Set(); + } + components[parentPos].add(idx as Vertex); + } + return components.filter((set) => !!set); }; /** @@ -381,35 +416,45 @@ export const connectedComponents = (graph: Graph): Set[] => { */ export const stronglyConnectedComponents = (graph: Graph): Set[] => { const deadEnds = [] as Vertex[]; - const visited = new Set(); - const visit = (visiting: Vertex) => { - visited.add(visiting); - for (const next of toIterator(adjsFrom(visiting)(graph))) { - if (!visited.has(next)) { - visit(next); + { + const visited = new Set(); + const visit = (visiting: Vertex) => { + visited.add(visiting); + for (const next of toIterator(adjsFrom(visiting)(graph))) { + if (!visited.has(next)) { + visit(next); + } + } + deadEnds.push(visiting); + }; + for (let start = 0 as Vertex; start < graph.length; ++start) { + if (!visited.has(start)) { + visit(start); } } - deadEnds.push(visiting); - }; - for (let start = 0 as Vertex; start < graph.length; ++start) { - visit(start); } + const reversed = toReversed(graph); const components = [] as Set[]; + const visited = new Set(); while (deadEnds.length > 0) { const componentStart = deadEnds.pop()!; - const visited = new Set(); + if (visited.has(componentStart)) { + continue; + } + const component = new Set(); const stack = [componentStart]; while (stack.length > 0) { const visiting = stack.pop()!; visited.add(visiting); + component.add(visiting); for (const next of toIterator(adjsFrom(visiting)(reversed))) { if (!visited.has(next)) { stack.push(next); } } } - components.push(visited); + components.push(component); } return components; }; @@ -447,6 +492,10 @@ export const reachableVertices = */ export const canReach = (start: Vertex) => (goal: Vertex) => (graph: Graph): boolean => { + if (start === goal) { + return true; + } + const visited = new Set(); const stack = [start]; while (stack.length > 0) { diff --git a/src/list.test.ts b/src/list.test.ts index a5f65c61..2640b564 100644 --- a/src/list.test.ts +++ b/src/list.test.ts @@ -65,6 +65,7 @@ import { traversable, tupleCartesian, unfoldR, + unique, unzip, zip3, zip4, @@ -78,6 +79,7 @@ import { applicative as arrayApp } from "./array.ts"; import { applicative as identityApp } from "./identity.ts"; import { decU32Be, encU32Be, runCode, runDecoder } from "./serial.ts"; import { unwrap } from "./result.ts"; +import { nonNanHash } from "./type-class/hash.ts"; Deno.test("with CatT", () => { // Find patterns where `x + y + z == 5` for all natural number `x`, `y`, and `z`. @@ -738,6 +740,11 @@ Deno.test("group", () => { assertEquals(grouped, ["M", "i", "ss", "i", "ss", "i", "pp", "i"]); }); +Deno.test("unique", () => { + const uniqueNums = unique(nonNanHash)(fromIterable([1, 4, 2, 3, 5, 2, 3])); + assertEquals(toArray(uniqueNums), [1, 4, 2, 3, 5]); +}); + Deno.test("tupleCartesian", () => { const deltas = tupleCartesian(range(-1, 2))(range(-1, 2)); assertEquals(toArray(deltas), [ diff --git a/src/list.ts b/src/list.ts index b4a82980..387943d6 100644 --- a/src/list.ts +++ b/src/list.ts @@ -141,6 +141,7 @@ import { type Applicative, liftA2 } from "./type-class/applicative.ts"; import { type Eq, fromEquality } from "./type-class/eq.ts"; import type { Foldable } from "./type-class/foldable.ts"; import type { Functor } from "./type-class/functor.ts"; +import { defaultHasher, type Hash } from "./type-class/hash.ts"; import type { Monad } from "./type-class/monad.ts"; import type { Monoid } from "./type-class/monoid.ts"; import { fromCmp, type Ord } from "./type-class/ord.ts"; @@ -777,6 +778,11 @@ export const foldL1 = (f: (a: T) => (b: T) => T) => (list: List): T => /** * Folds the elements of list from right. * + * Applying `foldR` to infinite structures usually doesn't terminate. But it may still terminate under one of the following conditions: + * + * - the folding function is short-circuiting, + * - the folding function is lazy on its second argument. + * * @param f - The fold operation. * @param init - The initial value of the operation. * @param list - The target list. @@ -799,12 +805,21 @@ export const foldL1 = (f: (a: T) => (b: T) => T) => (list: List): T => * ``` */ export const foldR = - (f: (a: T) => (b: U) => U) => (init: U): (list: List) => U => { - const go = (list: List): U => - Option.mapOr(init)(([y, ys]: [T, List]) => f(y)(go(ys)))( - unCons(list), - ); - return go; + (f: (a: T) => (b: U) => U) => (init: U) => (list: List): U => { + const stack: T[] = []; + while (true) { + const curr = list.current(); + if (Option.isNone(curr)) { + break; + } + stack.push(Option.unwrap(curr)); + list = list.rest(); + } + let res = init; + for (let i = stack.length - 1; 0 <= i; --i) { + res = f(stack[i])(res); + } + return res; }; /** @@ -2087,15 +2102,48 @@ export const group = ( groupBy((l) => (r) => equalityT.eq(l, r)); /** - * Filters the list by `pred`. The elements which satisfy `pred` are only passed. + * Filters the list items from the head by `pred`. The elements which satisfy `pred` are only passed. * * @param pred - The condition to pick up an element. * @returns The filtered list. */ export const filter = ( pred: (element: T) => boolean, -): (list: List) => List => - flatMap((element) => (pred(element) ? singleton(element) : empty())); +) => +(list: List): List => + Option.mapOr(list)(([x, xs]: [T, List]): List => + pred(x) ? appendToHead(x)(filter(pred)(xs)) : filter(pred)(xs) + )(unCons(list)); + +/** + * Removes duplicated elements by comparing the equality. + * + * @param equality - The condition to determine whether two items are same. + * @param list - The list to be filtered. + * @returns The filtered list. + * + * # Examples + * + * ```ts + * import { fromIterable, toArray, unique } from "./list.ts"; + * import { assertEquals } from "../deps.ts"; + * import { nonNanHash } from "./type-class/hash.ts"; + * + * const uniqueNums = unique(nonNanHash)(fromIterable([1, 4, 2, 3, 5, 2, 3])); + * assertEquals(toArray(uniqueNums), [1, 4, 2, 3, 5]); + * ``` + */ +export const unique = (hasher: Hash): (list: List) => List => { + const known = new Map(); + return filter((item: T) => { + const hash = hasher.hash(item)(defaultHasher()).state(); + if (!known.has(hash)) { + known.set(hash, item); + return true; + } + return !hasher.eq(item, known.get(hash)!); + }); +}; /** * Extracts the diagonals from the two-dimensional list. diff --git a/src/serial.ts b/src/serial.ts index e891868a..d01206d7 100644 --- a/src/serial.ts +++ b/src/serial.ts @@ -50,7 +50,7 @@ import { semiGroupSymbol } from "./type-class/semi-group.ts"; /** * A step of building a serial of binaries. It produces a signal to write data on `range` of bytes. */ -export type BuildStep = (range: BufferRange) => Promise>; +export type BuildStep = (range: BufferRange) => BuildSignal; /** * A range of data bytes that required to build a serial of binaries. @@ -119,21 +119,21 @@ export type BuildSignal = Readonly< */ export const fillWithBuildStep = (step: BuildStep) => - (onDone: (nextFreeIndex: number) => (computed: T) => Promise) => + (onDone: (nextFreeIndex: number) => (computed: T) => U) => ( onBufferFull: ( nextMinimalSize: number, ) => ( currentFreeIndex: number, - ) => (nextToRun: BuildStep) => Promise, + ) => (nextToRun: BuildStep) => U, ) => ( onInsertChunk: ( currentFreeIndex: number, - ) => (toInsert: DataView) => (nextToRun: BuildStep) => Promise, + ) => (toInsert: DataView) => (nextToRun: BuildStep) => U, ) => - async (range: BufferRange): Promise => { - const signal = await step(range); + (range: BufferRange): U => { + const signal = step(range); switch (signal.type) { case buildDoneNominal: return onDone(signal.nextFreeIndex)(signal.computed); @@ -153,12 +153,11 @@ export const fillWithBuildStep = /** * An identity build step that does nothing. */ -export const finalStep: BuildStep = ([start]) => - Promise.resolve({ - type: buildDoneNominal, - nextFreeIndex: start, - computed: [], - }); +export const finalStep: BuildStep = ([start]) => ({ + type: buildDoneNominal, + nextFreeIndex: start, + computed: [], +}); /** * A function that transforms between build steps on any type `I`. @@ -193,13 +192,12 @@ export const concat = /** * Flushes empty data to force to output before going on. */ -export const flush: Builder = (step) => ([start]) => - Promise.resolve({ - type: insertChunkNominal, - currentFreeIndex: start, - toInsert: new DataView(new ArrayBuffer(0)), - nextToRun: step, - }); +export const flush: Builder = (step) => ([start]) => ({ + type: insertChunkNominal, + currentFreeIndex: start, + toInsert: new DataView(new ArrayBuffer(0)), + nextToRun: step, +}); /** * Writes binaries of bytes from a `DataView`. @@ -209,21 +207,19 @@ export const flush: Builder = (step) => ([start]) => */ export const bytesBuilder = (bytes: DataView): Builder => (step) => ([start, length]) => - Promise.resolve( - length < bytes.byteLength - ? { - type: bufferFullNominal, - neededMinimalSize: bytes.byteLength, - currentFreeIndex: start, - nextToRun: step, - } - : { - type: insertChunkNominal, - currentFreeIndex: start + bytes.byteLength, - toInsert: bytes, - nextToRun: step, - }, - ); + length < bytes.byteLength + ? { + type: bufferFullNominal, + neededMinimalSize: bytes.byteLength, + currentFreeIndex: start, + nextToRun: step, + } + : { + type: insertChunkNominal, + currentFreeIndex: start + bytes.byteLength, + toInsert: bytes, + nextToRun: step, + }; /** * Writes a number as a signed 8-bit integer. @@ -455,7 +451,7 @@ export type AllocationStrategy = { * @param old - When it is `Some`, old buffer and required minimal size on reallocation. * @returns The new `ArrayBuffer`. */ - allocator: (old: Option<[ArrayBuffer, number]>) => Promise; + allocator: (old: Option<[ArrayBuffer, number]>) => ArrayBuffer; /** * Decides that the current `ArrayBuffer` should be trimmed. * @@ -487,11 +483,9 @@ const resize = (targetLen: number) => (buf: ArrayBuffer): ArrayBuffer => { export const untrimmedStrategy = (firstLen: number): AllocationStrategy => ({ allocator: (old) => { if (isNone(old)) { - return Promise.resolve( - new ArrayBuffer(firstLen), - ); + return new ArrayBuffer(firstLen); } - return Promise.resolve(resize(old[1][1])(old[1][0])); + return resize(old[1][1])(old[1][0]); }, shouldBeTrimmed: () => () => false, }); @@ -505,11 +499,9 @@ export const untrimmedStrategy = (firstLen: number): AllocationStrategy => ({ export const safeStrategy = (firstLen: number): AllocationStrategy => ({ allocator: (old) => { if (isNone(old)) { - return Promise.resolve( - new ArrayBuffer(firstLen), - ); + return new ArrayBuffer(firstLen); } - return Promise.resolve(resize(old[1][1])(old[1][0])); + return resize(old[1][1])(old[1][0]); }, shouldBeTrimmed: (used) => (len) => 2 * used < len, }); @@ -518,13 +510,12 @@ export const safeStrategy = (firstLen: number): AllocationStrategy => ({ * Builds bytes into an `ArrayBuffer` with a `Builder` by custom strategy. */ export const intoBytesWith = - (strategy: AllocationStrategy) => - async (builder: Builder): Promise => { - let buf = await strategy.allocator(none()); + (strategy: AllocationStrategy) => (builder: Builder): ArrayBuffer => { + let buf = strategy.allocator(none()); let currentIndex = 0; let step = runBuilder(builder); while (true) { - const signal = await step([ + const signal = step([ currentIndex, buf.byteLength - currentIndex, ]); @@ -538,7 +529,7 @@ export const intoBytesWith = } return buf; case bufferFullNominal: - buf = await strategy.allocator( + buf = strategy.allocator( some([buf, signal.neededMinimalSize]), ); step = signal.nextToRun; @@ -561,8 +552,9 @@ export const intoBytesWith = /** * Builds bytes into an `ArrayBuffer` with a `Builder`. The buffer is pre-allocated in 4 KiB and driven by `safeStrategy`. */ -export const intoBytes: (builder: Builder) => Promise = - intoBytesWith(safeStrategy(4 * 1024)); +export const intoBytes: (builder: Builder) => ArrayBuffer = intoBytesWith( + safeStrategy(4 * 1024), +); /** * Encoded result. A tuple of computation result `T` and builder. @@ -620,7 +612,7 @@ export const execCodeM = ([, b]: CodeM): Builder => b; * @param code - An encode result. * @returns The serialized `ArrayBuffer`. */ -export const runCode = ([, b]: Code): Promise => intoBytes(b); +export const runCode = ([, b]: Code): ArrayBuffer => intoBytes(b); /** * Transform a `CodeM` into result and `ArrayBuffer` of byte sequence. @@ -628,11 +620,11 @@ export const runCode = ([, b]: Code): Promise => intoBytes(b); * @param code - An encode result. * @returns The result and serialized `ArrayBuffer`. */ -export const runCodeM = async ( +export const runCodeM = ( put: CodeM, -): Promise => [ +): readonly [result: T, ArrayBuffer] => [ put[0], - await intoBytes(put[1]), + intoBytes(put[1]), ]; /** diff --git a/src/type-class.ts b/src/type-class.ts index e5eddd2e..8988a6e0 100644 --- a/src/type-class.ts +++ b/src/type-class.ts @@ -19,6 +19,7 @@ export * as FlatMap from "./type-class/flat-map.ts"; export * as Foldable from "./type-class/foldable.ts"; export * as Functor from "./type-class/functor.ts"; export * as Group from "./type-class/group.ts"; +export * as Hash from "./type-class/hash.ts"; export * as HasInf from "./type-class/has-inf.ts"; export * as HasNegInf from "./type-class/has-neg-inf.ts"; export * as Indexable from "./type-class/indexable.ts"; diff --git a/src/type-class/hash.ts b/src/type-class/hash.ts new file mode 100644 index 00000000..6be222ee --- /dev/null +++ b/src/type-class/hash.ts @@ -0,0 +1,192 @@ +import { doT } from "../cat.ts"; +import { unwrap } from "../result.ts"; +import { + decU16Le, + decU32Le, + decU64Le, + decU8, + type Encoder, + encU32Le, + monadForDecoder, + runCode, + runDecoder, + skip, +} from "../serial.ts"; +import type { Eq } from "./eq.ts"; +import { nonNanOrd } from "./ord.ts"; + +export interface Hash extends Eq { + readonly hash: (self: T) => (hasher: Hasher) => Hasher; +} + +export const nonNanHash: Hash = { + ...nonNanOrd, + hash: (self) => (hasher) => { + if (Number.isNaN(self)) { + throw new Error("NaN is not allowed for this hash impl"); + } + return hasher.write(runCode(encU32Le(self))); + }, +}; +export const fromEncoder = + (equality: Eq) => (encoder: Encoder): Hash => ({ + ...equality, + hash: (self) => (hasher) => hasher.write(runCode(encoder(self))), + }); + +export type Hasher = Readonly<{ + state: () => bigint; + write: (bytes: ArrayBuffer) => Hasher; +}>; + +type SipState = [v0: bigint, v1: bigint, v2: bigint, v3: bigint]; + +const wrappingAddU64 = (a: bigint, b: bigint): bigint => + BigInt.asUintN(64, a + b); +const rotateLeftU64 = (v: bigint, amount: number): bigint => + BigInt.asUintN(64, (v << BigInt(amount)) | (v >> BigInt(64 - amount))); +const compress = ([v0, v1, v2, v3]: SipState): SipState => { + v0 = wrappingAddU64(v0, v1); + v1 = rotateLeftU64(v1, 13); + v1 ^= v0; + v0 = rotateLeftU64(v0, 32); + v2 = wrappingAddU64(v2, v3); + v3 = rotateLeftU64(v3, 16); + v3 ^= v2; + v0 = wrappingAddU64(v0, v3); + v3 = rotateLeftU64(v3, 21); + v3 ^= v0; + v2 = wrappingAddU64(v2, v1); + v1 ^= v2; + v2 = rotateLeftU64(v2, 32); + return [v0, v1, v2, v3]; +}; +const cRounds: (state: SipState) => SipState = compress; +const dRounds = (state: SipState): SipState => + compress(compress(compress(state))); +const reset = ( + k0: bigint, + k1: bigint, +): SipState => [ + k0 ^ 0x736f6d6570736575n, + k1 ^ 0x646f72616e646f6dn, + k0 ^ 0x6c7967656e657261n, + k1 ^ 0x7465646279746573n, +]; + +type SipHasher = { + k0: bigint; + k1: bigint; + length: number; + state: SipState; + tail: bigint; + nTail: number; +}; +const newSip13: SipHasher = { + k0: 0n, + k1: 0n, + length: 0, + state: reset(0n, 0n), + tail: 0n, + nTail: 0, +}; + +const loadIntLe = ( + bytes: ArrayBuffer, + index: number, + bits: number, +): bigint => + unwrap( + runDecoder( + doT(monadForDecoder) + .run(skip(index)) + .finishM(() => { + switch (bits) { + case 8: + return monadForDecoder.map(BigInt)(decU8()); + case 16: + return monadForDecoder.map(BigInt)(decU16Le()); + case 32: + return monadForDecoder.map(BigInt)(decU32Le()); + case 64: + return decU64Le(); + default: + throw new Error("unexpected bits"); + } + }), + )(bytes), + ); + +const bytesToBigIntLe = ( + bytes: ArrayBuffer, + start: number, + len: number, +): bigint => { + let i = 0; + let out = 0n; + if (i + 3 < len) { + out |= loadIntLe(bytes, start + i, 32); + i += 4; + } + if (i + 1 < len) { + out |= loadIntLe(bytes, start + i, 16) << BigInt(i * 8); + i += 2; + } + if (i < len) { + out |= loadIntLe(bytes, start + i, 8) << BigInt(i * 8); + i += 1; + } + return out; +}; + +/** + * The default hasher implementation. It uses [SipHash 1-3](https://131002.net/siphash). + */ +export const defaultHasher = (sipHasher: SipHasher = newSip13): Hasher => ({ + state: () => { + let state = sipHasher.state; + const { length, tail } = sipHasher; + const base = ((BigInt(length) & 0xffn) << 56n) | tail; + + state[3] ^= base; + state = cRounds(state); + state[0] ^= base; + + state[2] ^= 0xffn; + state = dRounds(state); + + return state[0] ^ state[1] ^ state[2] ^ state[3]; + }, + write: (bytes) => { + const length = bytes.byteLength; + const next: SipHasher = { ...sipHasher, state: [...sipHasher.state] }; + next.length += length; + let needed = 0; + if (next.nTail !== 0) { + needed = 8 - next.nTail; + next.tail |= bytesToBigIntLe(bytes, 0, Math.min(length, needed)) << + (8n * BigInt(next.nTail)); + if (length < needed) { + next.nTail += length; + return defaultHasher(next); + } + next.state[3] ^= next.tail; + next.state = cRounds(next.state); + next.state[0] ^= next.tail; + next.nTail = 0; + } + + const len = length - needed; + const left = len % 8; + let i = needed; + for (; i < len - left; i += 8) { + const mi = loadIntLe(bytes, i, 64); + next.state[3] ^= mi; + next.state = cRounds(next.state); + next.state[0] ^= mi; + } + next.tail = bytesToBigIntLe(bytes, i, left); + next.nTail = left; + return defaultHasher(next); + }, +});