Monads in Clojure

of 75 /75
Monads in Clojure Or why burritos shouldn’t be scary [I] [I] http://blog.plover.com/prog/burritos.html Leonardo Borges @leonardo_borges www.leonardoborges.com www.thoughtworks.com Tuesday, 4 March 14

Embed Size (px)

description

Talk given at the Sydney Clojure User group in February 2014

Transcript of Monads in Clojure

  • 1.Monads in Clojure Or why burritos shouldnt 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 monadsThis 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. PreludeType signatures in HaskellTuesday, 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 signatureThis is a function of two arguments of type aTuesday, 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 signatureIt returns a Map where keys are of type [Char] and values are of type aTuesday, 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 10Tuesday, 4 March 14 7. Type signatures in Haskell Multiplication(*) :: Num a => a -> a -> aThis 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 -> aIn 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?Theyre 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 valueTuesday, 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 >>= _ -> yLets understand those type signatures! Tuesday, 4 March 14 13. The Monad Type Class return :: a -> m aSingle argument of type aTuesday, 4 March 14 14. The Monad Type Class return :: a -> m areturns a in a context mTuesday, 4 March 14 15. The Monad Type Class return :: a -> m a This is how we turn plain values into monadic valuesTuesday, 4 March 14 16. The Monad Type Class return :: a -> m a returnis an unfortunate name as it is confusing. It has nothing to do with the return keywords in languages like Java. return is sometimes called unitTuesday, 4 March 14 17. The Monad Type Class (>>=) :: m a -> (a -> m b) -> m b >>=Tuesday, 4 March 14is pronounced bind and takes two arguments 18. The Monad Type Class (>>=) :: m a -> (a -> m b) -> m bA monadTuesday, 4 March 14m a 19. The Monad Type Class (>>=) :: m a -> (a -> m b) -> m band a function from values of type a to monads of type mTuesday, 4 March 14b 20. The Monad Type Class (>>=) :: m a -> (a -> m b) -> m bSomehow each monad knows how to extract values from its context and bind them to the given functionTuesday, 4 March 14 21. The Monad Type Class (>>=) :: m a -> (a -> m b) -> m band it returns b in a context mTuesday, 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 thenand includes a defaultimplementation in terms of bindTuesday, 4 March 14(>>=) 24. The Monad Type Class (>>) :: m a -> m b -> m b x >> y = x >>= _ -> yIts second argument to bind is a function which ignores its argumentTuesday, 4 March 14 25. The Monad Type Class (>>) :: m a -> m b -> m b x >> y = x >>= _ -> yIt simply returns the value yielded by unwrapping the context of yTuesday, 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 monadmeh. Its damn hard to read. And we dont want to type that much. Theres a better wayTuesday, 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 >=) :: [a] -> (a -> [b]) -> [b] (>>) :: [a] -> [b] -> [b]* this isnt valid Haskell. Its just to illustrate the concept Tuesday, 4 March 14 31. Now lets learn how it does its thing... ...in Clojure!Tuesday, 4 March 14 32. The List Monad Lets do it together(def list-m { :return (fn [v] ... :bind (fn [mv f] ... })Tuesday, 4 March 14) ) 33. The List Monad Lets do it together(def list-m { :return (fn [v] (list v)) :bind (fn [mv f] ... ) })Tuesday, 4 March 14 34. The List Monad Lets 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 were 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 were ready to use itugh. Just as ugly. But we know theres a better way. Can we use the do notation in Clojure?Tuesday, 4 March 14 37. Macros to the rescue Haskells 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 Haskells do notation in ClojureIgnore the implementation. You can study it later :)Tuesday, 4 March 14 39. The List Monad Now were 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 40. 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 41. Example II Adding two numbers (defn add [a b] (+ a b)) (add 1 2) ;; 3(add 1 nil) ;; NullPointerExceptionTuesday, 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) ;; nilTuesday, 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 conguration (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 conguration (defn run-app [env] (do (connect-to-db env) (connect-to-api env) "Done."))(run-app {:db-uri "user:[email protected]/dbname" :api-key "AF167"}) ;; "Connected to db at user:[email protected]/dbname" ;; "Connected to api with key AF167" ;; "Done."Tuesday, 4 March 14 49. Example III Application congurationPassing env on to every single function that depends on it can be cumbersome. Obviously we dont want to resort to global vars. Can monads help?Tuesday, 4 March 14 50. Example III Application conguration 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 conguration 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:[email protected]/dbname" :api-key "AF167"}) ;; "Connected to db at user:[email protected]/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 conguration with the Reader MonadBut, 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 dened 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)))})Theres a lot going on in bind. Lets 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:[email protected]/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:[email protected]/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:[email protected]/dbname" :api-key "AF167"}] ((k "user:[email protected]/dbname") r)))Tuesday, 4 March 14 59. 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) nil)))] (let [r {:db-uri "user:[email protected]/dbname" :api-key "AF167"}] ((k "user:[email protected]/dbname") r)))Tuesday, 4 March 14 60. 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] ((fn [a] (fn [_] a) nil)))] (let [r {:db-uri "user:[email protected]/dbname" :api-key "AF167"}] ((k "user:[email protected]/dbname") r)))Tuesday, 4 March 14 61. 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] ((fn [a] (fn [_] a) nil)))] (let [r {:db-uri "user:[email protected]/dbname" :api-key "AF167"}] ((fn [_] nil) r)))Tuesday, 4 March 14 62. 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] ((fn [a] (fn [_] a) nil)))] (let [r {:db-uri "user:[email protected]/dbname" :api-key "AF167"}] nil))Tuesday, 4 March 14 63. The Reader MonadSimple, 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 cant be tested in isolation without resorting to heavy mocking of the involved functions Its 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 MonadLets start by turning our repository into a module that can be injected into our functionTuesday, 4 March 14 67. Example IV Dependency Injection using the Reader MonadTheres a couple of ways in which we can do that Ill use protocols as they have added performance benetsTuesday, 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 MonadRemember: One of the advantages of the reader monad is not having to pass parameters around to every single function that needs itTuesday, 4 March 14 70. Example IV Dependency Injection using the Reader MonadWith that in mind, lets 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 conguration and dependency injection Ive 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 presentationTuesday, 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 dont have the type system to tell you when you stuff up at compile time In such cases, type annotations in comments can be usefulTuesday, 4 March 14 75. Questions? Leonardo Borges @leonardo_borges www.leonardoborges.com www.thoughtworks.comTuesday, 4 March 14