On this page:
monad-do
maybe-do
list-do
id-do
hole-do
3.15.1 Implementing a monad

3.15 Monads and Do Notation🔗ℹ

monad-do provides a generic, specializable DSL for handling monadic values, inspired by Haskell’s do notation and Scala’s for comprehensions. monad-do itself is generic, expecting the provision of functions for the bind (>>=), return, and guard operators, but individual types can easily layer over this with a simple macro to provide a specialized version of the DSL for a particular data type.

syntax

(monad-do (bind return guard) exprs ... final-expr)

 
exprs = (name <- val)
  | (name = val)
  | (if test)
  | (expr ...)
     
final-expr = (yield val ...)
  | (return-expr ...)
The main implementation for do notation. The opening clause is a list of the three necessary operators for a given type to implement monadic operations, which should be implemented as follows:

The rest of the body of the form is composed of various operations, which bind, guard, or return values, described as follows. The last line of the do notation is special, in a sense, as it must consist of either yield or a bare expression.

(name <- val)

Binds val to name. val must be an instance of the type over which the do form operates.

(name = val)

Wraps val in the current type, and binds it to name.

(if test)

Filters the ongoing expression according to test.

(yield val ...)

When used as the last line of a do form, returns the given val(s) wrapped in the type of the ongoing do form.

(expr ...)

When used in the body of a do form, the expr is evaluated but its return value ignored. If the last line of the do form is a bare expression, then the form will return the result of the expression.

syntax

(maybe-do expr ...)

A specialization of monad-do for Maybe. This is useful for chaining operations that return Maybe, as the monad for Maybe short-circuits. If one operation in the chain is a None, then the result of a yield will be none.

Example:
> (is-none? (maybe-do
             (a <- (some 5))
             (b <- None)
             (c = (+ a b))
             (yield c)))

#t

syntax

(list-do expr ...)

A specialization of monad-do for lists. list-do flatmaps over its operations forming a single-dimensional list from its calculations. This essentially enables list comprehensions.

Example:
> (list-do
   (rank <- (append (range 2 to 10) '(J Q K A)))
   (suit <- '(   ))
   (if (equal? suit '))
   (card = (format$ "#_#_" rank suit))
   (yield card))

'("2♦" "3♦" "4♦" "5♦" "6♦" "7♦" "8♦" "9♦" "10♦" "J♦" "Q♦" "K♦" "A♦")

syntax

(id-do expr ...)

The Identity monad as a specialization of monad-do. This essentially replaces the functionality of the old "monadish" DSL from Heresy 0.1.0 and earlier. Mostly this is useful as an example, but can be used for chaining together operations and mock-mutable behavior.

Example:
> (id-do
   (x = 5)
   (y = 4)
   (z = (+ x y))
   (print (format$ "#_ + #_ = #_" x y z)))

5 + 4 = 9

syntax

(hole-do expr ...)

A specialization of monad-do for holes. Allows you to operate over and combine values from multiple holes easily, while returning a new hole for future use.

Example:
> (deref
   (hole-do
   (x <- (hole 5))
   (y <- (hole 6))
   (z = (+ x y))
   (yield z)))

11

3.15.1 Implementing a monad🔗ℹ

A "monad" is a data type which can contain a value, and a set of operator functions which operate on that type while obeying certain rules. You can think of them as a kind of container, and the components of an assembly line that processes the container and its contents.

Let’s say that we have a Thing called Box, defined thusly:

Example:
> (describe Box (val Null))

We then define a set of three functions, that work with Box. The first, is return, which is a constructor function that wraps a value in our type:

Example:
> (def fn box-return (val)
    (Box (list val)))

The next function is bind, known in some languages as the operator >>=. This takes an instance of our type, and a function, and applies the function to the value inside our type. The definition of bind for Box looks like this:

Example:
> (def fn box-bind (box fn)
    (fn (box 'val)))

The final function is guard, which is not especially useful on its own, but enables us to implement a filter effect inside monad-do. This function takes a boolean value, the result of some test, and returns either an instance of our type with empty contents, or nothing. For Box, it looks like this.

Example:
> (def fn box-guard (test)
    (if test then (box-return Null) else Null))

Now, we can provide those functions to monad-do ourselves, or for convenience, we can define a macro that wraps monad-do without new operators pre-defined. It is necessary to use def macroset here, due to the peculiarities of the underlying Racket macro system.

Example:
> (def macroset box-do
    [(_ e ...)
     (monad-do (box-bind box-return box-guard) e ...)])

Together, these three functions actually form an implementation of the Identity monad, and by combining these and providing them to monad-do, we can already perform imperative-like operations in our otherwise functional language of Heresy, and all without any mutability involved! Behold:

Examples:
> (do
    (describe Box (val Null))
    (def fn box-return (val)
      (Box (list val)))
    (def fn box-bind (box fn)
      (fn (box 'val)))
    (def fn box-guard (test)
      (if test then (box-return Null) else Null))
    (def macroset box-do
      [(_ e ...)
       (monad-do (box-bind box-return box-guard) e ...)]))
> (box-do
   (a <- (box-return 5))
   (print a)
   (a <- (box-return 10))
   (b = (* a 5))
   (print b))

5

50