mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-17 23:16:23 +00:00
combo: add pure
and fmap
combinators (#1817)
Summary: These are commonly used for building large parsers from small pieces, especially `fmap`, which is instrumental for transformation and validation. We pick the name `fmap` rather than `map` to avoid confusion with ES6 `Map` values, which are unrelated. Test Plan: Unit tests and type-checking tests included, with full coverage. wchargin-branch: combo-pure-fmap
This commit is contained in:
parent
297c4e9156
commit
c5bdfcd174
@ -85,6 +85,60 @@ export const null_: Parser<null> = new Parser((x) => {
|
|||||||
return success(x);
|
return success(x);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lift a plain value into a parser that always returns that value,
|
||||||
|
// ignoring its input.
|
||||||
|
export function pure<T>(t: T): Parser<T> {
|
||||||
|
return new Parser((_) => success(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the output of a parser with a pure function. For instance,
|
||||||
|
// if `p: Parser<number>` and `f = (n: number) => n % 2 === 0`, then
|
||||||
|
// `fmap(p, f)` is a `Parser<boolean>` that first uses `p` to parse its
|
||||||
|
// input to a number and then checks whether the number is even.
|
||||||
|
//
|
||||||
|
// If the function `f` throws, the thrown value will be converted to
|
||||||
|
// string and returned as a parse error. (The string conversion takes
|
||||||
|
// `e.message` if the thrown value `e` is an `Error`, else just converts
|
||||||
|
// with the `String` builtin.)
|
||||||
|
//
|
||||||
|
// This can be used for "strong validation". If `U` is a (possibly
|
||||||
|
// opaque) subtype of `T`, and `f: (T) => U` is a checked downcast that
|
||||||
|
// either returns a `U` or throws an error, then `fmap` can transform a
|
||||||
|
// `Parser<T>` into a validating `Parser<U>`, where the fact that the
|
||||||
|
// validation has been performed is encoded at the type level. Thus:
|
||||||
|
//
|
||||||
|
// import * as C from ".../combo";
|
||||||
|
// import {NodeAddress, type NodeAddressT} from ".../core/graph";
|
||||||
|
//
|
||||||
|
// const addressParser: Parser<NodeAddressT> =
|
||||||
|
// C.fmap(C.array(C.string), NodeAddress.fromParts);
|
||||||
|
//
|
||||||
|
// As a degenerate case, it can also be used for "weak validation",
|
||||||
|
// where the types `T` and `U` are the same and the function `f` simply
|
||||||
|
// returns its argument or throws, but in this case there is nothing
|
||||||
|
// preventing a user of a `Parser<T>` from simply forgetting to
|
||||||
|
// validate. Prefer strong validation when possible.
|
||||||
|
export function fmap<T, U>(p: Parser<T>, f: (T) => U): Parser<U> {
|
||||||
|
return new Parser((x) => {
|
||||||
|
const maybeT = p.parse(x);
|
||||||
|
if (!maybeT.ok) {
|
||||||
|
return failure(maybeT.err);
|
||||||
|
}
|
||||||
|
const t = maybeT.value;
|
||||||
|
let u: U;
|
||||||
|
try {
|
||||||
|
u = f(t);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
return failure(e.message);
|
||||||
|
} else {
|
||||||
|
return failure(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success(u);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function array<T>(p: Parser<T>): Parser<T[]> {
|
export function array<T>(p: Parser<T>): Parser<T[]> {
|
||||||
return new Parser((x) => {
|
return new Parser((x) => {
|
||||||
if (!Array.isArray(x)) {
|
if (!Array.isArray(x)) {
|
||||||
|
@ -75,6 +75,66 @@ describe("src/util/combo", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("pure", () => {
|
||||||
|
it("does what it says on the tin", () => {
|
||||||
|
type Color = "RED" | "GREEN" | "BLUE";
|
||||||
|
const p: C.Parser<Color> = C.pure("GREEN");
|
||||||
|
expect(p.parseOrThrow(p)).toEqual("GREEN");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fmap", () => {
|
||||||
|
type Color = "RED" | "GREEN" | "BLUE";
|
||||||
|
function stringToColor(s: string): Color {
|
||||||
|
const c = s.toLowerCase().charAt(0);
|
||||||
|
switch (c) {
|
||||||
|
case "r":
|
||||||
|
return "RED";
|
||||||
|
case "g":
|
||||||
|
return "GREEN";
|
||||||
|
case "b":
|
||||||
|
return "BLUE";
|
||||||
|
default:
|
||||||
|
throw new Error("unknown color: " + JSON.stringify(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it("handles the success case", () => {
|
||||||
|
const p: C.Parser<Color> = C.fmap(C.string, stringToColor);
|
||||||
|
expect(p.parseOrThrow("blu")).toEqual("BLUE");
|
||||||
|
});
|
||||||
|
it("handles failure of the base parser", () => {
|
||||||
|
const p: C.Parser<Color> = C.fmap(C.string, stringToColor);
|
||||||
|
const thunk = () => p.parseOrThrow(77);
|
||||||
|
expect(thunk).toThrow("expected string, got number");
|
||||||
|
});
|
||||||
|
it("handles `Error`s thrown by the mapping function", () => {
|
||||||
|
const p: C.Parser<Color> = C.fmap(C.string, stringToColor);
|
||||||
|
// Avoid `.toThrow` because that checks for a substring, and we
|
||||||
|
// want to ensure no "Error: " prefix is included.
|
||||||
|
expect(p.parse("wat")).toEqual({ok: false, err: 'unknown color: "wat"'});
|
||||||
|
});
|
||||||
|
it("handles failure of the mapping function", () => {
|
||||||
|
const p: C.Parser<Color> = C.fmap(C.string, () => {
|
||||||
|
throw 123;
|
||||||
|
});
|
||||||
|
expect(p.parse("wat")).toEqual({ok: false, err: "123"});
|
||||||
|
});
|
||||||
|
it("composes", () => {
|
||||||
|
const raw: C.Parser<string> = C.string;
|
||||||
|
const trimmed: C.Parser<string> = C.fmap(raw, (s) => s.trim());
|
||||||
|
const color: C.Parser<Color> = C.fmap(trimmed, stringToColor);
|
||||||
|
expect(color.parseOrThrow(" blu\n\n")).toEqual("BLUE");
|
||||||
|
});
|
||||||
|
it("is type-safe", () => {
|
||||||
|
// input safety
|
||||||
|
// $ExpectFlowError
|
||||||
|
C.fmap(C.string, (n: number) => n.toFixed());
|
||||||
|
// output safety
|
||||||
|
// $ExpectFlowError
|
||||||
|
(C.fmap(C.number, (n: number) => n.toFixed()): C.Parser<number>);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("array", () => {
|
describe("array", () => {
|
||||||
it("accepts an empty array", () => {
|
it("accepts an empty array", () => {
|
||||||
const p: C.Parser<string[]> = C.array(C.string);
|
const p: C.Parser<string[]> = C.array(C.string);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user