Add `identity/contractIdentities` (#1601)

This commit adds a `contractIdentities` method to the Identity plugin,
which allows contracting a WeightedGraph using the provided identities.
The method does not attempt to contract weights together, although as a
safety check it will error if weights have been explicitly provided for
any of the contracted nodes.

This PR replaces #1591; see that pull for some context on why this
method is defined on the identity plugin rather than as part of the
WeightedGraph module.

Test plan: For ease of testing, `contractIdentities` is a thin wrapper
around `nodeContractions` (which is already tested) and a new private
`_contractWeightedGraph` method (for which tests have been added). Since
`contractIdentities` is a trivial oneline composition, it does not need
any additional explicit testing.

`yarn test` passes.
This commit is contained in:
Dandelion Mané 2020-01-28 17:03:43 -08:00 committed by GitHub
parent 50a6a5f6a7
commit 02b072699e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 0 deletions

View File

@ -0,0 +1,67 @@
// @flow
import * as Weights from "../../core/weights";
import {type WeightedGraph as WeightedGraphT} from "../../core/weightedGraph";
import {type NodeContraction, NodeAddress} from "../../core/graph";
import {nodeContractions} from "./nodeContractions";
import {type Identity} from "./identity";
/**
* Applies nodeContractions to a WeightedGraph.
*
* This functionality is defined as part of the identity plugin rather than as
* a feature of WeightedGraph because it doesn't attempt to contract weights
* (it's not clear how to do this in a general principled way). Within the
* identity plugin, we do not expect user identities to have weights
* associated, so we can contract the graph without attempting to contract the
* weights.
*
* As a safety measure, this method will error if any of the node addresses
* being contracted has an explicitly set weight. It will not error if there
* are matching type weights, so that it is still possible (e.g.) to apply
* a weighting to an entire plugin.
*
* For more context on this decision, see discussion in #1591.
*/
export function _contractWeightedGraph(
wg: WeightedGraphT,
contractions: $ReadOnlyArray<NodeContraction>
): WeightedGraphT {
const {graph, weights} = wg;
for (const {old} of contractions) {
for (const address of old) {
const weight = weights.nodeWeights.get(address) || 0;
if (weight !== 0) {
throw new Error(
`Explicit weight ${weight} on contracted node ${NodeAddress.toString(
address
)}`
);
}
}
}
return {
graph: graph.contractNodes(contractions),
weights: Weights.copy(weights),
};
}
/**
* Given a WeightedGraph and identity information, produce a contracted
* WeightedGraph where all of an identitiy's aliases have been contracted into
* a unified identity.
*
* An error will be thrown if any of the aliases had an explicitly set weight,
* since we don't currently support weight contraction.
*
* Note: This function has no unit tests, because it is a trivial composition
* of two tested functions. Thus, flow typechecking in sufficient. If adding
* any complexity to this function, please also add tests.
*/
export function contractIdentities(
wg: WeightedGraphT,
identities: $ReadOnlyArray<Identity>,
discourseUrl: string | null
): WeightedGraphT {
return _contractWeightedGraph(wg, nodeContractions(identities, discourseUrl));
}

View File

@ -0,0 +1,45 @@
// @flow
import {node} from "../../core/graphTestUtil";
import {Graph, NodeAddress} from "../../core/graph";
import * as Weights from "../../core/weights";
import {_contractWeightedGraph} from "./contractIdentities";
describe("plugins/identity/contractIdentities", () => {
const a = node("a");
const b = node("b");
const c = node("c");
const graph = () => new Graph().addNode(a).addNode(b);
const contractions = () => [{old: [a.address, b.address], replacement: c}];
const weights = () => {
const w = Weights.empty();
w.nodeWeights.set(NodeAddress.empty, 3);
return w;
};
describe("_contractWeightedGraph", () => {
it("contracts the graph", () => {
const wg = {graph: graph(), weights: weights()};
const contracted = _contractWeightedGraph(wg, contractions());
const expected = graph().contractNodes(contractions());
expect(expected.equals(contracted.graph)).toBe(true);
});
it("returns a copy of the weights", () => {
const ws = weights();
const wg = {graph: graph(), weights: ws};
const contracted = _contractWeightedGraph(wg, contractions());
expect(contracted.weights).not.toBe(ws);
expect(ws).toEqual(contracted.weights);
// check they can be modified independently
ws.nodeWeights.set(a.address, 5);
expect(ws).not.toEqual(contracted.weights);
});
it("throws an error if a contracted node has an explicit weight", () => {
const ws = weights();
ws.nodeWeights.set(a.address, 5);
const wg = {graph: graph(), weights: ws};
expect(() => _contractWeightedGraph(wg, contractions())).toThrow(
"Explicit weight 5 on contracted node"
);
});
});
});