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:
parent
566ecdd255
commit
4407c4f9fc
|
@ -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<{|
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue