Skip to content

Composing Monads

blancas edited this page Feb 6, 2013 · 6 revisions

Having seen some of the benefits of using these codified design patterns called monads, it is natural to ask whether it could be possible to combine their effect such that, for example, one might generate output on the side while keeping track of a stateful computation. It turns out that Morph provides a set of monads, similar to the ones described in the previous section of this guide, but whose resulting value is itself a monad. These are called monad transformers and are the building blocks for working with nested monadic values. Thus composition of monads is done by using a monad type where we would otherwise use a regular data type.

To further illustrate the above, we present here sample code for the WriterT, which is similar to the Writer we have presented earlier, but whose computed value may be any other monad, including another monad transformer. WriterT's computed value will be an instance of the State monad; this is called the inner monad. We start by loading the Morph core, monad and transf namespaces.

(use 'blancas.morph.core
     'blancas.morph.monads
     'blancas.morph.transf)

We will again with our now familiar expression evaluator. This time, however, we want both a logging facility and the ability to declare variables and clear the symbol table. Recall that in previous chapters we did one or the other but not both.

writer-t is the constructor for writer transformer; it takes the constructor for the inner monad, the value involved in the computation, and an output value.

(writer-t just {:from "SFO"} "start")  ;; inner monad is a Maybe
;; WriterT Just Pair({:from SFO},start)

eval-writer-t returns the computer value from a WriterT instance; that is, the inner monad.

(def wtrans (writer-t just {:from "SFO"} "start"))  ;; inner monad is a Maybe
(eval-writer-t wtrans)
;; Just {:from SFO}

exec-writer-t returns the accumulated output from a WriterT instance.

(def wtrans (writer-t just {:from "SFO"} "here we go"))  ;; inner monad is a Maybe
(exec-writer-t wtrans)
;; Just here we go

Note that both the computed value and the output are given as boxed values in the inner monad. Client code must then extract these values as appropriate. The reason is that extraction is specific to a monad, while this plumbing is generic.

tell-wt is a WriterT instance that only generates output; it takes the inner monad constructor and the output value. It should be ignored by the computation.

(exec-writer-t (tell-wt just "That's all, folks!"))
;; Just That's all, folks!

The Evaluator

Since monad transformers take care of two monads, it is convenient to write helper functions for creating and getting values off them. The following functions make it easier to work with WriterT instances by using the functions described above. The symbol table is defined after the helper functions.

(defn make-ws
  "Makes an instance of the composed monad WriterT-State."
  ([x] (make-ws x empty-vector))
  ([x out] (writer-t state x out)))

(defn eval-ws
  "Returns the value of the inner monad."
  [m s] (eval-state (eval-writer-t m) s))

(defn exec-ws
  "Returns the final state of the inner monad."
  [m s] (exec-state (eval-writer-t m) s))

(defn get-log
  "Returns the final value of the output in the outer monad."
  [m s] (eval-state (exec-writer-t m) s))

(def table {'DEG 57.295779 'E 2.718281 'PI 3.141592})

The job of function calc is to apply and operator on two operands and log the operation. As before, all functions participating in the WriterT computation must return an instance of that type. Since run does so as well, its values must be unboxed in a monad macro.

(declare run)

(defn calc [op x y log]
  (monad [a (run x) b (run y)]
    (make-ws (op a b) [log])))

Function const looks up symbols in the table with gets as we have seen before. But this time State is an inner monad. In order for the WriterT (outer) monad to deal with this properly we must lift the inner monad up to the outer monad with lift-wt as follows:

(defn const [x]
  (if (symbol? x)
    (lift-wt (gets x)) ;; make State work in the outer monad's environment
    (make-ws x)))

The same situation applies in functions decl and clear for their calls to modify-state and put-state, respectively. These are State functions working where a WriterT is expected. Note the use of >> to sequence the above calls, and ignore their results, with the making of the return value.

(defn decl [x y]
  (>> (lift-wt (modify-state assoc x y))
      (make-ws y)))

(defn clear [x]
  (>> (lift-wt (put-state {}))
      (make-ws x)))

Finally, the run function includes the logging in its calls to calc. The following listing is the complete evaluator.

(defn make-ws
  "Makes an instance of the composed monad WriterT-State."
  ([x] (make-ws x empty-vector))
  ([x out] (writer-t state x out)))

(defn eval-ws
  "Returns the value of the inner monad."
  [m s] (eval-state (eval-writer-t m) s))

(defn exec-ws
  "Returns the final state of the inner monad."
  [m s] (exec-state (eval-writer-t m) s))

(defn get-log
  "Returns the final value of the output in the outer monad."
  [m s] (eval-state (exec-writer-t m) s))

(def table {'DEG 57.295779 'E 2.718281 'PI 3.141592})

(declare run)

(defn calc [op x y log]
  (monad [a (run x) b (run y)]
    (make-ws (op a b) [log])))

(defn const [x]
  (if (symbol? x)
    (lift-wt (gets x))
    (make-ws x)))

(defn decl [x y]
  (>> (lift-wt (modify-state assoc x y))
      (make-ws y)))

(defn clear [x]
  (>> (lift-wt (put-state {}))
      (make-ws x)))

(defn run [op]
  (if (list? op)
    (case (second op)
      + (calc + (first op) (last op) "add")
      - (calc - (first op) (last op) "subtrac")
      * (calc * (first op) (last op) "multiply")
      / (calc / (first op) (last op) "divide")
      = (decl   (first op) (last op))
      % (clear  (first op)))
    (const op)))

We can now try it to verify the change of state and logged output.

(eval-ws (run '((9 / 3) + (2 * (PI - E)))) table)
;; 3.846622
(exec-ws (run '((180 / (k = 30)) + (k * (PI - E)))) table)
;; {PI 3.141592, E 2.718281, DEG 57.295779, k 30}
(exec-ws (run '(((180 %) / (k = 30)) + ((j = 5) * (k - j)))) table)
;; {j 5, k 30}
(get-log (run '((9 / 3) + (2 * ((PI + DEG) - E)))) table)
;; ["divide" "add" "subtrac" "multiply" "add"]