7.7.0.3

28 — Monads

BJ’s recording

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).—O’CAML no longer relies on these ideas. Most of these systems have adapted something called ANF or A normal form instead, a close cousin to Moggi’s monadic calculus.

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

In this course we have seen three radically different styles of interpreters that address three different concerns:
  1. 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

  2. 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)

  3. 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.

These interpreters share commonalities that an extremely sophisticated reader can discern but are too veiled to be recognized and exploited.

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)
   

Figure 106: A Continuation-Passing Interpreter

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).

A monad implements three functions and exports them:
; ([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
>>=
These functions permit a clean organization of the basic features of a programming language interpreter. All common features benefit from these functions.

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

With additional functions in the monad implementation, the store monad interpreter can express the declaration of mutable variables, references to them, and updates:
[(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)))]

Similarly, the continuation monad interpreter can grab continuations:
; - - - - - - - - - - - - - - - - - - - - - - - - - - -
; 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))
   

Figure 107: The Plain Monad

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)
   

Figure 108: Monads Emphasize Sequencing

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])
   

Figure 109: The CPS Monad

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)
   

Figure 110: Monads Hide Continuations

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++))
   

Figure 111: The Store Monad

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)
   

Figure 112: Monads Hide Stores

Some Mathematics

Mathematically speaking a [Monad T] is a transformation on types plus two operations:

    return : T --> [Monad T]

    >>=    : [Monad T] -> [T -> [Monad T]] -> [Monad T]

The first injects a value of type T into a monad and the second sequences a monad with an injection. The run function extracts a value from a monad.

What makes monads interesting is that these three operations satisfy three equations no matter which monad we implement:

    [1] (>>= (return A) K) == (K A)

    

    [2] (>>= M return) == M

    

    [3] (>>= (λ (x) (>>= (K x) H))) == (>>= (>>= M K) H)

If you have seen group theory or abstract algebra, you recognize the first two laws as “laws of identity” and the last one as a form of “law of associativity.” Intuitively, if you return an answer only to inject it back into a computation, just inject. Conversely, if you sequence a computation with a return of the answer, just run the computation. Finally, whether you write a block as  (S1; S2); S3 or as  S1; (S2; S3) should not matter—as long as the sequencing is done properly.

Let’s see how these equations work out for the most complicated looking monad here, our store monad.

Equation 1
(>>= (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]
Each step either unfolds a definition or uses the substitution rule for function application.

Equation 2
(>>= 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
In mathematics and mathematical philosophy, this last law is called eta and also extensionality. It says that wrapping a function in a lambda plus an application behaves like the function.

Equation 3
(>>= 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)
This one is a mouthful but they are atomic steps.

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.