- 1. Run free with the monads! Free Monads for fun and profit @KenScambler #scalamelb March 2014
- 2. The problem • Separation of concerns is paramount to software • In FP, we try to banish effects to the peripheries of our programs • Results and decisions must be represented as data, such as ADTs • Interpretation can happen later • Not super expressive though.
- 3. Decision/Interpretation (tangled) def updateAccount(user: User, db: KVStore): Unit = { val account = db.get(user.id) if (!account.suspended) db.put(user.id, account.updateSpecialOffers) else if (account.abandoned) db.delete(user.id) }
- 4. Decisions as data sealed trait KVSAction case class Put(key: String, value: String) extends KVSAction case class Delete(key: String) extends KVSAction case object NoAction extends KVSAction
- 5. Decision def chooseAction(user: User, account: Account): KVSAction = { if (!account.suspended) Put(user.id, account.updateSpecialOffers) else if (account.abandoned) Delete(user.id) else NoAction }
- 6. Interpretation def interpret(action: KVSAction): Unit = { action match { case Put(key, value) => db.put(key, value) case Delete(key) => db.delete(key) case NoAction => () } } val account = db.get(bob,id) interpret(chooseAction(bob, account))
- 7. How far can we push it? • Can our pure “decision” data be as sophisticated as a program? • Can we create DSLs that can be run later in different ways? • Can we manipulate & rewrite our “program” on the fly? • Conditional logic? • Loops? • Coroutines?
- 8. How far can we push it? def updateAccount(user: User): Unit = for { account <- getAccount(user.id) _ <- when(!account.suspended)( put(user.id, user.updated)) _ <- when(account.abandoned)( delete(user.id)) } yield ()
- 9. The class called “Free” • Free is a data structure • Tree of computations Free[F[_], A]
- 10. The class called “Free” • Free is a data structure • Tree of computations Free[F[_], A]
- 11. The class called “Free” Suspend(F[Free[F,A]]) Return(A) Free[F[_], A]
- 12. The class called “Free” Suspend(F[Free[F,A]]) Return(A) Free[F[_], A]
- 13. The class called “Free” Suspend(F[Free[F,A]]) Return(A) Free[F[_], A]
- 17. Why “free monads”? If F[_] is a functor, Free is a monad…… for free! • This buys us a whole world of existing functionality • Better abstraction • Sequential computations • Elegant imperative-style syntax
- 19. Functors • Functors are things you can map over • F[A] => (A => B) => F[B] trait F[A] { def map(f: A => B): F[B] }
- 20. Functors trait F[A] { def map(f: A => B): F[B] }
- 21. Functors trait F[A] { def map(f: A => B): F[B] }
- 22. Functors trait F[A] { def map(f: A => B): F[B] }
- 23. Monads • Monads have a flatMap method that allows you to chain computations together sequentially class M[A] { def map(f: A => B): M[B] def flatMap(f: A => M[B]): M[B] }
- 24. Monads • Nesting flatmaps allows sequential actions, ignoring the specific context! nbaTeams.flatMap { team => team.players.flatMap { player => player.gamesPlayed.map { game => BasketballCard(team, player, game) } } }
- 25. Monads • Neat comprehension syntax in Scala and Haskell • Makes it look like a regular program for { team <- nbaTeams player <- team.players game <- player.gamesPlayed } yield BasketballCard(team, player, game)
- 26. Back to our regularly scheduled program…
- 27. “Free objects” in maths • Important concept in maths! • Many free structures in Category Theory • Free Monoids, Free Monads, Free Categories, Free Groups, etc • It only counts as “free” if the free thing gets generated in the simplest possible way
- 28. Free Blargles from Fraxblatts • A Fraxblatt is said to generate a Free Blargle if: 1. The Blargle doesn’t contain anything not directly produced from a Fraxblatt 2. The Blargle doesn’t contain anything beyond what it needs to be a Blargle
- 29. Free Blargles from Fraxblatts • A Fraxblatt is said to generate a Free Blargle if: 1. NO JUNK 2. NO NOISE
- 30. Making an honest monad of it case class Return[F[_], A](a: A) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = ??? } • Define flatMap for Return:
- 31. Making an honest monad of it case class Return[F[_], A](a: A) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = f(a) }
- 32. Making an honest monad of it • Define flatMap for Suspend: case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = ??? }
- 33. Making an honest monad of it • We need to map over the functor case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = { F??? map ??? } } F[???]
- 34. Making an honest monad of it • “next” is the only F we have lying around case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = { next map {free => ???} } } F[Free[F, ???]]
- 35. Making an honest monad of it • flatMap is almost the only thing we can do to a Free case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = { next map {free => free.flatMap(???)} } } F[Free[F, ???]]
- 36. Making an honest monad of it • Mapping function f will turn our As into Free[F, B]s case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = { next map {free => free.flatMap(f)} } } F[Free[F, B]]
- 37. Making an honest monad of it • Wrapping in Suspend matches the type signature! case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = { Suspend(next map {free => free.flatMap(f)}) } } Free[F, B]
- 38. Making an honest monad of it • Cleaning up the syntax a bit… case class Suspend[F[_], A](next: F[Free[F,A]]) extends Free[F, A] { def flatMap(f: A => Free[F, B]): Free[F, B] = { Suspend(next map (_ flatMap f)) } }
- 39. Stepping through flatMap Let’s plug in a really simple functor and see what happens. case class Box[A](a: A)
- 40. Stepping through flatMap Let’s plug in a really simple functor and see what happens. case class Box[A](a: A) { def map[B](f: A => B) = Box(f(a)) }
- 41. banana
- 42. Return(banana)
- 49. liftF Let’s automate creating the Suspend cell! F[A] => Free[F, A] =>
- 50. More flatmapping for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c
- 51. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 1
- 52. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 1
- 53. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 1
- 54. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 1
- 55. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 1
- 56. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 2
- 57. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 2
- 58. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 2
- 59. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 2
- 60. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 3
- 61. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 3
- 62. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 3
- 63. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 3
- 64. for { a <- liftF( Box(1) ) b <- liftF( Box(2) ) c <- liftF( Box(3) ) } yield a + b + c 6
- 65. Free[Box, A] • Chain of nothings, resulting in a single value • Not very useful!
- 66. Free[List, A] for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c
- 67. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 3
- 68. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 3
- 69. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 3
- 70. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 3
- 71. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 3
- 72. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 2 4 3 6
- 73. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 2 4 3 6
- 74. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 2 4 3 6
- 75. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c 1 2 2 4 3 6
- 76. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c
- 77. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c
- 78. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c
- 79. for { a <- liftF( List(1,2,3) ) b <- liftF( List(a,a*2) ) c <- liftF( Nil ) } yield a + b + c
- 80. Free[List,A] • Branching tree shape, with data at the leaves • Empty lists can terminate the tree, not just Return. • Again, not super useful. The functor controls the branching factor!
- 81. Funky functions • Functors are not just data structures that hold values • They are computations! • Free’s real power is unleashed when the Functor maps over functions!
- 82. Free[Function0, A] • No-arg functions, basically a lazy value • Flatmapping the free composes functions • Doesn’t actually run any code
- 83. Free[Function0, A] for { a <- liftF(() => 2 + 3) b <- liftF(() => a * 2) c <- liftF(() => a * b) } yield a + b + c
- 84. for { a <- liftF(() => 2 + 3) b <- liftF(() => a * 2) c <- liftF(() => a * b) } yield a + b + c => 2 + 3
- 85. for { a <- liftF(() => 2 + 3) b <- liftF(() => a * 2) c <- liftF(() => a * b) } yield a + b + c 2 + 3=>
- 86. for { a <- liftF(() => 2 + 3) b <- liftF(() => a * 2) c <- liftF(() => a * b) } yield a + b + c 2 + 3=>
- 87. for { a <- liftF(() => 2 + 3) b <- liftF(() => a * 2) c <- liftF(() => a * b) } yield a + b + c => 2 + 3
- 88. for { a <- liftF(() => 2 + 3) b <- liftF(() => a * 2) c <- liftF(() => a * b) } yield a + b + c => => 2 + 3=>
- 89. Trampolines
- 90. Trampolines • Believe it or not, Free[Function0,A] is incredibly useful! • Also known as Trampoline[A] • Moves tail calls onto the heap, avoiding stack overflows • The best we can get for mutual tail recursion on the JVM
- 91. Trampolines • Let’s take a look at some code…
- 92. Now for the power tool
- 93. Little languages • Small, imperative DSLs • Don’t directly do anything, can be interpreted many ways • Functionally pure and type-safe
- 94. A key-value store DSL • A bit like the KVSAction ADT way back at the start • There’s a “type hole” for the next thing • That means…. we can make it a Functor! • Mechanical translation from corresponding API functions
- 95. A key-value store DSL sealed trait KVS[Next] case class Put[Next](key: String, value: String, next: Next) extends KVS[Next] case class Delete[Next](key: String, next: Next) extends KVS[Next] case class Get[Next](key: String, onValue: String => Next) extends KVS[Next]
- 96. A key-value store DSL sealed trait KVS[Next] case class Put[Next](key: String, value: String, next: Next) extends KVS[Next] case class Delete[Next](key: String, next: Next) extends KVS[Next] case class Get[Next](key: String, onValue: String => Next) extends KVS[Next] Just have a slot for the next thing, if we don’t care about a result value
- 97. A key-value store DSL sealed trait KVS[Next] case class Put[Next](key: String, value: String, next: Next) extends KVS[Next] case class Delete[Next](key: String, next: Next) extends KVS[Next] case class Get[Next](key: String, onValue: String => Next) extends KVS[Next] Have a Result => Next function, if we want to “return” some Result.
- 98. Which looks a bit like… def put[A](key: String, value: String): Unit def delete[A](key: String): Unit def get[A](key: String): String
- 99. Which is a bit like… def put[A](key: String, value: String): Unit def delete[A](key: String): Unit def get[A](key: String): String
- 100. A functor for our KVS new Functor[KVS] { def map[A,B](kvs: KVS[A])(f: A => B): KVS[B] = kvs match { case Put(key, value, next) => Put(key, value, f(next)) case Delete(key, next) => Delete(key, f(next)) case Get(key, onResult) => Get(key, onResult andThen f) } }
- 101. A functor for our KVS new Functor[KVS] { def map[A,B](kvs: KVS[A])(f: A => B): KVS[B] = kvs match { case Put(key, value, next) => Put(key, value, f(next)) case Delete(key, next) => Delete(key, f(next)) case Get(key, onResult) => Get(key, onResult andThen f) } } To map over the next value, just apply f
- 102. A functor for our KVS new Functor[KVS] { def map[A,B](kvs: KVS[A])(f: A => B): KVS[B] = kvs match { case Put(key, value, next) => Put(key, value, f(next)) case Delete(key, next) => Delete(key, f(next)) case Get(key, onResult) => Get(key, onResult andThen f) } } To map over a function yielding the next value, compose f with it
- 103. Lifting into the Free Monad def put(key: String, value: String): Free[KVS, Unit] = liftF( Put(key, value, ()) ) def get(key: String): Free[KVS, String] = liftF( Get(key, identity) ) def delete(key: String): Free[KVS, Unit] = liftF( Delete(key, ()) )
- 104. Lifting into the Free Monad def put(key: String, value: String): Free[KVS, Unit] = liftF( Put(key, value, ()) ) def get(key: String): Free[KVS, String] = liftF( Get(key, identity) ) def delete(key: String): Free[KVS, Unit] = liftF( Delete(key, ()) ) Initialise with Unit, when we don’t care about the value
- 105. Lifting into the Free Monad def put(key: String, value: String): Free[KVS, Unit] = liftF( Put(key, value, ()) ) def get(key: String): Free[KVS, String] = liftF( Get(key, identity) ) def delete(key: String): Free[KVS, Unit] = liftF( Delete(key, ()) ) Initialise with the identity function, when we want to return a value
- 106. The payoff
- 107. Composable scripts def modify(key: String, f: String => String): Free[KVS, Unit] = for { v <- get(key) _ <- put(key, f(v)) } yield ()
- 108. Harmless imperative code val script: Free[KVS, Unit] = for { id <- get(“swiss-bank-account-id”) _ <- modify(id, (_ + 1000000)) _ <- put(“bermuda-airport”, “getaway car”) _ <- delete(“tax-records”) } yield ()
- 109. Pure interpreters type KVStore = Map[String, String] def interpretPure(kvs: Free[KVS, Unit], table: KVStore): KVStore = kvs.resume.fold({ case Get(key, onResult) => interpretPure(onResult(table(key)), table) case Put(key, value, next) => interpretPure(next, table + (key -> value)) case Delete(key, next) => interpretPure(next, table - key) }, _ => table)
- 110. Pure interpreters type KVStore = Map[String, String] def interpretPure(kvs: Free[KVS, Unit], table: KVStore): KVStore = kvs.resume.fold({ case Get(key, onResult) => interpretPure(onResult(table(key)), table) case Put(key, value, next) => interpretPure(next, table + (key -> value)) case Delete(key, next) => interpretPure(next, table - key) }, _ => table) KVStore is immutable
- 111. Pure interpreters type KVStore = Map[String, String] def interpretPure(kvs: Free[KVS, Unit], table: KVStore): KVStore = kvs.resume.fold({ case Get(key, onResult) => interpretPure(onResult(table(key)), table) case Put(key, value, next) => interpretPure(next, table + (key -> value)) case Delete(key, next) => interpretPure(next, table - key) }, _ => table) F[Free[F, A]] / A Resume and fold…
- 112. Pure interpreters type KVStore = Map[String, String] def interpretPure(kvs: Free[KVS, Unit], table: KVStore): KVStore = kvs.resume.fold({ case Get(key, onResult) => interpretPure(onResult(table(key)), table) case Put(key, value, next) => interpretPure(next, table + (key -> value)) case Delete(key, next) => interpretPure(next, table - key) }, _ => table) KVS[Free[KVS, Unit]] / Unit Resume and fold…
- 113. Pure interpreters type KVStore = Map[String, String] def interpretPure(kvs: Free[KVS, Unit], table: KVStore): KVStore = kvs.resume.fold({ case Get(key, onResult) => interpretPure(onResult(table(key)), table) case Put(key, value, next) => interpretPure(next, table + (key -> value)) case Delete(key, next) => interpretPure(next, table - key) }, _ => table) When resume finally returns Unit, return the table
- 114. Pure interpreters type KVStore = Map[String, String] def interpretPure(kvs: Free[KVS, Unit], table: KVStore): KVStore = kvs.resume.fold({ case Get(key, onResult) => interpretPure(onResult(table(key)), table) case Put(key, value, next) => interpretPure(next, table + (key -> value)) case Delete(key, next) => interpretPure(next, table - key) }, _ => table)
- 115. Effectful interpreter(s) type KVStore = mutable.Map[String, String] def interpretImpure(kvs: Free[KVS,Unit], table: KVStore): Unit = kvs.go { case Get(key, onResult) => onResult(table(key)) case Put(key, value, next) => table += (key -> value) next case Delete(key, next) => table -= key next }
- 116. Effectful interpreters type KVStore = mutable.Map[String, String] def interpretImpure(kvs: Free[KVS,Unit], table: KVStore): Unit = kvs.go { case Get(key, onResult) => onResult(table(key)) case Put(key, value, next) => table += (key -> value) next case Delete(key, next) => table -= key next } Mutable map
- 117. Effectful interpreters type KVStore = mutable.Map[String, String] def interpretImpure(kvs: Free[KVS,Unit], table: KVStore): Unit = kvs.go { case Get(key, onResult) => onResult(table(key)) case Put(key, value, next) => table += (key -> value) next case Delete(key, next) => table -= key next } def go(f: F[Free[F, A]] => Free[F, A]): A
- 118. Effectful interpreter(s) type KVStore = mutable.Map[String, String] def interpretImpure(kvs: Free[KVS,Unit], table: KVStore): Unit = kvs.go { case Get(key, onResult) => onResult(table(key)) case Put(key, value, next) => table += (key -> value) next case Delete(key, next) => table -= key next }
- 119. How-to summary 1. Fantasy API 2. ADT with type hole for next value 3. Functor definition for ADT 4. Lifting functions 5. Write scripts 6. Interpreter(s)
- 120. Tank game
- 121. Conclusion • Free Monads are really powerful • Separate decisions from interpretation, at a more sophisticated level • Type-safe • Easy to use!
- 122. Conclusion • Express your decisions in a “little language” • Pause and resume programs, co-routine style • Rewrite programs macro-style • Avoid stack overflows with Trampolines This is a great tool to have in your toolkit!
- 123. Further reading • Awodey, Category Theory • Bjarnason, Dead Simple Dependency Injection • Bjarnason, Stackless Scala with Free Monads • Doel, Many roads to Free Monads • Ghosh, A Language and its Interpretation: Learning Free Monads • Gonzalez, Why Free Monads Matter • Haskell.org, Control.Monad.Free • Perrett, Free Monads, Part 1 • Scalaz, scalaz.Free
- 125. Thank you Hope you enjoyed hearing about Free Monads!