Weights.merge: add support for resolvers (#1597)

This commit adds support for resolvers to `Weights.merge`. The change is
documented and unit tested. Another step towards #1557.

Test plan: Inspect included tests; `yarn test` passes.
This commit is contained in:
Dandelion Mané 2020-01-30 10:37:05 -08:00 committed by GitHub
parent 566ecdd255
commit 4407c4f9fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 28 deletions

View File

@ -1,7 +1,12 @@
// @flow
import * as MapUtil from "../util/map";
import {type NodeAddressT, type EdgeAddressT} from "../core/graph";
import {
type NodeAddressT,
type EdgeAddressT,
NodeAddress,
EdgeAddress,
} from "../core/graph";
import {toCompat, fromCompat, type Compatible} from "../util/compat";
/**
@ -11,6 +16,8 @@ import {toCompat, fromCompat, type Compatible} from "../util/compat";
*/
export type NodeWeight = number;
export type NodeOperator = (NodeWeight, NodeWeight) => NodeWeight;
/**
* Represents the forwards and backwards weights for a particular Edge (or
* edge address prefix).
@ -19,6 +26,8 @@ export type NodeWeight = number;
*/
export type EdgeWeight = {|+forwards: number, +backwards: number|};
export type EdgeOperator = (EdgeWeight, EdgeWeight) => EdgeWeight;
/**
* Represents the weights for nodes and edges.
*
@ -48,19 +57,68 @@ export function copy(w: Weights): Weights {
};
}
/**
* Merge multiple Weights together.
/** Merge multiple Weights together.
*
* The resultant Weights will have every weight specified by each of the
* input weights. If there are any overlaps (i.e. the same address is present
* in two or more of the input weights), an error will be thrown. In the future,
* we will likely modify this function to add a resolver that determines how to
* combine multiple overlapping weights.
* The resultant Weights will have every weight specified by each of the input
* weights.
*
* When there are overlaps (i.e. the same address is present in two or more of
* the Weights), then the appropriate resolver will be invoked to resolve the
* conflict. The resolver takes two weights and combines them to return a new
* weight.
*
* When no resolvers are explicitly provided, merge defaults to
* conservative "error on conflict" resolvers.
*/
export function merge(ws: $ReadOnlyArray<Weights>): Weights {
const nodeWeights = MapUtil.merge(ws.map((x) => x.nodeWeights));
const edgeWeights = MapUtil.merge(ws.map((x) => x.edgeWeights));
return {nodeWeights, edgeWeights};
export function merge(
ws: $ReadOnlyArray<Weights>,
resolvers: ?{|+nodeResolver: NodeOperator, +edgeResolver: EdgeOperator|}
): Weights {
if (resolvers == null) {
const nodeResolver = (_unused_a, _unused_b) => {
throw new Error(
"node weight conflict detected, but no resolver specified"
);
};
const edgeResolver = (_unused_a, _unused_b) => {
throw new Error(
"edge weight conflict detected, but no resolver specified"
);
};
resolvers = {nodeResolver, edgeResolver};
}
const weights: Weights = empty();
const {nodeWeights, edgeWeights} = weights;
const {nodeResolver, edgeResolver} = resolvers;
for (const w of ws) {
for (const [addr, val] of w.nodeWeights.entries()) {
const existing = nodeWeights.get(addr);
if (existing == null) {
nodeWeights.set(addr, val);
} else {
try {
nodeWeights.set(addr, nodeResolver(existing, val));
} catch (e) {
throw new Error(`${e} when resolving ${NodeAddress.toString(addr)}`);
}
}
}
for (const [addr, val] of w.edgeWeights.entries()) {
const existing = edgeWeights.get(addr);
if (existing == null) {
edgeWeights.set(addr, val);
} else {
try {
edgeWeights.set(addr, edgeResolver(existing, val));
} catch (e) {
throw new Error(
`Error ${e} when resolving ${EdgeAddress.toString(addr)}`
);
}
}
}
}
return weights;
}
export type WeightsJSON = Compatible<{|

View File

@ -116,25 +116,104 @@ describe("core/weights", () => {
const merged = Weights.merge([w1, w2]);
expect(merged).toEqual(w3);
});
it("throws an error on overlapping weights with no conflicts", () => {
const w1 = simpleWeights([["foo", 3]], [["bar", 2, 3]]);
const w2 = simpleWeights([["foo", 3]], [["bar", 2, 3]]);
expect(() => Weights.merge([w1, w2])).toThrowError("duplicate key");
it("uses node resolvers propertly", () => {
const w1 = simpleWeights(
[
["miss", 100],
["hit", 100],
],
[]
);
const w2 = simpleWeights([["hit", 100]], []);
const w3 = simpleWeights([["hit", 100]], []);
const nodeResolver = (a, b) => a + b;
const edgeResolver = (_unused_a, _unused_b) => {
throw new Error("edge");
};
const resolvers = {nodeResolver, edgeResolver};
const merged = Weights.merge([w1, w2, w3], resolvers);
const expected = simpleWeights(
[
["miss", 100],
["hit", 300],
],
[]
);
expect(expected).toEqual(merged);
});
it("errors on conflicting node weights", () => {
const w1 = simpleWeights([["foo", 3]], []);
const w2 = simpleWeights([["foo", 4]], []);
expect(() => Weights.merge([w1, w2])).toThrowError("duplicate key");
it("gives the node address when a node resolver errors", () => {
const w1 = simpleWeights([["hit", 100]], []);
const w2 = simpleWeights([["hit", 100]], []);
expect(() => Weights.merge([w1, w2])).toThrow(
'when resolving NodeAddress["hit"]'
);
});
it("errors on conflicting edge weights (forwards)", () => {
const w1 = simpleWeights([], [["foo", 3, 4]]);
const w2 = simpleWeights([], [["foo", 4, 4]]);
expect(() => Weights.merge([w1, w2])).toThrowError("duplicate key");
it("gives the edge address when a edge resolver errors", () => {
const w1 = simpleWeights([], [["hit", 3, 3]]);
const w2 = simpleWeights([], [["hit", 3, 3]]);
expect(() => Weights.merge([w1, w2])).toThrow(
'when resolving EdgeAddress["hit"]'
);
});
it("errors on conflicting edge weights (backwards)", () => {
const w1 = simpleWeights([], [["foo", 4, 4]]);
const w2 = simpleWeights([], [["foo", 4, 5]]);
expect(() => Weights.merge([w1, w2])).toThrowError("duplicate key");
it("uses edge resolvers propertly", () => {
const w1 = simpleWeights(
[],
[
["hit", 3, 3],
["miss", 3, 3],
]
);
const w2 = simpleWeights([], [["hit", 3, 3]]);
const w3 = simpleWeights([], [["hit", 3, 3]]);
const nodeResolver = (a, b) => a + b;
const edgeResolver = (a, b) => ({
forwards: a.forwards + b.forwards,
backwards: a.backwards * b.backwards,
});
const merged = Weights.merge([w1, w2, w3], {nodeResolver, edgeResolver});
const expected = simpleWeights(
[],
[
["hit", 9, 27],
["miss", 3, 3],
]
);
expect(expected).toEqual(merged);
});
describe("when no resolvers are provided", () => {
it("throws an error on overlapping weights with no conflicts", () => {
const w1 = simpleWeights([["foo", 3]], [["bar", 2, 3]]);
const w2 = simpleWeights([["foo", 3]], [["bar", 2, 3]]);
expect(() => Weights.merge([w1, w2])).toThrowError(
"node weight conflict"
);
});
it("errors on conflicting node weights", () => {
const w1 = simpleWeights([["foo", 3]], []);
const w2 = simpleWeights([["foo", 4]], []);
expect(() => Weights.merge([w1, w2])).toThrowError(
"node weight conflict"
);
});
it("errors on conflicting edge weights (forwards)", () => {
const w1 = simpleWeights([], [["foo", 3, 4]]);
const w2 = simpleWeights([], [["foo", 4, 4]]);
expect(() => Weights.merge([w1, w2])).toThrowError(
"edge weight conflict"
);
});
it("errors on conflicting edge weights (backwards)", () => {
const w1 = simpleWeights([], [["foo", 4, 4]]);
const w2 = simpleWeights([], [["foo", 4, 5]]);
expect(() => Weights.merge([w1, w2])).toThrowError(
"edge weight conflict"
);
});
});
});
});