Question

Leonardo Borges has put together a fantastic presentation on Monads in Clojure. In it he describes the reader monad in Clojure using the following code:

;; Reader Monad

(def reader-m
  {:return (fn [a]
             (fn [_] a))
   :bind (fn [m k]
           (fn [r]
             ((k (m r)) r)))})

(defn ask  []  identity)
(defn asks [f]
  (fn [env]
    (f env)))

(defn connect-to-db []
  (do-m reader-m
        [db-uri (asks :db-uri)]
        (prn (format "Connected to db at %s" db-uri))))

(defn connect-to-api []
  (do-m reader-m
        [api-key (asks :api-key)
         env (ask)]
        (prn (format "Connected to api with key %s" api-key))))

(defn run-app []
  (do-m reader-m
        [_ (connect-to-db)
         _ (connect-to-api)]
        (prn "Done.")))

((run-app) {:db-uri "user:passwd@host/dbname" :api-key "AF167"})
;; "Connected to db at user:passwd@host/dbname"
;; "Connected to api with key AF167"
;; "Done."

The benefit of this is that you're reading values from the environment in a purely functional way.

But this approach looks very similar to the partial function in Clojure. Consider the following code:

user=> (def hundred-times (partial * 100))
#'user/hundred-times

user=> (hundred-times 5)
500

user=> (hundred-times 4 5 6)
12000

My question is: What is the difference between the reader monad and a partial function in Clojure?

Was it helpful?

Solution

The reader monad is a set of rules we can apply to cleanly compose readers. You could use partial to make a reader, but it doesn't really give us a way to put them together.

For example, say you wanted a reader that doubled the value it read. You might use partial to define it:

(def doubler
  (partial * 2))

You might also want a reader that added one to whatever value it read:

(def plus-oner
  (partial + 1))

Now, suppose you wanted to combine these guys in a single reader that adds their results. You'll probably end up with something like this:

(defn super-reader
  [env]
  (let [x (doubler env)
        y (plus-oner env)]
    (+ x y)))

Notice that you have to explicitly forward the environment to those readers. Total bummer, right? Using the rules provided by the reader monad, we can get much cleaner composition:

(def super-reader
  (do-m reader-m
    [x doubler
     y plus-oner]
    (+ x y)))

OTHER TIPS

You can use partial to "do" the reader monad. Turn let into a do-reader by doing syntactic transformation on let with partial application of the environment on the right-hand side.

(defmacro do-reader
  [bindings & body] 
  (let [env (gensym 'env_)
        partial-env (fn [f] (list `(partial ~f ~env)))
        bindings* (mapv #(%1 %2) (cycle [identity partial-env]) bindings)] 
    `(fn [~env] (let ~bindings* ~@body))))

Then do-reader is to the reader monad as let is to the identity monad (relationship discussed here).

Indeed, since only the "do notation" application of the reader monad was used in Beyamor's answer to your reader monad in Clojure question, the same examples will work as is with m/domonad Reader replaced with do-reader as above.

But, for the sake of variety I'll modify the first example to be just a bit more Clojurish with the environment map and take advantage of the fact that keywords can act as functions.

(def sample-bindings {:count 3, :one 1, :b 2})

(def ask identity)

(def calc-is-count-correct? 
  (do-reader [binding-count :count 
              bindings ask] 
    (= binding-count (count bindings))))

(calc-is-count-correct? sample-bindings)
;=> true

Second example

(defn local [modify reader] (comp reader modify))

(def calc-content-len 
  (do-reader [content ask] 
    (count content)))

(def calc-modified-content-len
  (local #(str "Prefix " %) calc-content-len))

(calc-content-len "12345")
;=> 5

(calc-modified-content-len "12345")
;=> 12

Note since we built on let, we still have destructing at our disposal. Silly example:

(def example1 
  (do-reader [a :foo
              b :bar] 
    (+ a b)))

 (example1 {:foo 2 :bar 40 :baz 800})
 ;=> 42

 (def example2 
   (do-reader [[a b] (juxt :foo :bar)]
     (+ a b)))

(example2 {:foo 2 :bar 40 :baz 800})
;=> 42

So, in Clojure, you can indeed get the functionality of the do notation of reader monad without introducing monads proper. Analagous to doing a ReaderT transform on the identity monad, we can do a syntactic transformation on let. As you surmised, one way to do so is with partial application of the environment.

Perhaps more Clojurish would be to define a reader-> and reader->> to syntactically insert the environment as the second and last argument respectively. I'll leave those as an exercise for the reader for now.

One take-away from this is that while types and type-classes in Haskell have a lot of benefits and the monad structure is a useful idea, not having the constraints of the type system in Clojure allows us to treat data and programs in the same way and do arbitrary transformations to our programs to implement syntax and control as we see fit.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top