28 — Monads
Friday, 17 April 2020
Programming language research seeks abstractions that help developers organize their thoughts, articulate them clearly as code, and do so effectively. They are also looking to organize their own world and understand how language constructs relate to each other, what they have in common, and how they differ. While many abstractions and organization ideas are discovered in a ‘\bottom up” manner, that is, building up from concrete examples, people in this field have always sought the kind of cross-pollination between our discipline and mathematics the way that has benefited physics for centuries.
Moggi recognized the connection between mathematical models
of programming languages and the idea of Kleisli triples also known as
monads. Before him Scott suggested the use of category theory in the 1970s
and Cousineu, Curien, and Mauny proposed to exploit the correspondence
between cartesian-closed categories and simply typed lambda calculus for
the design of an abstract machine, the Categorical Abstract Machine, and
with it Categorical Abstract Machine Language (CAML).—
This lecture is about a rather strong example of this kind. It emerged in the mid 1980s and early 1990s. The discovery coincided with several “bottom up” ideas that emerged at that time.
Sequencing vs Computing
7 —
Errors and Ordering, which injects the idea of strictly ordering sub-computations so that even error reporting becomes predictable and helpful to the developer 9 —
Store Passing, which enhances the ordering interpreter to pass around a data structure— the store— that represents the dynamic flow of effects as opposed to the static scope (program regions) 17 —
CPS, which could have presented a continuation-passing interpreter that can express dynamic extent and non-local flow of control, e.g. "grab". We didn’t. So I have done it here in figure 106.
This idea is the concern of this lecture: monads.
Lectures/28/cps-interpret.rkt
#lang racket ;; a monad interpreter that explicitly orders evaluations (require "cps-aux.rkt") #; {type Value = Number || (function-value parameter FExpr Env) || [Value Kont -> Value]} #; {type Kont = [Value -> Value]} #; {FExpr -> Value} ;; determine the value of ae via a substitutione semantics (define (cps-interpret ae0) #; {FExpr Env Kont -> Value} ;; ACCUMULATOR env tracks all declarations between ae and ae0 (define (with-accu ae env k) (match ae [(? integer?) (k ae)] [(node o a1 a2) (with-accu a2 env (λ (right0) (define right (number> right0)) (with-accu a1 env (λ (left0) (define left (number> left0)) (k (o left right))))))] [(call ae1 ae2) (with-accu ae2 env (λ (right) (with-accu ae1 env (λ (left0) (define left (function> left0)) (fun-apply left right k)))))] [(fun para body) (k (function-value para body env))] [(if-0 tst thn els) (with-accu tst env (λ (t) (with-accu (if (is-zero? t) thn els) env k)))] [(decl x a1 a2) (with-accu a2 (decl-interpret x a1 env) k)] [(? string?) (if (defined? ae env) (k (lookup ae env)) (error UNDECLARED ae))] ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; let's grab continuations: [(grab x body) (with-accu body (add x (kont->value k) env) k)])) #; {Value Value Kont -> Value} (define (fun-apply function-representation argument-value k) (match function-representation [(? procedure?) ((function-representation argument-value) k)] [(function-value fpara fbody env) (with-accu fbody (add fpara argument-value env) k)])) #; {Var (U Integer Fun) Env -> Env} (define (decl-interpret x a1 env) (match a1 [(? integer?) (add x a1 env)] [(fun p b) (add-rec x (λ (env*) (function-value p b env*)) env)])) #; {Kont -> Value} (define (kont->value k) (λ (x) (λ (_) (k x)))) (with-accu ae0 empty (λ (x) x))) (provide cps-interpret)
Compare and Contrast
The following three snippets from these interpreters share a common concept, and yet, the program text cannot reveal it.
Ordering Interpreter
[(node o a1 a2) (define right (number> (interpret a2 env))) (define left (number> (interpret a1 env))) (o left right)]
Store-Passing Interpreter
[(node o a1 a2) (define-values (right0 store+) (interpret a2 env store)) (define right (number> right0)) (define-values (left0 store++) (interpret a1 env store+)) (define left (number> left0)) (values (o left right) store++)]
Continuation-Passing Interpreter
[(node o a1 a2) (interpret a2 env (λ (right0) (define right (number> right0)) (interpret a1 env (λ (left0) (define left (number> left0)) (k (o left right))))))]
The idea of monads is to bring out the commonality among these three snippets.
Stop! Take a look at the store-passing version. How does it express that the store isn’t used?
Monads, Kleisli Triples, and Such
The commonality of all three code snippets is that they express an ordering among the three computations. Monads help us separate the sequencing or compositioning of computations from the computations themselves. The idea itself is to separate values from computations at the syntactic level (types, functions).
; ([Monad T] -> T) ; extract a value from a Monad computation run ; {T -> [Monad T]} ; inject a value into a Monad computation return ; {[Monad T] [T -> [Monad T]] -> [Monad T]} ; sequence two computations by passing the value of the first to the second >>=
A monad module may provide additional, custom-made functions on the underlying data representation. These additional functions allow the authors of interpreters and compilers to express language features that advantage of the data structure: extend the store, update it, grab a continuations.
Commonalities Revisited
Let’s see how we express the three snippets about once we have a monad implementation for each of our concerns:
Ordering Monad Interpreter
[(node o a1 a2) (>>= (interpret a2 env) (λ (right0) (define right (number> right0)) (>>= (interpret a1 env) (λ (left0) (define left (number> left0)) (return (o left right))))))]
Store Monad Interpreter
[(node o a1 a2) (>>= (interpret a2 env) (λ (right0) (define right (number> right0)) (>>= (interpret a1 env) (λ (left0) (define left (number> left0)) (return (o left right))))))]
Continuation Monad Interpreter
[(node o a1 a2) (>>= (interpret a2 env) (λ (right0) (define right (number> right0)) (>>= (interpret a1 env) (λ (left0) (define left (number> left0)) (return (o left right))))))]
No you don’t have vision problems. They really look the same. The replacement for store-passing really brings across that the store does not play a role in this part of the interpreter.
Adding Functions to Monads
[(decl x a1 a2) (alloc* (λ (loc) (>>= (interpret a1 (add x loc env)) (λ (val) (update* loc val (λ _ (define env+ (add x loc env)) (interpret a2 env+)))))))] [(? string?) (if (defined? ae env) (retrieve* (lookup ae env)) (error 'I UNDECLARED ae))] [(set lhs rhs) (>>= (interpret rhs env) (λ (val) (update* (lookup lhs env) val return)))]
; - - - - - - - - - - - - - - - - - - - - - - - - - - - ; let's grab continuations: [(grab x body) (grab* (λ (k) (interpret body (add x k env))))]
A Monadic Order Interpreter
This is a finger exercise.
Lectures/28/order-monad.rkt
#lang racket ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; generic monad operation, for some type T: (provide #; ([Monad T] -> T) run #; {T -> [Monad T]} return #; {[Monad T] [T -> [Monad T]] -> [Monad T]} >>=) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; Monad[T] = T -> Symbol x T (define GO (gensym)) (define (run m) (define-values (result _go) (m GO)) result) (define ((return result) _go) (values result _go)) (define ((>>= m f) _go) (define-values (val _go-next) (m _go)) ((f val) _go-next))
Lectures/28/order-monad-interpret.rkt
#lang racket ;; a monad interpreter that explicitly orders evaluations (require "./order-monad.rkt") (require "order-aux.rkt") #; {Value = Number || (function-value parameter FExpr Env)} #; {FExpr -> Value} ;; determine the value of ae via a substitutione semantics (define (order-interpret ae0) #; {FExpr Env -> Value} ;; ACCUMULATOR env tracks all declarations between ae and ae0 (define (with-accu ae env) (match ae [(? integer?) (return ae)] [(node o a1 a2) (>>= (with-accu a2 env) (λ (right0) (define right (number> right0)) (>>= (with-accu a1 env) (λ (left0) (define left (number> left0)) (return (o left right))))))] [(call ae1 ae2) (>>= (with-accu ae2 env) (λ (right) (>>= (with-accu ae1 env) (λ (left0) (define left (function> left0)) (fun-apply left right)))))] [(fun para body) (return (function-value para body env))] [(if-0 tst thn els) (>>= (with-accu tst env) (λ (t) (with-accu (if (is-zero? t) thn els) env)))] ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; declare variables and reference them [(decl x a1 a2) (with-accu a2 (decl-interpret x a1 env))] [(? string?) (if (defined? ae env) (return (lookup ae env)) (error UNDECLARED ae))])) #; {Value Value -> Value} (define (fun-apply function-representation argument-value) (match function-representation [(function-value fpara fbody env) (with-accu fbody (add fpara argument-value env))])) (define (decl-interpret x a1 env) (match a1 [(? integer?) (add-rec x a1 env)] [(fun para body) (add-rec x (λ (r-env) (function-value para body r-env)) env)])) (run (with-accu ae0 empty))) (provide order-interpret)
A Monadic Continuation Interpreter
Let’s do continuations next. They are simpler than stores, an intermediate step before we get to store monads.
Lectures/28/cps-monad.rkt
#lang racket ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; generic monad operation, for some type T: (provide #; ([Monad T] -> T) run #; {T -> [Monad T]} return #; {[Monad T] [T -> [Monad T]] -> [Monad T]} >>=) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; Monad[T] = [T -> T] -> T ;; really (Value -> Answer) -> Answer && Answer ~ final Value (define (run m) (define result (m (λ (final-val) final-val))) result) (define ((return result) k) (k result)) (define ((>>= m f) k) (m (λ (val-m) ((f val-m) k)))) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; continuation-specific operations (provide #; {[Kont -> [Monad T]] -> T} grab*) (define [(grab* consumer-m) k] [(consumer-m (λ (x) (λ (kp) (k x)))) k])
Lectures/28/cps-monad-interpret.rkt
#lang racket ;; a monad interpreter that explicitly orders evaluations (require "./cps-monad.rkt") (require "cps-aux.rkt") #; {type Value = Number || (function-value parameter FExpr Env) || [Value Kont -> Value]} #; {type Kont = [Value -> Value]} #; {FExpr -> Value} ;; determine the value of ae via a substitutione semantics (define (cps-interpret ae0) #; {FExpr Env -> Value} ;; ACCUMULATOR env tracks all declarations between ae and ae0 (define (with-accu ae env) (match ae [(? integer?) (return ae)] [(node o a1 a2) (>>= (with-accu a2 env) (λ (right0) (define right (number> right0)) (>>= (with-accu a1 env) (λ (left0) (define left (number> left0)) (return (o left right))))))] [(call ae1 ae2) (>>= (with-accu ae2 env) (λ (right) (>>= (with-accu ae1 env) (λ (left0) (define left (function> left0)) (fun-apply left right)))))] [(fun para body) (return (function-value para body env))] [(if-0 tst thn els) (>>= (with-accu tst env) (λ (t) (with-accu (if (is-zero? t) thn els) env)))] [(decl x a1 a2) (with-accu a2 (decl-interpret x a1 env))] [(? string?) (if (defined? ae env) (return (lookup ae env)) (error UNDECLARED ae))] ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; let's grab continuations: [(grab x body) (grab* (λ (k) (with-accu body (add x k env))))])) #; {Value Value -> Value} (define (fun-apply function-representation argument-value) (match function-representation [(? procedure?) (function-representation argument-value)] [(function-value fpara fbody env) (with-accu fbody (add fpara argument-value env))])) #; {Var (U Integer Fun) Env -> Env} (define (decl-interpret x a1 env) (match a1 [(? integer?) (add x a1 env)] [(fun p b) (add-rec x (λ (env*) (function-value p b env*)) env)])) (run (with-accu ae0 empty))) (provide cps-interpret)
A Monadic Store Interpreter
Here is one complex monad.
Lectures/28/store-monad.rkt
#lang racket ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; generic monad operation, for some type T: (provide #; ([Monad T] -> T) run #; {T -> [Monad T]} return #; {[Monad T] [T -> [Monad T]] -> [Monad T]} >>=) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; Monad[T] = Store -> T x Store (define (run m) (define-values (result _store) (m plain)) result) (define ((return result) store) (values result store)) (define ((>>= m f) store) (define-values (val store++) (m store)) ((f val) store++)) ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; store-specific operations (provide #; {[Location -> [Monad T]] -> T} alloc* #; {Location -> [Monad T]} retrieve* #; {Location Value [Value ->* Value Store] -> [Monad Store]} update*) (require "../9/store.rkt") (define ((alloc* f) store) (define-values (loc store+) (alloc store #f)) ((f loc) store+)) (define ((retrieve* l) store) (values (retrieve store l) store)) (define ((update* loc val f) store) (define old (retrieve store loc)) (define store++ (update store loc val)) ((f old) store++))
Lectures/28/store-monad-interpret.rkt
#lang racket ;; an interpreter that uses the store monad (require "store-monad.rkt") (require "store-aux.rkt") #; {Value = Number || (function-value parameter FExpr Env)} ;; basic monad functions #; {FExpr -> Value} (define (store-interpret ae0) #; {FExpr Env -> Store ->* Value Store} ;; ACCUMULATOR env tracks all declarations between ae and ae0 (define (with-accu ae env) (match ae [(? integer?) (return ae)] [(node o a1 a2) (>>= (with-accu a2 env) (λ (right0) (define right (number> right0)) (>>= (with-accu a1 env) (λ (left0) (define left (number> left0)) (return (o left right))))))] [(call ae1 ae2) (>>= (with-accu ae2 env) (λ (right) (>>= (with-accu ae1 env) (λ (left) (fun-apply (function> left) right)))))] [(fun para body) (return (function-value para body env))] [(if-0 tst thn els) (>>= (with-accu tst env) (λ (t) (with-accu (if (is-zero? t) thn els) env)))] ;; - - - - - - - - - - - - - - - - - - - - - - - - - - - ;; modified or new clauses compared to order monad [(decl x a1 a2) (alloc* (λ (loc) (>>= (with-accu a1 (add x loc env)) (λ (val) (update* loc val (λ _ (define env+ (add x loc env)) (with-accu a2 env+)))))))] [(? string?) (if (defined? ae env) (retrieve* (lookup ae env)) (error 'I UNDECLARED ae))] [(set lhs rhs) (>>= (with-accu rhs env) (λ (val) (update* (lookup lhs env) val return)))] [(sequ fst rst) (>>= (with-accu fst env) (λ (_) (with-accu rst env)))])) #; {Value Value -> Store ->* Value Store} (define (fun-apply function-representation argument-value) (match function-representation [(function-value fpara fbody env) (alloc* (λ (loc) (update* loc argument-value (λ _ (with-accu fbody (add fpara loc env))))))])) (run (with-accu ae0 empty))) (provide store-interpret)
Some Mathematics
return : T --> [Monad T] |
>>= : [Monad T] -> [T -> [Monad T]] -> [Monad T] |
[1] (>>= (return A) K) == (K A) |
|
[2] (>>= M return) == M |
|
[3] (>>= (λ (x) (>>= (K x) H))) == (>>= (>>= M K) H) |
Let’s see how these equations work out for the most complicated looking monad here, our store monad.
(>>= (return A) K) == (>>= (λ (store) (values A store)) K) == (λ (store) (define-values (val store+) [(λ (store) (values A store)) store]) ([K val] store)) == (λ (store) (define-values (val store+) (values A store)) ([K val] store)) == (λ (store) ([K A] store)) == [K A]
(>>= M return) == (λ (store) (define-values (val store+) (M store)) ((return val) store+)) == (λ (store) (define-values (val store+) (M store)) (values val store+)) == (λ (store) (M store)) M
(>>= M (λ (x) (>>= (K x) H))) == (λ (store) (define-values (val store+) [M store]) (((λ (x) (>>= (K x) H)) val) store+)) == (λ (store) (define-values (val store+) [M store]) ((>>= (K val) H) store+)) == (λ (store) (define-values (val store+) [M store]) ((λ (s) (define-values (val2 store++) ((K val) s)) ((H val2) store++)) store+)) == (λ (store) (define-values (val store+) [M store]) (define-values (val2 store++) ((K val) store+)) ((H val2) store++)) == ; here we use out understanding of the ordering of let (λ (store) (define-values (val store++) (let () (define-values (val2 store+) [M store]) ; still eval-ed before the upper define-values ((K val2) store+))) ((H val) store++)) == (λ (store) (define-values (val store++) ((λ (s) (define-values (val2 store+) [M s]) ((K val2) store+)) store)) ((H val) store++)) == (λ (store) (define-values (val store+) ((>>= M K) store)) ((H val) store+)) == (>>= (>>= M K) H)
These laws give us ways to organize equivalence proofs between program fragments, both if we are developers or compiler writers who wish to optimize monad-based programs.
On the Expressive Power of Monad Types
Since the original transfer of monads into programming language theory, one language fully embraced its use: Haskell. This transfer is not coincidence, because Haskell is a purely functional language and is lazy in addition. This combination of features severely limits the programmer and people struggled with this problem for the initial five or so years of Haskell’s existence. To their credit, they came up with beautiful ways to work around these limitations, but work-arounds are just that: things that should not exist.
Haskell’s embrace of monads allowed them to continue the pretense of having a functional language while at the same time injecting true effects: variable assignment, structure mutation, I/O, manipulating continuations, concurrency effects, and so on. Naturally the injection of monads also improves Haskell’s performance in these directions.
Of course, Haskell programmers must explicitly set up the monads that they wish to use. Otherwise they cannot deploy the kind of effects they wish to have. Although this may seem to be a form of boilerplate programming, the setup is relatively painless and actually seems to provide an advantage.
In a nutshell, the power of monads really derives from their type construction. A Haskell program that “yells” I/O monad tells the reader that this part of the program is imperative. A different part may use the continuation monad, and the reader immediately knows that it may realize a fancy form of backtracking. Finally when there is no monad, the reader can apply the principles of high school algebra alone to think about the program. Put differently, monad types enrich the developers’ language to emphasize an idea in one place and, like all type systems, prohibit ideas in another.
The Downsides
You may have noticed that my monad-based interpreters implement either store manipulations or continuation manipulations. The problem is that monads don’t compose (easily). If you want both continuations and stores, the monad implementation must explicitly combine them.
In a way this problem doesn’t come as a surprise at all. Before programming language researchers organized their field with monads, they had already understood the idea without having a word for it. Using monads beautifies these old constructions, brings out similarities, emphasizes differences, but it does not change how interpreters (or programs) work. And we knew from the time before monads that composing effects takes work. In the end, it remains open whether many languages will embrace monads or whether they continue to embrace effects just as they are.
For the past decade, the research area has begun to embrace the idea of algebraic effects as an alternative to monads. Plotkin and Powers introduced the idea as an explicit correspondent to monads in operational semantics in the early 00s. Some five years ago, people began to realize that they generalized an idea I had present in 1994 as the keynote speaker at a conference in Sendai, Japan. (This paper had been in my “drawer” for nearly eight years back then.) Recent implementation techniques exploit another one of my ideas, delimited continuations. The Journal of Functional Programming has started to publish a special issue on algebraic effects; the first paper is out and you may wish to read the others as they appear.