Skip to content

Instantly share code, notes, and snippets.

@MonoidMusician
Created January 19, 2026 18:06
Show Gist options
  • Select an option

  • Save MonoidMusician/49f75ed9ecc7cea0afa9a79164619e4a to your computer and use it in GitHub Desktop.

Select an option

Save MonoidMusician/49f75ed9ecc7cea0afa9a79164619e4a to your computer and use it in GitHub Desktop.
Selective applicative parser demo in TypeScript
// 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