From 9c781fcfee8936390747c8ee06e34cda3fb49888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Mon, 6 Aug 2018 19:56:25 -0700 Subject: [PATCH] Add `NodeTrie` and `EdgeTrie` classes (#612) For #465, I'm planning to create an abstraction over NodeTypes and EdgeTypes which traverses a hierarchy of types and aggregates/reduces information across all the matching types for a given Node/Edge address. To do that efficiently, we will want tries[1]. Thanks to @wchargin for helping me figure out how to implement this. Test plan: Unit tests. The code is a little tricky so please review it closely. [1]: https://en.wikipedia.org/wiki/Trie --- src/core/trie.js | 99 +++++++++++++++++++++++++++++++++++++++++++ src/core/trie.test.js | 98 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/core/trie.js create mode 100644 src/core/trie.test.js diff --git a/src/core/trie.js b/src/core/trie.js new file mode 100644 index 0000000..a4242f4 --- /dev/null +++ b/src/core/trie.js @@ -0,0 +1,99 @@ +// @flow + +import {type AddressModule} from "./address"; +import { + NodeAddress, + type NodeAddressT, + EdgeAddress, + type EdgeAddressT, +} from "./graph"; +import * as NullUtil from "../util/null"; + +const EMPTY_ENTRY_SYMBOL = Symbol("EMPTY"); + +type Entry = {|+map: RecursiveMap, value: V | typeof EMPTY_ENTRY_SYMBOL|}; +type RecursiveMap = Map>; +class BaseTrie { + addressModule: AddressModule; + entry: Entry; + + /** + * Create an empty trie backed by the given address module. + */ + constructor(m: AddressModule) { + this.addressModule = m; + this.entry = {value: EMPTY_ENTRY_SYMBOL, map: new Map()}; + } + + /** + * Add key `k` to this trie with value `v`. Return `this`. + */ + add(k: K, val: V): this { + const parts = this.addressModule.toParts(k); + let entry = this.entry; + for (const part of parts) { + if (!entry.map.has(part)) { + entry.map.set(part, {map: new Map(), value: EMPTY_ENTRY_SYMBOL}); + } + entry = NullUtil.get(entry.map.get(part)); + } + if (entry.value !== EMPTY_ENTRY_SYMBOL) { + throw new Error( + `Tried to overwrite entry at ${this.addressModule.toString(k)}` + ); + } + entry.value = val; + return this; + } + + /** + * Get the values in this trie along the path to `k`. + * + * More specifically, this method has the following observable + * behavior. Let `inits` be the list of all prefixes of `k`, ordered + * by length (shortest to longest). Observe that the length of `inits` + * is `n + 1`, where `n` is the number of parts of `k`; `inits` begins + * with the empty address and ends with `k` itself. Initialize the + * result to an empty array. For each prefix `p` in `inits`, if `p` + * was added to this trie with value `v`, then append `v` to + * `result`. Return `result`. + */ + get(k: K): V[] { + const parts = this.addressModule.toParts(k); + const result: V[] = []; + let entry: Entry = this.entry; + // nb: if parts has length `n`, there are `n+1` opportunities to add a + // value to the resultant array, which is correct as there may be `n+1` + // appropriate values to return: one for each part, and another for the + // empty address. + for (const part of parts) { + if (entry.value !== EMPTY_ENTRY_SYMBOL) { + const value: V = (entry.value: any); + result.push(value); + } + const tmpEntry = entry.map.get(part); + if (tmpEntry == null) { + return result; + } else { + entry = tmpEntry; + } + } + if (entry.value !== EMPTY_ENTRY_SYMBOL) { + const value: V = (entry.value: any); + result.push(value); + } + return result; + } +} + +export class NodeTrie extends BaseTrie { + constructor() { + super(NodeAddress); + } +} + +export class EdgeTrie extends BaseTrie { + constructor() { + super(EdgeAddress); + } +} diff --git a/src/core/trie.test.js b/src/core/trie.test.js new file mode 100644 index 0000000..6d1a05e --- /dev/null +++ b/src/core/trie.test.js @@ -0,0 +1,98 @@ +// @flow + +import {NodeTrie, EdgeTrie} from "./trie"; +import {NodeAddress, EdgeAddress} from "./graph"; + +describe("core/trie", () => { + describe("type safety", () => { + it("NodeTrie and EdgeTrie are distinct", () => { + // $ExpectFlowError + const _unused_trie: NodeTrie = new EdgeTrie(); + }); + it("NodeTrie rejects edge addresses", () => { + // $ExpectFlowError + expect(() => new NodeTrie().add(EdgeAddress.empty, 7)).toThrowError( + "EdgeAddress" + ); + }); + it("EdgeTrie rejects node addresses", () => { + // $ExpectFlowError + expect(() => new EdgeTrie().add(NodeAddress.empty, 7)).toThrowError( + "EdgeAddress" + ); + }); + it("NodeTrie accepts node addresses", () => { + new NodeTrie().add(NodeAddress.empty, 7); + }); + it("EdgeTrie accepts edge addresses", () => { + new EdgeTrie().add(EdgeAddress.empty, 7); + }); + }); + + const empty = NodeAddress.empty; + const foo = NodeAddress.fromParts(["foo"]); + const fooBar = NodeAddress.fromParts(["foo", "bar"]); + const fooBarZod = NodeAddress.fromParts(["foo", "bar", "zod"]); + + it("get returns empty list if nothing added", () => { + const x = new NodeTrie(); + expect(x.get(foo)).toHaveLength(0); + }); + + it("can match the empty address", () => { + const x = new NodeTrie().add(empty, 5); + expect(x.get(empty)).toEqual([5]); + }); + + it("can match non-empty address", () => { + expect(new NodeTrie().add(foo, 3).get(foo)).toEqual([3]); + }); + + it("matches empty address when given non-empty key", () => { + const x = new NodeTrie().add(empty, 5); + expect(x.get(foo)).toEqual([5]); + }); + + it("can match a (non-empty) prefix", () => { + const x = new NodeTrie().add(foo, 3); + expect(x.get(fooBar)).toEqual([3]); + }); + + it("does not match node that contains key", () => { + const x = new NodeTrie().add(fooBarZod, 3); + expect(x.get(fooBar)).toHaveLength(0); + }); + + it("can return a match on empty and non-empty", () => { + const x = new NodeTrie().add(empty, 1).add(foo, 2); + expect(x.get(foo)).toEqual([1, 2]); + }); + + it("swapping the order of addition doesn't change results", () => { + const x = new NodeTrie().add(foo, 2).add(empty, 1); + expect(x.get(foo)).toEqual([1, 2]); + }); + + it("get isn't fazed by intermediary parts missing values", () => { + const x = new NodeTrie() + .add(fooBar, 2) + .add(fooBarZod, 3) + .add(empty, 0); + // note there is no "foo" node + expect(x.get(fooBarZod)).toEqual([0, 2, 3]); + }); + + it("overwriting a value is illegal", () => { + expect(() => + new NodeTrie() + .add(foo, 3) + .add(empty, 1) + .add(foo, 4) + ).toThrowError("overwrite"); + }); + + it("null and undefined are legal values", () => { + const x = new NodeTrie().add(foo, null).add(fooBar, undefined); + expect(x.get(fooBarZod)).toEqual([null, undefined]); + }); +});