Created
January 19, 2026 18:06
-
-
Save MonoidMusician/49f75ed9ecc7cea0afa9a79164619e4a to your computer and use it in GitHub Desktop.
Selective applicative parser demo in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This is a monadic parser type, but we will just use it as a selective applicative | |
| type Parser<T> = (input: string) => [T, string] | (null | Error); | |
| // The first parser in a select will return a union like | |
| // `{ case: "1", value: T1 } | { case: "2", value: T2 }` | |
| type SelectCases<Cases extends Record<string, unknown>> = { | |
| [k in keyof Cases]: { case: k, value: Cases[k] } | |
| }[keyof Cases]; // squash it into a union type | |
| // The second arugment is a record of parsers to handle each case. | |
| // The case gets the value of the match and returns the common output type. | |
| type SelectBranches<Cases extends Record<string, unknown>, Output> = { | |
| [k in keyof Cases]: Parser<(value: Cases[k]) => Output> | |
| }; | |
| // Now we can implement it | |
| function select< | |
| Cases extends Record<string, unknown>, | |
| Output, | |
| >(scrutinee: Parser<SelectCases<Cases>>, | |
| branches: SelectBranches<Cases, Output> | |
| ): Parser<Output> { | |
| return function selector(input0: string) { | |
| // Run the first parser | |
| const output0 = scrutinee(input0); | |
| if (!output0 || output0 instanceof Error) return output0; | |
| // It has selected a branch now | |
| const [{ case: selected, value: matched }, input1] = output0; | |
| const output1 = branches[selected](input1); | |
| if (!output1 || output1 instanceof Error) return output1; | |
| const [value, remaining] = output1; | |
| // Finally, if both passed, we pass `matched` to the second parser | |
| return [value(matched), remaining]; | |
| } | |
| } | |
| // Other standard combinators | |
| function map<I, O>(parser: Parser<I>, fn: (input: I) => O): Parser<O> { | |
| return function(input: string) { | |
| const output = parser(input); | |
| if (!output || output instanceof Error) return output; | |
| const [value, remaining] = output; | |
| return [fn(value), remaining]; | |
| } | |
| } | |
| function fail(error?: null | Error): Parser<never> { | |
| return function(_input: string) { | |
| return error ?? null; | |
| } | |
| } | |
| function pure<O>(output: O): Parser<O> { | |
| return function(input: string) { | |
| return [output, input]; | |
| } | |
| } | |
| // We can implement `filterMap` now. Surprisingly it can infer `Cases`! | |
| function filterMap<I, O>(parser: Parser<I>, fn: (input: I) => O | null): Parser<O> { | |
| return select(map(parser, (input: I) => { | |
| let output = fn(input); | |
| if (output === null) return { case: "none", value: null }; | |
| else return { case: "some", value: output }; | |
| }), { | |
| "none": fail(), | |
| "some": pure((output: O) => output), | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment