Use `Map`s in `WeightConfig` (#497)

Summary:
Now that `MapUtil` provides `toObject`/`fromObject`, we can keep storing
weights in localStorage while representing them as ES6 `Map`s in memory.

Here are some advantages:
  - The code is genuinely more typesafe. While writing this,
    I accidentally wrote `edgeWeights.get(key)`, where `edgeWeights`
    should have been `nodeWeights`. This was caught at compile time, and
    would not have been in the previous version.
  - Relatedly, the code now has zero `any`-casts as opposed to five.
  - The initialization of the default values is not abysmally ugly.
  - Whenever we iterate over these maps, (a) we can use `.entries()`,
    and (b) we don’t have to cast between string keys and semantic keys.
    This simplifies some of the control flow.
  - The extra null-checking on `get` forces us to either think about
    ways in which the check might fail, or reuse a previously fetched
    value that is known to be non-null (perhaps because it came from
    `entries`).
  - A particularly annoying Prettier line-break is avoided. :-)

Here are some disadvantages:
  - The null-pipelining around the `rehydrate` function is a bit
    annoying. As @decentralion pointed out, what we want here is not a
    default value, but a default value and a function to transform a
    present value. This is Haskell’s `maybe : β → (α → β) → Maybe α → β`
    or Java’s `optional.map(fromObject).orElse(defaultValue)`. This
    commit implements one approach; another is to note that `fromObject`
    is invertible, writing `fromObject(LocalStore.get(k, toObject(d)))`.
  - That’s it, I think?

Test Plan:
I’ve tested that the sliders for both edge and node weights correctly
influence the PageRank behavior, that the component is properly
initialized with an empty localStorage, and that the component properly
rehydrates from localStorage.

wchargin-branch: weightconfig-maps
This commit is contained in:
William Chargin 2018-07-06 22:23:23 -07:00 committed by GitHub
parent 9a1fee285c
commit 09958e1ee2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 110 additions and 99 deletions

View File

@ -12,6 +12,7 @@ import {
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
import {byEdgeType, byNodeType} from "./edgeWeights";
import LocalStore from "./LocalStore";
import * as MapUtil from "../../util/map";
// Hacks...
import * as GithubNode from "../../plugins/github/nodes";
@ -24,40 +25,36 @@ type Props = {
};
// The key should be an EdgeAddressT, but a Flow bug prevents this.
type EdgeWeights = {[string]: UserEdgeWeight};
type EdgeWeights = Map<EdgeAddressT, UserEdgeWeight>;
type UserEdgeWeight = {|+logWeight: number, +directionality: number|};
const EDGE_WEIGHTS_KEY = "edgeWeights";
const defaultEdgeWeights = (): EdgeWeights => ({
[(GithubEdge._Prefix.authors: string)]: {logWeight: 0, directionality: 0.5},
[(GithubEdge._Prefix.mergedAs: string)]: {logWeight: 0, directionality: 0.5},
[(GithubEdge._Prefix.references: string)]: {
logWeight: 0,
directionality: 0.5,
},
[(GithubEdge._Prefix.hasParent: string)]: {logWeight: 0, directionality: 0.5},
[(GitEdge._Prefix.hasTree: string)]: {logWeight: 0, directionality: 0.5},
[(GitEdge._Prefix.hasParent: string)]: {logWeight: 0, directionality: 0.5},
[(GitEdge._Prefix.includes: string)]: {logWeight: 0, directionality: 0.5},
[(GitEdge._Prefix.becomes: string)]: {logWeight: 0, directionality: 0.5},
[(GitEdge._Prefix.hasContents: string)]: {logWeight: 0, directionality: 0.5},
});
const defaultEdgeWeights = (): EdgeWeights =>
new Map()
.set(GithubEdge._Prefix.authors, {logWeight: 0, directionality: 0.5})
.set(GithubEdge._Prefix.mergedAs, {logWeight: 0, directionality: 0.5})
.set(GithubEdge._Prefix.references, {logWeight: 0, directionality: 0.5})
.set(GithubEdge._Prefix.hasParent, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.hasTree, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.hasParent, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.includes, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.becomes, {logWeight: 0, directionality: 0.5})
.set(GitEdge._Prefix.hasContents, {logWeight: 0, directionality: 0.5});
// The key should be a NodeAddressT, but a Flow bug prevents this.
type NodeWeights = {[string]: UserNodeWeight};
type NodeWeights = Map<NodeAddressT, UserNodeWeight>;
type UserNodeWeight = number /* in log space */;
const NODE_WEIGHTS_KEY = "nodeWeights";
const defaultNodeWeights = (): NodeWeights => ({
[(GithubNode._Prefix.repo: string)]: 0,
[(GithubNode._Prefix.issue: string)]: 0,
[(GithubNode._Prefix.pull: string)]: 0,
[(GithubNode._Prefix.review: string)]: 0,
[(GithubNode._Prefix.comment: string)]: 0,
[(GithubNode._Prefix.userlike: string)]: 0,
[(GitNode._Prefix.blob: string)]: 0,
[(GitNode._Prefix.commit: string)]: 0,
[(GitNode._Prefix.tree: string)]: 0,
[(GitNode._Prefix.treeEntry: string)]: 0,
});
const defaultNodeWeights = (): NodeWeights =>
new Map()
.set(GithubNode._Prefix.repo, 0)
.set(GithubNode._Prefix.issue, 0)
.set(GithubNode._Prefix.pull, 0)
.set(GithubNode._Prefix.review, 0)
.set(GithubNode._Prefix.comment, 0)
.set(GithubNode._Prefix.userlike, 0)
.set(GitNode._Prefix.blob, 0)
.set(GitNode._Prefix.commit, 0)
.set(GitNode._Prefix.tree, 0)
.set(GitNode._Prefix.treeEntry, 0);
type State = {
edgeWeights: EdgeWeights,
@ -75,10 +72,24 @@ export class WeightConfig extends React.Component<Props, State> {
componentDidMount() {
this.setState(
(state) => ({
edgeWeights: LocalStore.get(EDGE_WEIGHTS_KEY, state.edgeWeights),
nodeWeights: LocalStore.get(NODE_WEIGHTS_KEY, state.nodeWeights),
}),
(state) => {
function rehydrate<K: string, V>(
x: ?{[K]: V},
default_: Map<K, V>
): Map<K, V> {
return x ? MapUtil.fromObject(x) : default_;
}
return {
edgeWeights: rehydrate(
LocalStore.get(EDGE_WEIGHTS_KEY),
state.edgeWeights
),
nodeWeights: rehydrate(
LocalStore.get(NODE_WEIGHTS_KEY),
state.nodeWeights
),
};
},
() => this.fire()
);
}
@ -106,17 +117,21 @@ export class WeightConfig extends React.Component<Props, State> {
fire() {
const {edgeWeights, nodeWeights} = this.state;
LocalStore.set(EDGE_WEIGHTS_KEY, edgeWeights);
LocalStore.set(NODE_WEIGHTS_KEY, nodeWeights);
const edgePrefixes = Object.keys(edgeWeights).map((key) => {
const {logWeight, directionality} = edgeWeights[key];
const prefix: EdgeAddressT = (key: any);
return {prefix, weight: 2 ** logWeight, directionality};
});
const nodePrefixes = Object.keys(nodeWeights).map((key) => ({
prefix: ((key: any): NodeAddressT),
weight: 2 ** nodeWeights[key],
}));
LocalStore.set(EDGE_WEIGHTS_KEY, MapUtil.toObject(edgeWeights));
LocalStore.set(NODE_WEIGHTS_KEY, MapUtil.toObject(nodeWeights));
const edgePrefixes = Array.from(edgeWeights.entries()).map(
([prefix, {logWeight, directionality}]) => ({
prefix,
weight: 2 ** logWeight,
directionality,
})
);
const nodePrefixes = Array.from(nodeWeights.entries()).map(
([prefix, logWeight]) => ({
prefix,
weight: 2 ** logWeight,
})
);
const edgeEvaluator = byNodeType(byEdgeType(edgePrefixes), nodePrefixes);
this.props.onChange(edgeEvaluator);
}
@ -127,57 +142,51 @@ class EdgeConfig extends React.Component<{
onChange: (EdgeWeights) => void,
}> {
weightControls() {
return Object.keys(this.props.edgeWeights).map((key) => {
const {logWeight} = this.props.edgeWeights[key];
return (
<label style={{display: "block"}} key={key}>
<input
type="range"
min={-10}
max={10}
step={0.1}
value={logWeight}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const edgeWeights = {
...this.props.edgeWeights,
[key]: {...this.props.edgeWeights[key], logWeight: value},
};
this.props.onChange(edgeWeights);
}}
/>{" "}
{formatNumber(logWeight)}{" "}
{JSON.stringify(EdgeAddress.toParts((key: any)))}
</label>
);
});
return Array.from(this.props.edgeWeights.entries()).map(([key, datum]) => (
<label style={{display: "block"}} key={key}>
<input
type="range"
min={-10}
max={10}
step={0.1}
value={datum.logWeight}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const edgeWeights = MapUtil.copy(this.props.edgeWeights).set(key, {
...datum,
logWeight: value,
});
this.props.onChange(edgeWeights);
}}
/>{" "}
{formatNumber(datum.logWeight)}{" "}
{JSON.stringify(EdgeAddress.toParts(key))}
</label>
));
}
directionControls() {
return Object.keys(this.props.edgeWeights).map((key) => {
const {directionality} = this.props.edgeWeights[key];
return (
<label style={{display: "block"}} key={key}>
<input
type="range"
min={0}
max={1}
step={0.01}
value={directionality}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const edgeWeights = {
...this.props.edgeWeights,
[key]: {...this.props.edgeWeights[key], directionality: value},
};
this.props.onChange(edgeWeights);
}}
/>{" "}
{directionality.toFixed(2)}{" "}
{JSON.stringify(EdgeAddress.toParts((key: any)))}
</label>
);
});
return Array.from(this.props.edgeWeights.entries()).map(([key, datum]) => (
<label style={{display: "block"}} key={key}>
<input
type="range"
min={0}
max={1}
step={0.01}
value={datum.directionality}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const edgeWeights = MapUtil.copy(this.props.edgeWeights).set(key, {
...datum,
directionality: value,
});
this.props.onChange(edgeWeights);
}}
/>{" "}
{datum.directionality.toFixed(2)}{" "}
{JSON.stringify(EdgeAddress.toParts(key))}
</label>
));
}
render() {
return (
@ -196,9 +205,8 @@ class NodeConfig extends React.Component<{
onChange: (NodeWeights) => void,
}> {
render() {
const controls = Object.keys(this.props.nodeWeights).map((key) => {
const currentValue = this.props.nodeWeights[key];
return (
const controls = Array.from(this.props.nodeWeights.entries()).map(
([key, currentValue]) => (
<label style={{display: "block"}} key={key}>
<input
type="range"
@ -208,15 +216,18 @@ class NodeConfig extends React.Component<{
value={currentValue}
onChange={(e) => {
const value: number = e.target.valueAsNumber;
const nodeWeights = {...this.props.nodeWeights, [key]: value};
const nodeWeights = MapUtil.copy(this.props.nodeWeights).set(
key,
value
);
this.props.onChange(nodeWeights);
}}
/>{" "}
{formatNumber(currentValue)}{" "}
{JSON.stringify(NodeAddress.toParts((key: any)))}
{JSON.stringify(NodeAddress.toParts(key))}
</label>
);
});
)
);
return (
<div>
<h2>Node weights (in log space)</h2>