1.
Monads in Clojure
Or why burritos shouldn’t be scary [I]
Leonardo Borges
@leonardo_borges
www.leonardoborges.com
www.thoughtworks.com
[I] http://blog.plover.com/prog/burritos.html
Tuesday, 4 March 14
2.
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-i
Tuesday, 4 March 14
3.
Prelude
Type signatures in Haskell
Tuesday, 4 March 14
4.
Type signatures in Haskell
Vectors as maps
import qualified Data.Map as Map -- just giving Data.Map an alias
mkVec :: a -> a -> Map.Map [Char] a -- this is the type signature
This is a function of two
arguments of type a
Tuesday, 4 March 14
5.
Type signatures in Haskell
Vectors as maps
import qualified Data.Map as Map -- just giving Data.Map an alias
mkVec :: a -> a -> Map.Map [Char] a -- this is the type signature
It returns a Map where
keys are of type [Char]
and values are of type a
Tuesday, 4 March 14
6.
Type signatures in Haskell
Vectors as maps
import qualified Data.Map as Map -- just giving Data.Map an alias
mkVec :: 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.
-- using it
myVec = mkVec 10 15
Map.lookup "x" myVec -- Just 10
Tuesday, 4 March 14
7.
Type signatures in Haskell
Multiplication
(*) :: Num a => a -> a -> a
This is also a function of
two arguments of type a
Tuesday, 4 March 14
8.
Type signatures in Haskell
Multiplication
Num a is a type
constraint
(*) :: Num a => a -> a -> a
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
9.
Monads
what on earth are they?
They’re simply an abstraction
...with a funny name to confuse people.
Tuesday, 4 March 14
10.
Monads
what on earth are they?
They are not however....
... a burrito
... an elephant
... a space suit
... or a writing desk.
Tuesday, 4 March 14
11.
Monads
what 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
12.
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
13.
The Monad Type Class
return :: a -> m a
Single argument of type a
Tuesday, 4 March 14
14.
The Monad Type Class
return :: a -> m a
returns a in a “context” m
Tuesday, 4 March 14
15.
The Monad Type Class
return :: a -> m a
This is how we turn plain values into monadic values
Tuesday, 4 March 14
16.
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
17.
The Monad Type Class
(>>=) :: m a -> (a -> m b) -> m b
>>=
Tuesday, 4 March 14
is pronounced bind and takes two arguments
18.
The Monad Type Class
(>>=) :: m a -> (a -> m b) -> m b
A monad
Tuesday, 4 March 14
m a
19.
The Monad Type Class
(>>=) :: m a -> (a -> m b) -> m b
and a function from values of type a
to monads of type m
Tuesday, 4 March 14
b
20.
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
21.
The Monad Type Class
(>>=) :: m a -> (a -> m b) -> m b
and it returns b in a
“context” m
Tuesday, 4 March 14
22.
The Monad Type Class
(>>) :: m a -> m b -> m b
x >> y = x >>= _ -> y
You can work out the type signature all by yourself now :)
Tuesday, 4 March 14
23.
The Monad Type Class
(>>) :: m a -> m b -> m b
x >> y = x >>= _ -> y
>> is pronounced then
and includes a default
implementation in terms of bind
Tuesday, 4 March 14
(>>=)
24.
The Monad Type Class
(>>) :: m a -> m b -> m b
x >> y = x >>= _ -> y
Its second argument to bind is a
function which ignores its argument
Tuesday, 4 March 14
25.
The Monad Type Class
(>>) :: m a -> m b -> m b
x >> y = x >>= _ -> y
It simply returns the value yielded by
“unwrapping” the context of y
Tuesday, 4 March 14
26.
Example I
Calculating 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
27.
Example I
Calculating 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
28.
Example I
Calculating 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
29.
You’ve just seen your ﬁrst monad, the list monad!
Tuesday, 4 March 14
30.
The Monad Type Class *
As if it worked for lists only
class Monad [] where
return :: a -> [a]
(>>=) :: [a] -> (a -> [b]) -> [b]
(>>) :: [a] -> [b] -> [b]
* this isn’t valid Haskell. It’s just to illustrate the concept
Tuesday, 4 March 14
31.
Now let’s learn how it does its thing...
...in Clojure!
Tuesday, 4 March 14
32.
The List Monad
Let’s do it together
(def list-m {
:return (fn [v] ...
:bind
(fn [mv f]
...
})
Tuesday, 4 March 14
)
)
33.
The List Monad
Let’s do it together
(def list-m {
:return (fn [v] (list v))
:bind
(fn [mv f]
...
)
})
Tuesday, 4 March 14
34.
The List Monad
Let’s do it together
(def list-m {
:return (fn [v] (list v))
:bind
(fn [mv f]
(mapcat f mv))
})
Tuesday, 4 March 14
35.
The List Monad
Now 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
36.
The List Monad
Now 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
37.
Macros to the rescue
Haskell’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
38.
Macros to the rescue
Haskell’s do notation in Clojure
Ignore the implementation. You can study it later :)
Tuesday, 4 March 14
39.
The List Monad
Now 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
41.
Example II
Adding two numbers
(defn add [a b]
(+ a b))
(add 1 2) ;; 3
(add 1 nil)
;; NullPointerException
Tuesday, 4 March 14
42.
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
43.
The Maybe Monad
Again, everyone together
(def maybe-m {
:return (fn [v] ...)
:bind (fn [mv f]
...
)
})
Tuesday, 4 March 14
44.
The Maybe Monad
Again, everyone together
(def maybe-m {
:return (fn [v] v)
:bind (fn [mv f]
...
)
})
Tuesday, 4 March 14
45.
The Maybe Monad
Again, everyone together
(def maybe-m {
:return (fn [v] v)
:bind (fn [mv f]
(when mv
(f mv)))
})
Tuesday, 4 March 14
46.
Not too bad, huh? Ready for more?
Tuesday, 4 March 14
47.
Example III
Application conﬁguration
(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
48.
Example III
Application conﬁguration
(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
49.
Example III
Application conﬁguration
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
50.
Example III
Application conﬁguration 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
51.
Example III
Application conﬁguration with the Reader Monad
(defn run-app []
(do-m reader-m
[_ (m-connect-to-db)
_ (m-connect-to-api)]
(prn "Done.")))
Note the env parameter disappeared.
The Reader monad somehow feeds that
into our functions.
((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."
Also this is now a function call as
run-app is now pure
Tuesday, 4 March 14
52.
Example III
Application conﬁguration with the Reader Monad
But, how how does it work?!
Tuesday, 4 March 14
53.
The Reader Monad
Tricky one, but not that tricky
(defn ask [] identity)
(defn asks [f]
(fn [env]
(f env)))
The function names come from
Haskell. They are deﬁned in the
MonadReader type class.
Tuesday, 4 March 14
54.
The Reader Monad
Tricky 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
55.
The Reader Monad
Expanding connect-to-db
This:
(defn connect-to-db []
(do-m reader-m
[db-uri (asks :db-uri)]
(prn (format "Connected to db at %s" db-uri))))
Expands into:
((:bind reader-m)
(asks :db-uri)
(fn* ([db-uri]
((:return reader-m) (prn "Connected to db at " db-uri)))))
Tuesday, 4 March 14
56.
The Reader Monad
Expanding connect-to-db
(def reader-m
{:return (fn [a]
(fn [_] a))
:bind (fn [m k]
(fn [r]
((k (m r)) r)))})
(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"}))
Tuesday, 4 March 14
57.
The Reader Monad
Expanding connect-to-db
(def reader-m
{:return (fn [a]
(fn [_] a))
:bind (fn [m k]
(fn [r]
((k (m r)) r)))})
(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)))
Tuesday, 4 March 14
58.
The Reader Monad
Expanding connect-to-db
(def reader-m
{:return (fn [a]
(fn [_] a))
:bind (fn [m k]
(fn [r]
((k (m r)) r)))})
(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)))
Tuesday, 4 March 14
63.
The Reader Monad
Simple, right? :)
Tuesday, 4 March 14
64.
Example IV
Dependency 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
65.
Example IV
Dependency 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
66.
Example IV
Dependency 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
67.
Example IV
Dependency Injection using the Reader Monad
There’s a couple of ways in
which we can do that
I’ll use protocols as they have
added performance beneﬁts
Tuesday, 4 March 14
68.
Example IV
Dependency 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
69.
Example IV
Dependency 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
70.
Example IV
Dependency Injection using the Reader Monad
With that in mind, let’s rewrite clone!
Tuesday, 4 March 14
71.
Example IV
Dependency Injection using the Reader Monad
It can be helpful to think of clone as having this type signature:
;;
clone! :: Number -> String -> Reader UserRepository Number
(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))))
Tuesday, 4 March 14
72.
Example IV
Dependency Injection using the Reader Monad
And this is how we might use it:
(defn run-clone []
(do-m reader-m
[_ (clone! 10 "leo")]
(prn "Cloned.")))
((run-clone) (mk-user-repo))
;;
;;
;;
;;
;;
Tuesday, 4 March 14
"Fetched user id " 10
"Compute clone for user"
"Create user triggered by " "leo"
"Updated user triggered by " "leo"
"Cloned."
73.
The Reader Monad
A 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 conﬁguration 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
74.
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
Be the first to comment