-
Notifications
You must be signed in to change notification settings - Fork 4
Composing Monads
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!
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"]