Add foundations to unify address implementations (#355)

Summary:
We have `NodeAddress` and `EdgeAddress`, which are opaque aliases of
`string` each with separate associated functions. We really want to keep
this setup: having the address types be structurally distinct is very
nice. But currently the implementation is extremely repetitive. Core
functionality is implemented twice, once for nodes and once for edges.
The test code is even worse, as it is forced to include ugly, hacky,
parametric generalizations to test both implementations—which are really
the same code, anyway!

In this commit, we introduce a system to unify the _implementations_
while keeping the _APIs_ separate. That is, users still see separate
opaque types `NodeAddressT` and `EdgeAddressT`. Users now also see
separate modules `NodeAddress` and `EdgeAddress`, each of which
implements the same interface for its appropriate type. These modules
are each implemented by the same address module factory.

To get this to work, we clearly need to parameterize the module type
over the address type. The problem is getting this to work in a way that
interacts nicely with the opaque types. The trick is to let the factory
always return a transparent module at type `string`, but to then
specialize the type of the resulting module in the module in which the
underlying type of the opaque type is known.

This commit includes specifications for all functions that are in the
current version of the API, but includes only as much implementation
code as is needed to convince me that tests and Flow are actually
running (i.e., very little). I’ll send implementations in separate PRs
for easier review.

The preliminary modules created in this commit _are_ exported from the
graph module, even though they are incomplete. This is so that we can be
sure that nothing will catch fire in Flow-land when we try to export
them (which is plausible, given that we have nontrivial interactions
between opaque types and parametric polymorphism).

Test Plan:
Unit tests included. Run `yarn travis`.

wchargin-branch: address-unified-foundations
This commit is contained in:
William Chargin 2018-06-07 09:06:28 -07:00 committed by GitHub
parent 08cb60e762
commit fc7e8886b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 215 additions and 2 deletions

142
src/v3/core/address.js Normal file
View File

@ -0,0 +1,142 @@
// @flow
export interface AddressModule<Address> {
/**
* Assert at runtime that the provided address is actually a valid
* address of this kind, throwing an error if it is not. If `what` is
* provided, it will be included in the error message.
*/
assertValid(address: Address, what?: string): void;
/**
* Assert at runtime that the provided array is a valid array of
* address parts (i.e., a valid input to `fromParts`), throwing an
* error if it is not. If `what` is provided, it will be included in
* the error message.
*/
assertValidParts(parts: $ReadOnlyArray<string>, what?: string): void;
/**
* Convert an array of address parts to an address. The input must be
* a non-null array of non-null strings, none of which contains the
* NUL character. This is the inverse of `toParts`.
*/
fromParts(parts: $ReadOnlyArray<string>): Address;
/**
* Convert an address to the array of parts that it represents. This
* is the inverse of `fromParts`.
*/
toParts(address: Address): string[];
/**
* Pretty-print an address. The result will be human-readable and
* contain only printable characters. Clients should not make any
* assumptions about the format.
*/
toString(address: Address): string;
/**
* Construct an address by extending the given address with the given
* additional components. This function is equivalent to:
*
* return fromParts([...toParts(address), ...components]);
*
* but may be more efficient.
*/
append(address: Address, ...components: string[]): Address;
/**
* Test whether the given address has the given prefix. This function
* is equivalent to:
*
* const prefixParts = toParts(prefix);
* const addressParts = toParts(address);
* const actualPrefix = addressParts.slice(0, prefixParts.length);
* return deepEqual(prefix, actualPrefix);
*
* (where `deepEqual` checks value equality on arrays of strings), but
* may be more efficient.
*
* Note that this is an array-wise prefix, not a string-wise-prefix:
* e.g., `toParts(["ban"])` is not a prefix of `toParts(["banana"])`.
*/
hasPrefix(address: Address, prefix: Address): boolean;
}
export type Options = {|
/**
* The name of this kind of address, like `NodeAddress`.
*/
+name: string,
/**
* A unique nonce for the runtime representation of this address. For
* compact serialization, this should be short; a single letter
* suffices.
*/
+nonce: string,
/**
* For the purposes of nice error messages: in response to an address
* of the wrong kind, we can inform the user what kind of address they
* passed (e.g., "expected NodeAddress, got EdgeAddress"). This
* dictionary maps another address module's nonce to the name of that
* module.
*/
+otherNonces?: Map<string, string>,
|};
export function makeAddressModule(options: Options): AddressModule<string> {
type Address = string; // for readability and interface consistency
const _ = options;
function assertValid(address: Address, what?: string): void {
const _ = {address, what};
throw new Error("assertValid");
}
function assertValidParts(
parts: $ReadOnlyArray<string>,
what?: string
): void {
const _ = {parts, what};
throw new Error("assertValidParts");
}
function fromParts(parts: $ReadOnlyArray<string>): Address {
const _ = parts;
throw new Error("fromParts");
}
function toParts(address: Address): string[] {
const _ = address;
throw new Error("toParts");
}
function toString(address: Address): string {
const _ = address;
throw new Error("toString");
}
function append(address: Address, ...parts: string[]): Address {
const _ = {address, parts};
throw new Error("append");
}
function hasPrefix(address: Address, prefix: Address): boolean {
const _ = {address, prefix};
throw new Error("hasPrefix");
}
const result = {
assertValid,
assertValidParts,
fromParts,
toParts,
toString,
append,
hasPrefix,
};
return Object.freeze(result);
}

View File

@ -0,0 +1,39 @@
// @flow
import {makeAddressModule} from "./address";
describe("core/address", () => {
describe("makeAddressModule", () => {
const makeModules = () => ({
FooAddress: makeAddressModule({
name: "FooAddress",
nonce: "F",
otherNonces: new Map().set("B", "BarAddress"),
}),
BarAddress: makeAddressModule({
name: "BarAddress",
nonce: "B",
otherNonces: new Map().set("F", "FooAddress"),
}),
WatAddress: makeAddressModule({
name: "WatAddress",
nonce: "W",
otherNonces: new Map(),
}),
});
it("makes an address module given the mandatory options", () => {
makeAddressModule({name: "FooAddress", nonce: "F"});
});
it("makes address modules using all the options", () => {
makeModules();
});
it("returns an object with read-only properties", () => {
const {FooAddress} = makeModules();
expect(() => {
// $ExpectFlowError
FooAddress.assertValid = FooAddress.assertValid;
}).toThrow(/read.only property/);
});
});
});

View File

@ -2,8 +2,25 @@
import type {NodeAddress, EdgeAddress} from "./_address";
import * as Address from "./_address";
import {makeAddressModule, type AddressModule} from "./address";
export type {NodeAddress, EdgeAddress} from "./_address";
// New-style node and edge address types and modules. Will be made
// public once implementation is complete.
export opaque type _NodeAddressT: string = string;
export opaque type _EdgeAddressT: string = string;
export const _NodeAddress: AddressModule<_NodeAddressT> = (makeAddressModule({
name: "NodeAddress",
nonce: "N",
otherNonces: new Map().set("E", "EdgeAddress"),
}): AddressModule<string>);
export const _EdgeAddress: AddressModule<_EdgeAddressT> = (makeAddressModule({
name: "EdgeAddress",
nonce: "E",
otherNonces: new Map().set("N", "NodeAddress"),
}): AddressModule<string>);
Object.freeze(Address);
export {Address};

View File

@ -1,7 +1,15 @@
// @flow
import {Address, Direction, Graph, edgeToString} from "./graph";
import type {NodeAddress, EdgeAddress} from "./graph";
import {
type EdgeAddress,
type NodeAddress,
type _EdgeAddressT,
type _NodeAddressT,
Address,
Direction,
Graph,
edgeToString,
} from "./graph";
describe("core/graph", () => {
const {nodeAddress, edgeAddress} = Address;
@ -30,6 +38,13 @@ describe("core/graph", () => {
});
});
function _unused_itExportsDistinctNodeAddressAndEdgeAddressTypes() {
// $ExpectFlowError
const _unused_nodeToEdge = (x: _NodeAddressT): _EdgeAddressT => x;
// $ExpectFlowError
const _unused_edgeToNode = (x: _EdgeAddressT): _NodeAddressT => x;
}
describe("Direction values", () => {
it("are read-only", () => {
expect(() => {