Last active
March 11, 2025 21:01
-
-
Save dyerw/51b3382b96bbbf24936ed8252cee7d6c to your computer and use it in GitHub Desktop.
First part of Wadler's "Monads for funtional programming" translated to 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
// Monads for functional programming in TypeScript | |
// Here's a basic evaluator | |
type Term = | |
| { tag: 'Con', n: number } | |
| { tag: 'Div', arg1: Term, arg2: Term } | |
const con = (n: number): Term => ({ tag: 'Con', n }); | |
const div = (arg1: Term, arg2: Term): Term => ({ tag: 'Div', arg1, arg2 }); | |
function evalTerm(t: Term): number { | |
switch (t.tag) { | |
case 'Con': | |
return t.n; | |
case 'Div': | |
return evalTerm(t.arg1) / evalTerm(t.arg2); | |
} | |
} | |
// 20 / (10 / 2) = 4 | |
const ex1 = evalTerm( | |
div( | |
con(20), | |
div( | |
con(10), | |
con(2)))) | |
console.log(ex1 === 4); | |
// Say it is desired to add error checking, so that divide by zero returns | |
// a sensible error message. In an impure language, this is easily achieved with the | |
// use of exceptions. But we have to do this: | |
type WithException<A> = { tag: 'Raise', error: string } | { tag: 'Return', a: A }; | |
function raise<A>(error: string): WithException<A> { return ({ tag: 'Raise', error }); } | |
function ret<A>(a: A): WithException<A> { return ({ tag: 'Return', a }) } | |
// It is straightforward, but tedious, to adapt the evaluator to this representation | |
function evalWithException(t: Term): WithException<number> { | |
switch (t.tag) { | |
case 'Con': | |
return ret(t.n); | |
case 'Div': | |
const a1 = evalWithException(t.arg1); | |
switch (a1.tag) { | |
case 'Raise': | |
return a1; // re-raise first arg | |
case 'Return': | |
const a2 = evalWithException(t.arg2); | |
switch (a2.tag) { | |
case 'Raise': | |
return a2; // re-raise second arg | |
case 'Return': | |
if (a2.a === 0) { | |
return raise("divide by zero"); | |
} else { | |
return ret(a1.a / a2.a); | |
} | |
} | |
} | |
} | |
} | |
// 20 / (10 / 2) = 4 | |
const ex2 = evalWithException( | |
div( | |
con(20), | |
div( | |
con(10), | |
con(2)))) | |
console.log(JSON.stringify(ex2) === JSON.stringify(ret(4))); | |
// 20 / (10 / 0) = divide by zero | |
const ex3 = evalWithException( | |
div( | |
con(20), | |
div( | |
con(10), | |
con(0)))); | |
console.log(JSON.stringify(ex3) === JSON.stringify(raise("divide by zero"))); | |
// Forgetting errors for the moment, say it is desired to count the number of divisions performed during evaluation. | |
// In an impure language, this is easily achieved by the use of state. But we can: | |
// Accepts _initial_ state and returns _final_ state and computation result | |
type WithState<S, A> = (state: S) => readonly [A, S]; | |
function evalWithState(t: Term): WithState<number, number> { | |
switch (t.tag) { | |
case 'Con': | |
return (s) => [t.n, s]; | |
case 'Div': | |
return (s) => { | |
const [n1, s1] = evalWithState(t.arg1)(s); | |
const [n2, s2] = evalWithState(t.arg2)(s); | |
return [n1 / n2, 1 + s1 + s2]; | |
} | |
} | |
} | |
// 20 / (10 / 2) = 4 w/ 2 divisions | |
const ex4 = evalWithState( | |
div( | |
con(20), | |
div( | |
con(10), | |
con(2)))) | |
const [answer, numDivisions] = ex4(0); | |
console.log(answer === 4); | |
console.log(numDivisions === 2); | |
// Finally, say it is desired to display a trace of execution. In an impure language, | |
// this is easily done by inserting output commands at appropriate points. | |
// In a pure language, output may be mimicked | |
// by introducing a type to represent computations that generate output. | |
type WithOutput<A> = readonly [string, A]; | |
function evalWithOutput(t: Term): WithOutput<number> { | |
switch (t.tag) { | |
case 'Con': | |
return ["", t.n]; | |
case 'Div': | |
const [o1, n1] = evalWithOutput(t.arg1); | |
const [o2, n2] = evalWithOutput(t.arg2); | |
const result = n1 / n2; | |
const line = ` ${n1} / ${n2} = ${result};`; | |
return [o1 + o2 + line, n1 / n2]; | |
} | |
} | |
// 20 / (10 / 2) = 4 | |
const ex5 = evalWithOutput( | |
div( | |
con(20), | |
div( | |
con(10), | |
con(2)))) | |
const [output, ans] = ex5; | |
console.log(ans === 4); | |
console.log(output === " 10 / 2 = 5; 20 / 5 = 4;"); | |
// 2.5 A monadic evaluator | |
// Each of the variations on the interpreter has a similar structure, which may be | |
// abstracted to yield the notion of a monad. | |
// Observe: | |
type OriginalEvaluator = (t: Term) => number; | |
type ExceptionEvaluator = (t: Term) => WithException<number>; | |
type StateEvaluator = (t: Term) => WithState<number, number>; | |
type OutputEvaluator = (t: Term) => WithOutput<number>; | |
// Or generically for any M: type Evaluator = (t: Term) => M<number> | |
// (Note: we cannot express this type in TS due to lack of higher-kinded types, | |
// in other words: type variables cannot be _applied_ in type signatures like M<number>) | |
// If we wanted to generically write a single "evalM" fn for all these cases what would we need? | |
// Recalling the `Con` case of all previous evaluators we would need | |
// function pure(t: Term): M<Term> | |
// and then we need a way to take the nested responses of evalM in the `Div` case and | |
// combine them | |
// function bind(a: M<Term>, f: (t: Term) => M<number>): M<number> | |
// A Monad is an instance of each of these functions for a given type | |
// What I would very much like to write here is: | |
// type Monad<M, A> = { | |
// pure: (a: A) => M<A>; | |
// bind: <B>(ma: M<A>, f: (a: A) => M<B>) => M<B> | |
// } | |
// but because of the afore-mentioned lack of higher-kinded types here are all our examples | |
// individually, but they are all exactly the same with different M | |
type ExceptionMonad<A> = { | |
pure: (a: A) => WithException<A>; | |
bind: <B>( | |
ma: WithException<A>, | |
f: (a: A) => WithException<B> | |
) => WithException<B>; | |
} | |
type StateMonad<S,A> = { | |
pure: (a: A) => WithState<S,A>; | |
bind: <B>( | |
ma: WithState<S,A>, | |
f: (a: A) => WithState<S,B> | |
) => WithState<S,B>; | |
} | |
type OutputMonad<A> = { | |
pure: (a: A) => WithOutput<A>; | |
bind: <B>( | |
ma: WithOutput<A>, | |
f: (a: A) => WithOutput<B> | |
) => WithOutput<B>; | |
} | |
// and their concrete implementations: | |
const ExceptionMonad = function<A>(): ExceptionMonad<A> { | |
return { | |
pure: (a) => ret(a), | |
bind: (ma, f) => { | |
switch (ma.tag) { | |
case 'Raise': | |
return ma; | |
case 'Return': | |
return f(ma.a); | |
} | |
} | |
} | |
}; | |
const StateMonad = function<S, A>(): StateMonad<S, A> { | |
return { | |
pure: (a) => (s) => [a, s], | |
bind: (ma, f) => { | |
return (s0) => { | |
const [a, s1] = ma(s0); | |
const [b, s2] = f(a)(s1); | |
return [b, s2]; | |
} | |
} | |
} | |
} | |
function puts<S>(f: (s: S) => S): WithState<S, undefined> { | |
return (s) => [undefined, f(s)]; | |
} | |
const OutputMonad = function<A>(): OutputMonad<A> { | |
return { | |
pure: (a) => ["", a], | |
bind: (ma, f) => { | |
const [o1, a] = ma; | |
const [o2, b] = f(a); | |
return [o1 + o2, b]; | |
} | |
} | |
} | |
function write(s: string): WithOutput<undefined> { | |
return [s, undefined]; | |
} | |
// Now we are able to write all three evaluators with a single function | |
// We also now pass in a side-effecting action (a, b) => M<undefined> | |
function evalM(m: ExceptionMonad<number>, t: Term, onDiv: (a: number, b: number) => WithException<undefined>): WithException<number>; | |
function evalM(m: StateMonad<number, number>, t: Term, onDiv: (a: number, b: number) => WithState<number, undefined>): WithState<number, number>; | |
function evalM(m: OutputMonad<number>, t: Term, onDiv: (a: number, b: number) => WithOutput<undefined>): WithOutput<number>; | |
function evalM(m: any, t: Term, onDiv: any): any { | |
switch (t.tag) { | |
case 'Con': | |
return m.pure(t.n); | |
case 'Div': | |
// Here we get some sense of why this is called "bind" | |
return m.bind( | |
evalM(m, t.arg1, onDiv), // action -> eval arg1 | |
(a: number) => m.bind( // result -> a | |
evalM(m, t.arg2, onDiv), // action -> eval arg2 | |
(b: number) => m.bind( // result -> b | |
onDiv(a, b), // action -> onDiv(a, b) | |
() => m.pure(a / b) // result is ignored (undefined) | |
) | |
) | |
) | |
} | |
} | |
const t6 = div(con(20), div(con(10), con(0))); | |
const ex6 = evalM( | |
ExceptionMonad<number>(), | |
t6, | |
(_a, b) => b == 0 ? raise("divide by zero") : ret(undefined) | |
); | |
const validTerm = div(con(20), div(con(10), con(5))); | |
const ex7 = evalM( | |
StateMonad<number, number>(), | |
validTerm, | |
() => puts(i => i + 1) | |
) | |
const ex8 = evalM( | |
OutputMonad<number>(), | |
validTerm, | |
(a, b) => write(` ${a} / ${b} = ${a / b};`) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment