1 / 125

Running Free with the Monads

Free Monads are a powerful technique that can separate the representation of programs from the messy details of how they get run. I'll go into the details of how they work, how to use them for fun and profit in your own code, and demonstrate a live Free Monad-driven tank game. Supporting code at https://github.com/kenbot/free

nisamdop
Download Presentation

Running Free with the Monads

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Running Free with the Monads 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. 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) } 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 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 } 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)) 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? 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 () 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] 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]

  14. . Why “free monads”?

  15. . Why “free monads”?

  16. . Why “free monads”?

  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

  18. . Remedial interlude

  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)

  43. . Box(Return(banana))

  44. . Suspend(Box(Return(banana)))

  45. . that.flatMap(banana => Return(banana.peel))

  46. . that.flatMap(banana => Return(banana.peel))

  47. . that.flatMap(banana => Return(banana.peel))

  48. . that.flatMap(banana => Return(banana.peel))

  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

More Related