Download - Monads in Clojure

Transcript
Page 1: Monads in Clojure

Monads in ClojureOr why burritos shouldn’t be scary [I]

[I] http://blog.plover.com/prog/burritos.html

Leonardo Borges@leonardo_borges

www.leonardoborges.comwww.thoughtworks.com

Tuesday, 4 March 14

Page 2: Monads in Clojure

Monads in Clojure

The plan:

Type Signatures in Haskell The Monad Type Class

A few useful monads

This presentation is a super condensed version of my blog post series on monads [II]

[II] http://bit.ly/monads-part-iTuesday, 4 March 14

Page 3: Monads in Clojure

Prelude

Type signatures in Haskell

Tuesday, 4 March 14

Page 4: Monads in Clojure

Type signatures in Haskell

import qualified Data.Map as Map -- just giving Data.Map an aliasmkVec :: a -> a -> Map.Map [Char] a -- this is the type signature

Vectors as maps

This is a function of two arguments of type a

Tuesday, 4 March 14

Page 5: Monads in Clojure

Type signatures in Haskell

import qualified Data.Map as Map -- just giving Data.Map an aliasmkVec :: a -> a -> Map.Map [Char] a -- this is the type signature

Vectors as maps

It returns a Map where keys are of type [Char] and values are of type a

Tuesday, 4 March 14

Page 6: Monads in Clojure

Type signatures in Haskell

import qualified Data.Map as Map -- just giving Data.Map an aliasmkVec :: a -> a -> Map.Map [Char] a -- this is the type signature

mkVec x y = Map.fromList [("x", x), ("y", y)] -- this is the implementation. You can ignore this part.

Vectors as maps

-- using itmyVec = mkVec 10 15Map.lookup "x" myVec -- Just 10

Tuesday, 4 March 14

Page 7: Monads in Clojure

Type signatures in HaskellMultiplication

(*) :: Num a => a -> a -> a

This is also a function of two arguments of type a

Tuesday, 4 March 14

Page 8: Monads in Clojure

Type signatures in HaskellMultiplication

(*) :: Num a => a -> a -> a

Num a is a type constraint

In this case, the type constraint tells the compiler that (*)works on any value a as long as a is an

instance of Num

Tuesday, 4 March 14

Page 9: Monads in Clojure

Monadswhat on earth are they?

They’re simply an abstraction

...with a funny name to confuse people.

Tuesday, 4 March 14

Page 10: Monads in Clojure

Monadswhat on earth are they?

They are not however....... a burrito

... an elephant

... a space suit... or a writing desk.

Tuesday, 4 March 14

Page 11: Monads in Clojure

Monadswhat on earth are they?

Every monad has an associated “context”

It can be useful to think of them as “wrapping” some value

Tuesday, 4 March 14

Page 12: Monads in Clojure

The Monad Type Class

class Monad m where return :: a -> m a

(>>=) :: m a -> (a -> m b) -> m b

(>>) :: m a -> m b -> m b x >> y = x >>= \_ -> y

Let’s understand those type signatures!

Tuesday, 4 March 14

Page 13: Monads in Clojure

The Monad Type Class

return :: a -> m a

Single argument of type a

Tuesday, 4 March 14

Page 14: Monads in Clojure

The Monad Type Class

return :: a -> m a

returns a in a “context” m

Tuesday, 4 March 14

Page 15: Monads in Clojure

The Monad Type Class

return :: a -> m a

This is how we turn plain values into monadic values

Tuesday, 4 March 14

Page 16: Monads in Clojure

The Monad Type Class

return :: a -> m a

return is an unfortunate name as it is confusing. It has nothing to do with the return keywords in languages like Java. return is sometimes called unit

Tuesday, 4 March 14

Page 17: Monads in Clojure

The Monad Type Class

(>>=) :: m a -> (a -> m b) -> m b

>>= is pronounced bind and takes two arguments

Tuesday, 4 March 14

Page 18: Monads in Clojure

The Monad Type Class

(>>=) :: m a -> (a -> m b) -> m b

A monad m a

Tuesday, 4 March 14

Page 19: Monads in Clojure

The Monad Type Class

(>>=) :: m a -> (a -> m b) -> m b

and a function from values of type a to monads of type m b

Tuesday, 4 March 14

Page 20: Monads in Clojure

The Monad Type Class

(>>=) :: m a -> (a -> m b) -> m b

Somehow each monad knows how to extract values from its context and “bind” them to the given function

Tuesday, 4 March 14

Page 21: Monads in Clojure

The Monad Type Class

(>>=) :: m a -> (a -> m b) -> m b

and it returns b in a “context” m

Tuesday, 4 March 14

Page 22: Monads in Clojure

The Monad Type Class

(>>) :: m a -> m b -> m bx >> y = x >>= \_ -> y

You can work out the type signature all by yourself now :)

Tuesday, 4 March 14

Page 23: Monads in Clojure

The Monad Type Class

(>>) :: m a -> m b -> m bx >> y = x >>= \_ -> y

>> is pronounced then and includes a default implementation in terms of bind (>>=)

Tuesday, 4 March 14

Page 24: Monads in Clojure

The Monad Type Class

(>>) :: m a -> m b -> m bx >> y = x >>= \_ -> y

Its second argument to bind is a function which ignores its argument

Tuesday, 4 March 14

Page 25: Monads in Clojure

The Monad Type Class

(>>) :: m a -> m b -> m bx >> y = x >>= \_ -> y

It simply returns the value yielded by “unwrapping” the context of y

Tuesday, 4 March 14

Page 26: Monads in Clojure

Example ICalculating all combinations between elements of two lists using the

list monad

combinations :: [(Int,Int)] combinations = [1, 2, 3] >>= \a -> [4, 5] >>= \b -> return (a,b)

-- [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

Tuesday, 4 March 14

Page 27: Monads in Clojure

Example ICalculating all combinations between elements of two lists using the

list monad

meh. It’s damn hard to read. And we don’t want to type that much.

There’s a better way

Tuesday, 4 March 14

Page 28: Monads in Clojure

Example ICalculating all combinations between elements of two lists using the

list monad (using do notation)

combinations' :: [(Int,Int)] combinations' = do a <- [1, 2, 3] b <- [4, 5] return (a,b)

-- [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

Tuesday, 4 March 14

Page 29: Monads in Clojure

You’ve just seen your first monad, the list monad!

Tuesday, 4 March 14

Page 30: Monads in Clojure

The Monad Type Class *

class Monad [] where return :: a -> [a]

(>>=) :: [a] -> (a -> [b]) -> [b]

(>>) :: [a] -> [b] -> [b]

As if it worked for lists only

* this isn’t valid Haskell. It’s just to illustrate the conceptTuesday, 4 March 14

Page 31: Monads in Clojure

Now let’s learn how it does its thing......in Clojure!

Tuesday, 4 March 14

Page 32: Monads in Clojure

The List MonadLet’s do it together

(def list-m { :return (fn [v] ... ) :bind (fn [mv f] ... ) })

Tuesday, 4 March 14

Page 33: Monads in Clojure

The List MonadLet’s do it together

(def list-m { :return (fn [v] (list v)) :bind (fn [mv f] ... ) })

Tuesday, 4 March 14

Page 34: Monads in Clojure

The List MonadLet’s do it together

(def list-m { :return (fn [v] (list v)) :bind (fn [mv f] (mapcat f mv)) })

Tuesday, 4 March 14

Page 35: Monads in Clojure

The List MonadNow we’re ready to use it

(defn combinations [] (let [bind (:bind list-m) return (:return list-m)] (bind [1 2 3] (fn [a] (bind [4 5] (fn [b] (return [a b])))))))

;; ([1 4] [1 5] [2 4] [2 5] [3 4] [3 5])

Tuesday, 4 March 14

Page 36: Monads in Clojure

The List MonadNow we’re ready to use it

ugh. Just as ugly.But we know there’s a better way.

Can we use the do notation in Clojure?

Tuesday, 4 March 14

Page 37: Monads in Clojure

Macros to the rescueHaskell’s do notation in Clojure

(defn m-steps [m [name val & bindings] body] (if (seq bindings) `(-> ~val ((:bind ~m) (fn [~name] ~(m-steps m bindings body)))) `(-> ~val ((:bind ~m) (fn [~name] ((:return ~m) ~body))))))

(defmacro do-m [m bindings body] (m-steps m bindings body))

Tuesday, 4 March 14

Page 38: Monads in Clojure

Macros to the rescueHaskell’s do notation in Clojure

Ignore the implementation. You can study it later :)

Tuesday, 4 March 14

Page 39: Monads in Clojure

The List MonadNow we’re ready to use it

(defn combinations [] (do-m list-m [a [1 2 3] b [4 5]] [a b]))

;; ([1 4] [1 5] [2 4] [2 5] [3 4] [3 5])

Tuesday, 4 March 14

Page 40: Monads in Clojure

Familiar?List comprehension

(defn combinations [] (for [a [1 2 3] b [4 5]] [a b]))

;; ([1 4] [1 5] [2 4] [2 5] [3 4] [3 5])

Tuesday, 4 March 14

Page 41: Monads in Clojure

Example IIAdding two numbers

(defn add [a b] (+ a b))

(add 1 2) ;; 3

(add 1 nil)

;; NullPointerException

Tuesday, 4 March 14

Page 42: Monads in Clojure

Example II[Maybe]Adding two numbers

(defn m-add [ma mb] (do-m maybe-m [a ma b mb] (+ a b)))

(m-add 1 2) ;; 3

(m-add nil 1) ;; nil

Tuesday, 4 March 14

Page 43: Monads in Clojure

The Maybe MonadAgain, everyone together

(def maybe-m { :return (fn [v] ...) :bind (fn [mv f] ... ) })

Tuesday, 4 March 14

Page 44: Monads in Clojure

The Maybe MonadAgain, everyone together

(def maybe-m { :return (fn [v] v) :bind (fn [mv f] ... ) })

Tuesday, 4 March 14

Page 45: Monads in Clojure

The Maybe MonadAgain, everyone together

(def maybe-m { :return (fn [v] v) :bind (fn [mv f] (when mv (f mv))) })

Tuesday, 4 March 14

Page 46: Monads in Clojure

Not too bad, huh? Ready for more?

Tuesday, 4 March 14

Page 47: Monads in Clojure

Example IIIApplication configuration

(defn connect-to-db [env] (let [db-uri (:db-uri env)] (prn (format "Connected to db at %s" db-uri))))

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

Tuesday, 4 March 14

Page 48: Monads in Clojure

Example IIIApplication configuration

(defn run-app [env] (do (connect-to-db env) (connect-to-api env) "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."

Tuesday, 4 March 14

Page 49: Monads in Clojure

Example IIIApplication configuration

Passing env on to every single function that depends on it can be cumbersome.

Obviously we don’t want to resort to global vars. Can monads help?

Tuesday, 4 March 14

Page 50: Monads in Clojure

Example IIIApplication configuration with the Reader Monad

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

Bear with me for now. These will be explained soon.

Tuesday, 4 March 14

Page 51: Monads in Clojure

Example IIIApplication configuration with the Reader Monad

(defn run-app [] (do-m reader-m [_ (m-connect-to-db) _ (m-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."

Note the env parameter disappeared.

The Reader monad somehow feeds that into our functions.

Also this is now a function call as run-app is now pure

Tuesday, 4 March 14

Page 52: Monads in Clojure

Example IIIApplication configuration with the Reader Monad

But, how how does it work?!

Tuesday, 4 March 14

Page 53: Monads in Clojure

The Reader MonadTricky one, but not that tricky

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

The function names come from Haskell. They are defined in the MonadReader type class.

Tuesday, 4 March 14

Page 54: Monads in Clojure

The Reader MonadTricky one, but not that tricky

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

There’s a lot going on in bind. Let’s unravel it.

Tuesday, 4 March 14

Page 55: Monads in Clojure

The Reader MonadExpanding connect-to-db

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

This:

((:bind reader-m) (asks :db-uri) (fn* ([db-uri] ((:return reader-m) (prn "Connected to db at " db-uri)))))

Expands into:

Tuesday, 4 March 14

Page 56: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((:return reader-m) (prn "Connected to db at " db-uri))))] ((fn [r] ((k (m r)) r)) {:db-uri "user:passwd@host/dbname" :api-key "AF167"}))

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

Tuesday, 4 March 14

Page 57: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((:return reader-m) (prn "Connected to db at " db-uri))))] (let [r {:db-uri "user:passwd@host/dbname" :api-key "AF167"}] ((k (m r)) r)))

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

Tuesday, 4 March 14

Page 58: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((:return reader-m) (prn "Connected to db at " db-uri))))] (let [r {:db-uri "user:passwd@host/dbname" :api-key "AF167"}] ((k "user:passwd@host/dbname") r)))

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

Tuesday, 4 March 14

Page 59: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((:return reader-m) nil)))] (let [r {:db-uri "user:passwd@host/dbname" :api-key "AF167"}] ((k "user:passwd@host/dbname") r)))

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

Tuesday, 4 March 14

Page 60: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((fn [a] (fn [_] a) nil)))] (let [r {:db-uri "user:passwd@host/dbname" :api-key "AF167"}] ((k "user:passwd@host/dbname") r)))

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

Tuesday, 4 March 14

Page 61: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((fn [a] (fn [_] a) nil)))] (let [r {:db-uri "user:passwd@host/dbname" :api-key "AF167"}] ((fn [_] nil) r)))

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

Tuesday, 4 March 14

Page 62: Monads in Clojure

The Reader MonadExpanding connect-to-db

(let [m (fn [env] (:db-uri env)) k (fn* ([db-uri] ((fn [a] (fn [_] a) nil)))] (let [r {:db-uri "user:passwd@host/dbname" :api-key "AF167"}] nil))

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

Tuesday, 4 March 14

Page 63: Monads in Clojure

The Reader Monad

Simple, right? :)

Tuesday, 4 March 14

Page 64: Monads in Clojure

Example IVDependency Injection

(require '[app.repository :as repository])

(defn clone! [id user] (let [old-user (repository/fetch-by-id id {}) cloned-user (repository/create! (compute-clone old-user) user) updated-user (assoc old-user :clone-id (:id cloned-user))] (repository/update! old-user updated-user user)))

See the problem?

Tuesday, 4 March 14

Page 65: Monads in Clojure

Example IVDependency Injection: the problem

• clone! is tightly coupled to the repository namespace• It can’t be tested in isolation without resorting to heavy mocking of the involved functions• It’s limited to a single repository implementation - makes experimenting at the REPL harder• ...

Basically, good software engineering principles apply

Tuesday, 4 March 14

Page 66: Monads in Clojure

Example IVDependency Injection using the Reader Monad

Let’s start by turning our repository into a module that can

be injected into our function

Tuesday, 4 March 14

Page 67: Monads in Clojure

Example IV

There’s a couple of ways in which we can do that

I’ll use protocols as they have added performance benefits

Dependency Injection using the Reader Monad

Tuesday, 4 March 14

Page 68: Monads in Clojure

Example IVDependency Injection using the Reader Monad

(defprotocol UserRepository (fetch-by-id! [this id]) (create! [this user username]) (update! [this old-user new-user username]))

(defn mk-user-repo [] (reify UserRepository (fetch-by-id! [this id] (prn "Fetched user id " id)) (create! [this user username] (prn "Create user triggered by " username)) (update! [this old-user new-user username] (prn "Updated user triggered by " username))))

Tuesday, 4 March 14

Page 69: Monads in Clojure

Example IVDependency Injection using the Reader Monad

Remember: One of the advantages of the reader monad is not having to pass parameters around to

every single function that needs it

Tuesday, 4 March 14

Page 70: Monads in Clojure

Example IVDependency Injection using the Reader Monad

With that in mind, let’s rewrite clone!

Tuesday, 4 March 14

Page 71: Monads in Clojure

Example IVDependency Injection using the Reader Monad

(defn clone! [id user] (do-m reader-m [repo (ask)] (let [old-user (fetch-by-id! repo id) cloned-user (create! repo (compute-clone old-user) user) updated-user (assoc old-user :clone-id (:id cloned-user))] (update! repo old-user updated-user user))))

;; clone! :: Number -> String -> Reader UserRepository Number

It can be helpful to think of clone as having this type signature:

Tuesday, 4 March 14

Page 72: Monads in Clojure

Example IVDependency Injection using the Reader Monad

(defn run-clone [] (do-m reader-m [_ (clone! 10 "leo")] (prn "Cloned.")))

((run-clone) (mk-user-repo))

And this is how we might use it:

;; "Fetched user id " 10;; "Compute clone for user";; "Create user triggered by " "leo";; "Updated user triggered by " "leo";; "Cloned."

Tuesday, 4 March 14

Page 73: Monads in Clojure

The Reader MonadA few observations from a clojure perspective

• Useful abstraction for computations that read values from a shared environment• As we saw, it has multiple use cases such as configuration and dependency injection• I’ve cheated in the examples:• Each function using the reader monad also performed IO - via prn• In Haskell, IO is a monad so we would need to use monad transformers to compose them - beyond the scope of this presentation

Tuesday, 4 March 14

Page 74: Monads in Clojure

Monads: Summary

• Lots of useful abstractions through a common interface• Some monads, such as Maybe and Either, are a lot more powerful in a statically typed language like Haskell or Scala due to its usage being encoded in the type system, as well as native support to pattern matching• Others, such as the Reader monad, can be very useful in a dynamic setting - though you don’t have the type system to tell you when you stuff up at compile time• In such cases, type annotations in comments can be useful

Tuesday, 4 March 14

Page 75: Monads in Clojure

Questions?Leonardo Borges

@leonardo_borgeswww.leonardoborges.comwww.thoughtworks.com

Tuesday, 4 March 14