mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-05 01:04:53 +00:00
Add utilities for working with ES6 Maps (#495)
Summary: We’d like to like ES6 `Map`s, because they provide better type safety than objects (primarily, `Map.prototype.get` has nullable result type). However, the vanilla APIs are weak. Prominent problems are that `Map`s always become `"{}"` under `JSON.stringify`, that there is no easy way to convert between `Map`s and objects, and that there are no functions to map over the keys and values of `Map`s. In this commit, we add versions of those functions to a utility module. The value-level implementations are straightforward, but these functions nevertheless deserve a utility module because the types are somewhat tricky to get right. The implementation requires casts through `any`, and these should be written, analyzed, and proven correct just once. (In particular, it would be easy to write an unsound type for `fromObject`.) In a followup commit, we will amend existing portions of the codebase to use these functions. Test Plan: Unit tests added; run `yarn travis`. wchargin-branch: map-util
This commit is contained in:
parent
daf2f9a376
commit
812b2d322e
124
src/util/map.js
Normal file
124
src/util/map.js
Normal file
@ -0,0 +1,124 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Convert a string-keyed map to an object. Useful for conversion to
|
||||
* JSON. If a map's keys are not strings, consider invoking `mapKeys`
|
||||
* first.
|
||||
*/
|
||||
export function toObject<K: string, V, InK: K, InV: V>(
|
||||
map: Map<InK, InV>
|
||||
): {[K]: V} {
|
||||
const result = {};
|
||||
for (const [k, v] of map.entries()) {
|
||||
result[k] = v;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object to a map. The resulting map will have key-value
|
||||
* pairs corresponding to the enumerable own properties of the object in
|
||||
* iteration order, as returned by `Object.keys`.
|
||||
*/
|
||||
export function fromObject<K, V, InK: K & string, InV: V>(object: {
|
||||
[InK]: InV,
|
||||
}): Map<K, V> {
|
||||
const result = new Map();
|
||||
const keys = (((Object.keys(object): string[]): any): InK[]);
|
||||
for (const key of keys) {
|
||||
result.set(key, object[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow-copy a map, allowing upcasting its type parameters.
|
||||
*
|
||||
* The `Map` type constructor is not covariant in its type parameters,
|
||||
* which means that (e.g.) `Map<string, Dog>` is not a subtype of
|
||||
* `Map<string, Animal>` even if `Dog` is a subtype of `Animal`. This is
|
||||
* because, given a `Map<string, Animal>`, one can insert a `Cat`, which
|
||||
* would break invariants of existing references to the variable as a
|
||||
* map containing only `Dog`s.
|
||||
*
|
||||
* declare class Animal {};
|
||||
* declare class Dog extends Animal {};
|
||||
* declare class Cat extends Animal {};
|
||||
* declare var dogMap: Map<string, Dog>;
|
||||
* const animalMap: Map<string, Animal> = dogMap; // must fail
|
||||
* animalMap.set("tabby", new Cat()); // or we could do this...
|
||||
* (dogMap.values(): Iterator<Dog>); // ...now contains a `Cat`!
|
||||
*
|
||||
* This problem only exists when a map with existing references is
|
||||
* mutated. Therefore, when we shallow-copy a map, we have the
|
||||
* opportunity to upcast its type parameters: `copy(dogMap)` _can_ be a
|
||||
* `Map<string, Animal>`.
|
||||
*/
|
||||
export function copy<K, V, InK: K, InV: V>(map: Map<InK, InV>): Map<K, V> {
|
||||
const entries = map.entries();
|
||||
return new Map((((entries: Iterator<[InK, InV]>): any): Iterator<[K, V]>));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map across the keys of a map. Note that the key-mapping function is
|
||||
* provided both the key and the value for each entry.
|
||||
*
|
||||
* The key-mapping function must be injective on the map's key set. If
|
||||
* it maps two distinct input keys to the same output key, an error may
|
||||
* be thrown.
|
||||
*/
|
||||
export function mapKeys<K, V, InK, InV: V>(
|
||||
map: Map<InK, InV>,
|
||||
f: (InK, InV) => K
|
||||
): Map<K, V> {
|
||||
const result = new Map();
|
||||
for (const [k, v] of map.entries()) {
|
||||
const outK = f(k, v);
|
||||
if (result.has(outK)) {
|
||||
throw new Error("duplicate key: " + String(outK));
|
||||
}
|
||||
result.set(outK, v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map across the values of a map. Note that the value-mapping function
|
||||
* is provided both the key and the value for each entry.
|
||||
*
|
||||
* There are no restrictions on the value-mapping function (in
|
||||
* particular, it need not be injective).
|
||||
*/
|
||||
export function mapValues<K, V, InK: K, InV>(
|
||||
map: Map<InK, InV>,
|
||||
g: (InK, InV) => V
|
||||
): Map<K, V> {
|
||||
const result = new Map();
|
||||
for (const [k, v] of map.entries()) {
|
||||
result.set(k, g(k, v));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map simultaneously across the keys and values of a map.
|
||||
*
|
||||
* The key-mapping function must be injective on the map's key set. If
|
||||
* it maps two distinct input keys to the same output key, an error may
|
||||
* be thrown. There are no such restrictions on the value-mapping
|
||||
* function.
|
||||
*/
|
||||
export function mapEntries<K, V, InK, InV>(
|
||||
map: Map<InK, InV>,
|
||||
h: (InK, InV) => [K, V]
|
||||
): Map<K, V> {
|
||||
const result = new Map();
|
||||
for (const [k, v] of map.entries()) {
|
||||
const [outK, outV] = h(k, v);
|
||||
if (result.has(outK)) {
|
||||
throw new Error("duplicate key: " + String(outK));
|
||||
}
|
||||
result.set(outK, outV);
|
||||
}
|
||||
return result;
|
||||
}
|
248
src/util/map.test.js
Normal file
248
src/util/map.test.js
Normal file
@ -0,0 +1,248 @@
|
||||
// @flow
|
||||
|
||||
import * as MapUtil from "./map";
|
||||
|
||||
describe("util/map", () => {
|
||||
describe("toObject", () => {
|
||||
it("works on a map with string keys", () => {
|
||||
const input: Map<string, number> = new Map().set("one", 1).set("two", 2);
|
||||
const output: {[string]: number} = MapUtil.toObject(input);
|
||||
expect(output).toEqual({one: 1, two: 2});
|
||||
});
|
||||
it("works on a map with keys a subtype of string", () => {
|
||||
type Fruit = "APPLE" | "ORANGE";
|
||||
const input: Map<Fruit, string> = new Map()
|
||||
.set("APPLE", "good")
|
||||
.set("ORANGE", "also good");
|
||||
const output: {[Fruit]: string} = MapUtil.toObject(input);
|
||||
expect(output).toEqual({APPLE: "good", ORANGE: "also good"});
|
||||
});
|
||||
it("statically rejects a map with keys not a subtype of string", () => {
|
||||
const input: Map<number, string> = new Map()
|
||||
.set(12, "not okay")
|
||||
.set(13, "also not okay");
|
||||
// $ExpectFlowError
|
||||
MapUtil.toObject(input);
|
||||
});
|
||||
it("statically refuses to output an object with non-string keys", () => {
|
||||
const input: Map<string, number> = new Map().set("one", 1).set("two", 2);
|
||||
// $ExpectFlowError
|
||||
const _: {[string | number]: number} = MapUtil.toObject(input);
|
||||
});
|
||||
it("allows upcasting the key and value types of the result object", () => {
|
||||
type Fruit = "APPLE" | "ORANGE";
|
||||
type Froot = "APPLE" | "ORANGE" | "BRICK";
|
||||
const input: Map<Fruit, string> = new Map()
|
||||
.set("APPLE", "good")
|
||||
.set("ORANGE", "also good");
|
||||
const _: {[Froot]: string | void} = MapUtil.toObject(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromObject", () => {
|
||||
it("works on an object with string keys", () => {
|
||||
const input: {[string]: number} = {one: 1, two: 2};
|
||||
const output: Map<string, number> = MapUtil.fromObject(input);
|
||||
expect(output).toEqual(new Map().set("one", 1).set("two", 2));
|
||||
});
|
||||
it("works on an object with keys a subtype of string", () => {
|
||||
type Fruit = "APPLE" | "ORANGE";
|
||||
const input: {[Fruit]: string} = {APPLE: "good", ORANGE: "also good"};
|
||||
const output: Map<Fruit, string> = MapUtil.fromObject(input);
|
||||
expect(output).toEqual(
|
||||
new Map().set("APPLE", "good").set("ORANGE", "also good")
|
||||
);
|
||||
});
|
||||
it("statically rejects a map with keys not a subtype of string", () => {
|
||||
const input: {[number]: string} = {};
|
||||
input[12] = "not okay";
|
||||
input[13] = "also not okay";
|
||||
// $ExpectFlowError
|
||||
MapUtil.fromObject(input);
|
||||
// If that were valid, then `(result.keys(): Iterator<number>)`
|
||||
// would contain the strings "12" and "13".
|
||||
});
|
||||
it("allows upcasting the key and value types of the result map", () => {
|
||||
type Fruit = "APPLE" | "ORANGE";
|
||||
type Froot = "APPLE" | "ORANGE" | "BRICK";
|
||||
const input: {[Fruit]: string} = {APPLE: "good", ORANGE: "also good"};
|
||||
const output: Map<Froot, string | void> = MapUtil.fromObject(input);
|
||||
expect(output).toEqual(
|
||||
new Map().set("APPLE", "good").set("ORANGE", "also good")
|
||||
);
|
||||
});
|
||||
it("allows upcasting the result map's key type to non-strings", () => {
|
||||
// Contrast with `toObject`, where this is not and must not be
|
||||
// permitted.
|
||||
type Fruit = "APPLE" | "ORANGE";
|
||||
type Froot = "APPLE" | "ORANGE" | "BRICK" | number;
|
||||
const input: {[Fruit]: string} = {APPLE: "good", ORANGE: "also good"};
|
||||
const output: Map<Froot, string> = MapUtil.fromObject(input);
|
||||
expect(output).toEqual(
|
||||
new Map().set("APPLE", "good").set("ORANGE", "also good")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("copy", () => {
|
||||
it("returns a reference-distinct but value-equal map", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.copy(input);
|
||||
expect(input).not.toBe(output);
|
||||
expect(input).toEqual(output);
|
||||
});
|
||||
it("returns a map that is not linked to the original", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.copy(input);
|
||||
input.set(3, "three");
|
||||
output.set(4, "four");
|
||||
expect(input).toEqual(
|
||||
new Map()
|
||||
.set(1, "one")
|
||||
.set(2, "two")
|
||||
.set(3, "three")
|
||||
);
|
||||
expect(output).toEqual(
|
||||
new Map()
|
||||
.set(1, "one")
|
||||
.set(2, "two")
|
||||
.set(4, "four")
|
||||
);
|
||||
});
|
||||
it("allows upcasting the key and value types of the result map", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const _: Map<number | boolean, string | void> = MapUtil.copy(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapKeys", () => {
|
||||
it("works in a simple case", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.mapKeys(input, (n) => n + 10);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(11, "one").set(12, "two"));
|
||||
});
|
||||
it("provides the corresponding value", () => {
|
||||
const input: Map<number, string> = new Map()
|
||||
.set(1, "one")
|
||||
.set(2, "twooo");
|
||||
const output: Map<number, string> = MapUtil.mapKeys(
|
||||
input,
|
||||
(n, s) => n + s.length
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "twooo"));
|
||||
expect(output).toEqual(new Map().set(4, "one").set(7, "twooo"));
|
||||
});
|
||||
it("allows mapping to a different key type", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<string, string> = MapUtil.mapKeys(
|
||||
input,
|
||||
(n) => n + "!"
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set("1!", "one").set("2!", "two"));
|
||||
});
|
||||
it("allows upcasting the value type", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string | void> = MapUtil.mapKeys(
|
||||
input,
|
||||
(n) => n + 10
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(11, "one").set(12, "two"));
|
||||
});
|
||||
it("throws on a non-injective key-mapping function", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
expect(() => MapUtil.mapKeys(input, (_) => 0)).toThrow(
|
||||
"duplicate key: 0"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapValues", () => {
|
||||
it("works in a simple case", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.mapValues(
|
||||
input,
|
||||
(_, s) => s + "!"
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(1, "one!").set(2, "two!"));
|
||||
});
|
||||
it("provides the corresponding key", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.mapValues(
|
||||
input,
|
||||
(n, s) => s + "!".repeat(n)
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(1, "one!").set(2, "two!!"));
|
||||
});
|
||||
it("allows mapping to a different value type", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string[]> = MapUtil.mapValues(input, (_, s) =>
|
||||
s.split("")
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(
|
||||
new Map().set(1, ["o", "n", "e"]).set(2, ["t", "w", "o"])
|
||||
);
|
||||
});
|
||||
it("allows upcasting the key type", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number | void, string> = MapUtil.mapValues(
|
||||
input,
|
||||
(_, n) => n + "!"
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(1, "one!").set(2, "two!"));
|
||||
});
|
||||
it("permits a non-injective value-mapping function", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.mapValues(
|
||||
input,
|
||||
(_unused_key, _unused_value) => "wat"
|
||||
);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(1, "wat").set(2, "wat"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapEntries", () => {
|
||||
it("works in a simple case", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.mapEntries(input, (n, s) => [
|
||||
-n,
|
||||
"negative " + s,
|
||||
]);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(
|
||||
new Map().set(-1, "negative one").set(-2, "negative two")
|
||||
);
|
||||
});
|
||||
it("allows mapping to different key and value types", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<string, number> = MapUtil.mapEntries(input, (k, v) => [
|
||||
v,
|
||||
k,
|
||||
]);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set("one", 1).set("two", 2));
|
||||
});
|
||||
it("throws on a non-injective key-mapping function", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
expect(() => MapUtil.mapEntries(input, (_, v) => ["wat", v])).toThrow(
|
||||
"duplicate key: wat"
|
||||
);
|
||||
});
|
||||
it("permits a non-injective value-mapping function", () => {
|
||||
const input: Map<number, string> = new Map().set(1, "one").set(2, "two");
|
||||
const output: Map<number, string> = MapUtil.mapEntries(input, (k, _) => [
|
||||
k + 10,
|
||||
"wat",
|
||||
]);
|
||||
expect(input).toEqual(new Map().set(1, "one").set(2, "two"));
|
||||
expect(output).toEqual(new Map().set(11, "wat").set(12, "wat"));
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user