From 4407c4f9fce86d3fb9f2fe7810caf8f81969cbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dandelion=20Man=C3=A9?= Date: Thu, 30 Jan 2020 10:37:05 -0800 Subject: [PATCH] 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. --- src/core/weights.js | 82 ++++++++++++++++++++++++----- src/core/weights.test.js | 111 +++++++++++++++++++++++++++++++++------ 2 files changed, 165 insertions(+), 28 deletions(-) diff --git a/src/core/weights.js b/src/core/weights.js index 2b39751..aaae9e7 100644 --- a/src/core/weights.js +++ b/src/core/weights.js @@ -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 { - 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, + 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<{| diff --git a/src/core/weights.test.js b/src/core/weights.test.js index 24a7ced..1cfaea5 100644 --- a/src/core/weights.test.js +++ b/src/core/weights.test.js @@ -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" + ); + }); }); }); });