clojuremonadspartial-applicationreader-monad

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


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?


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