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
This commit is contained in:
Dandelion Mané 2018-08-06 19:56:25 -07:00 committed by GitHub
parent 383f8d406e
commit 9c781fcfee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 197 additions and 0 deletions

99
src/core/trie.js Normal file
View File

@ -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<V> = {|+map: RecursiveMap<V>, value: V | typeof EMPTY_ENTRY_SYMBOL|};
type RecursiveMap<V> = Map<string, Entry<V>>;
class BaseTrie<K, V> {
addressModule: AddressModule<K>;
entry: Entry<V>;
/**
* Create an empty trie backed by the given address module.
*/
constructor(m: AddressModule<K>) {
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<V> = 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<V> extends BaseTrie<NodeAddressT, V> {
constructor() {
super(NodeAddress);
}
}
export class EdgeTrie<V> extends BaseTrie<EdgeAddressT, V> {
constructor() {
super(EdgeAddress);
}
}

98
src/core/trie.test.js Normal file
View File

@ -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<number> = 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]);
});
});