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:
parent
383f8d406e
commit
9c781fcfee
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue