Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Monads for the working class

Monads for the working class

Avatar for Yannick Gladow

Yannick Gladow

June 20, 2017
Tweet

More Decks by Yannick Gladow

Other Decks in Education

Transcript

  1. WTF, why should I care? • make our life easier

    • help us to separate description and implementation • make implementations easily replaceable we will focus on real and easy examples
  2. Example If you want to create a Json with circe1,

    you'll need a typeclass.. // we can't change the user, imagine from a library case class User(name: String, age: Int) // simplification trait Decoder[A] { def toJson(a: A): String } 1 https://github.com/circe/circe
  3. Example val userDecoder = new Decoder[User] { def toJson(user: User):

    String = s"""{ "name" : "${user.name}", "age" : ${user.age}}""" } val me: User = User("Yannick Gladow", 27) userDecoder.toJson(me) // or import io.circe.syntax._ // magic that adds functionality to to our type - simplified me.toJson //res0: String = //"{ // "name" : "Yannick Gladow", // "age" : 27 //}"
  4. Example -- Scala 2.12 -- SAM Types // instead of:

    val userDecoder = new Decoder[User] { def toJson(user: User): String = s"""{ "name" : "${user.name}", "age" : ${user.age}}""" } // Single Abstract Method val userDecoder: Decoder[User] = user => s"""{ "name" : "${user.name}", "age" : ${user.age}}"""
  5. Example case class User(name: String, age: Int) val users1: Map[String,

    Set[User]] = Map("B" -> Set(User("Brian", 19))) val users2: Map[String, Set[User]] = Map("B" -> Set(User("Bob", 33))) users1 ++ users2
  6. Example case class User(name: String, age: Int) val users1: Map[String,

    Set[User]] = Map("B" -> Set(User("Brian", 19))) val users2: Map[String, Set[User]] = Map("B" -> Set(User("Bob", 33))) users1 ++ users2 res0: Map[String,Set[User]] = Map(B -> Set(User(Bob,33))) Where is Brian?
  7. Example How to keep Brian -- the hard way? val

    users1: Map[String, Set[User]] = Map("B" -> Set(User("Brian", 19))) val users2: Map[String, Set[User]] = Map("B" -> Set(User("Bob", 33))) users1.foldLeft(users2) { case(accumulatedUsers, (category, users)) => accumulatedUsers.updated( key = category, value = accumulatedUsers.getOrElse(category, Set.empty) ++ users ) }
  8. Example Whats going on? Monoid Typeclasses! trait Monoid[A] { def

    empty: A def combine(a: A, b: A): A } Adds combination to A
  9. Example Actually two monoids in Action // monoid for Sets

    def setMonoid[A] = new Monoid[Set[A]] { override def empty: Set[A] = Set.empty override def combine(a: Set[A], b: Set[A]): Set[A] = a ++ b } } // monoid for Maps def mapMonoid[K, V](V: Monoid[V]) = new Monoid[Map[K, V]] { def empty: Map[K, V] = Map.empty def combine(map1: Map[K, V], map2: Map[K, V]): Map[K, V] = map1.foldLeft(map2) { case (accumulatedMap, (key, values)) => accumulatedMap.updated( key = key, value = V.combine(values, accumulatedMap.getOrElse(key, V.empty)) ) } }
  10. Example This stuff already exists -- you don't have to

    define it! But we can for our own types now case class SortedUsers(users: List[User]) implicit val sortedByAgeMonoid = new Monoid[SortedUsers] { def empty: SortedUsers = SortedUsers(List.empty) def combine(a: SortedUsers, b: SortedUsers): SortedUsers = SortedUsers((a.users ::: b.users).sortBy(_.age)) } implicit val sortedByLexicographicOrderMonoid = new Monoid[SortedUsers] { def empty: SortedUsers = SortedUsers(List.empty) def combine(a: SortedUsers, b: SortedUsers): SortedUsers = SortedUsers((a.users ::: b.users).sortBy(_.name)) }
  11. Example val yannick = User("Yannick", 27) val brian = User("Brian",

    19) val bob = User("Bob", 33) import cats.implicits._ List( SortedUsers(List(yannick)), SortedUsers(List(bob, brian)) ).combineAll/*(sortedByAgeMonoid)*/ // res0: SortedUsers = SortedUsers(List(User(Brian,19), User(Yannick,27), User(Bob,33))) List( SortedUsers(List(yannick)), SortedUsers(List(bob, brian)) ).combineAll/*(sortedByLexicographicOrderMonoid)*/ // res0: SortedUsers = SortedUsers(List(User(Bob,33), User(Brian,19), User(Yannick,27)))
  12. Conclusion • everything in the background was already defined, no

    work for us • use it for existing stuff, like Map • define it when you want to combine your own types once and get everything else for free • define different versions of your Monoid
  13. Monoid Laws // associativity combine(combine(x, y), z) == combine(x, combine(y,

    z)) // identity combine(x, empty) == x combine(empty, x) == x
  14. Monoid Laws // breaking the law val divisionMonoid = new

    Monoid[Double] { def empty: Double = 1.0 def combine(x: Double, y: Double): Double = x / y } // identity combine(empty, x) == x import divisionMonoid._ combine(empty, 2.0) == 2.0 // res3: Boolean = false
  15. Monoid Laws // breaking the law val divisionMonoid = new

    Monoid[Double] { def empty: Double = 1.0 def combine(x: Double, y: Double): Double = x / y } // associativity combine(combine(x, y), z) == combine(x, combine(y, z)) import divisionMonoid._ combine(combine(2.0, 3.0), 4.0) == combine(2.0 ,combine(3.0, 4.0)) // (2 / 3) / 4 == 2 / (3 / 4) // res3: Boolean = false
  16. A monad is just a monoid in the category of

    endofunctors, what's the probleⅿ2? 2 https://stackoverflow.com/questions/3870088/a-monad-is-just-a-monoid-in-the-category-of-endofunctors-whats-the- proble%E2%85%BF
  17. Representing Failure def getUser(id: Int): Option[User] = ??? val success:

    Option[User] = Some(user) val failure: Option[User] = None
  18. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): User = ??? // db operations
  19. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): User = ??? // db operations val maybeUser1: Option[User] = getUser(1) // how to update this user?
  20. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): User = ??? // db operations val maybeUser1: Option[User] = getUser(1) val updatedUser: Option[User] = maybeUser1.map(user1 => updateUser(user1))
  21. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): User = ??? // db operations
  22. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): Option[User] = ??? // db operations
  23. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): Option[User] = ??? // db operations val maybeUser1: Option[User] = getUser(1) val updatedUser: ??? = maybeUser1.map(user1 => updateUser(user1))
  24. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): Option[User] = ??? // db operations val maybeUser1: Option[User] = getUser(1) val updatedUser: Option[Option[User]] = maybeUser1.map(user1 => updateUser(user1))
  25. // generic definition trait Monad[M[_]] { } // concrete implementation

    for Option val optionMonad = new Monad[Option] { }
  26. trait Monad[M[_]] { def pure[A](a: A): M[A] } val optionMonad

    = new Monad[Option] { def pure[A](a: A): Option[A] = Option(a) }
  27. trait Monad[M[_]] extends Functor[M] { def pure[A](a: A): M[A] def

    map[A, B](a: M[A], f: A => B): M[B] } val optionMonad = new Monad[Option] { def pure[A](a: A): Option[A] = Option(a) def map[A, B](a: Option[A], f: A => B): Option[B] = a match { case None => None case Some(a) => Some(f(a)) } }
  28. trait Monad[M[_]] extends Functor[M] { def pure[A](a: A): M[A] def

    map[A, B](a: M[A], f: A => B): M[B] def flatten[A](a: M[M[A]]): M[A] } val optionMonad = new Monad[Option] { def pure[A](a: A): Option[A] = Option(a) def map[A, B](a: Option[A], f: A => B): Option[B] = ??? def flatten[A](a: Option[Option[A]]): Option[A] = ??? }
  29. val optionMonad = new Monad[Option] { def pure[A](a: A): Option[A]

    = Option(a) def map[A, B](a: Option[A], f: A => B): Option[B] = ??? def flatten[A](a: Option[Option[A]]): Option[A] = a match { case None => None case Some(x/*Option[A]*/) => x } }
  30. trait Monad[M[_]] extends Functor[M] { def pure[A](a: A): M[A] def

    map[A, B](a: M[A], f: A => B): M[B] def flatten[A](a: M[M[A]]): M[A] } val optionMonad = new Monad[Option] { def pure[A](a: A): Option[A] = Option(a) def map[A, B](a: Option[A], f: A => B): Option[B] = ??? def flatten[A](a: Option[Option[A]]): Option[A] = ??? }
  31. trait Monad[M[_]] extends Functor[M] { def pure[A](a: A): M[A] def

    map[A, B](a: M[A], f: A => B): M[B] = ??? def flatMap[A, B](a: M[A], f: A => M[B]): M[B] } val optionMonad = new Monad[Option] { def pure[A](a: A): Option[A] = Option(a) // remember the Db ops def flatMap[A, B](a: Option[A], f: A => Option[B]): Option[B] = ??? }
  32. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): Option[User] = ??? // db operations def getUsersCity(user: User): Option[City] = ??? // db operations val optCity: Option[City] = getUser(1) .flatMap( user1 => updateUser(user1.change) .flatMap(user1Changed => getUsersCity(user1Changed)))
  33. def getUser(id: Int): Option[User] = ??? // db operations def

    updateUser(user: User): Option[User] = ??? // db operations def getUsersCity(user: User): Option[City] = ??? // db operations val optCity: Option[City] = for { user <- getUser(1) updateUser <- updateUser(user.change) city <- getUsersCity(updateUser) } yield city
  34. trait UserRepo[M: Monad] { def getUser(id: Int): M[User] = ???

    // db operations def updateUser(user: User): M[User] = ??? // db operations def getUsersCity(user: User): M[City] = ??? // db operations } // M could be Option val optionMonad: Monad[Option]
  35. trait UserRepo[M: Monad] { def getUser(id: Int): M[User] = ???

    // db operations def updateUser(user: User): M[User] = ??? // db operations def getUsersCity(user: User): M[City] = ??? // db operations }
  36. def updateAndGetCity[M: Monad](repo: UserRepo[M]): M[City] = for { user <-

    repo.getUser(1) updateUser <- repo.updateUser(user.change) city <- repo.getUsersCity(updateUser) } yield city
  37. def updateAndGetCity[M: Monad](repo: UserRepo[M]): M[City] = for { user <-

    repo.getUser(1) updateUser <- repo.updateUser(user.change) city <- repo.getUsersCity(updateUser) } yield city
  38. def updateAndGetCity[M: Monad](repo: UserRepo[M]): M[City] = for { user <-

    repo.getUser(1) updateUser <- repo.updateUser(user.change) city <- repo.getUsersCity(updateUser) } yield city // in production code val city: Future[City] = updateAndGetCity( repo = new UserRepo[Future] { def getUser(id: Int): Future[User] = db.getUser(id) ... } )
  39. def updateAndGetCity[M: Monad](repo: UserRepo[M]): M[City] = for { user <-

    repo.getUser(1) updateUser <- repo.updateUser(user.change) city <- repo.getUsersCity(updateUser) } yield city // in test code val city: Id[City] = updateAndGetCity( repo = new UserRepo[Id] { def getUser(id: Int): Id[User] = User(name = "Pete") ... } )
  40. def f(i: Int): Option[String] = Some(i.toString) val optionMonad: Monad[Option] //Identity

    optionMonad.pure(2).flatMap(f) == f(x) Some("2") == Some("2") Some(2).flatMap(i => optionMonad.pure(i)) == Some(2) Some(2) == Some(2)
  41. def f(i: Int): Option[String] = Some(i.toString) def g(s: String): Option[Char]

    = s.headOption //associativity Option(22).flatMap(f).flatMap(g) == Option(22).flatMap(i => f(i).flatMap(g)) Some('2') == Some('2')
  42. Some(2).flatMap(optionMonad.pure(_)) == Some(2) lazy val printSth: Future[Unit] = Future(println("Hi!")) val

    futureMonad: Monad[Future] printSth // Hi! printSth .flatMap(sideeffect => futureMonad.pure(sideeffect)) // ??? Nothing, future is done, just once
  43. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a while def getUserCity(user: User): Future[City] = ??? def doSomething(user: User, city: City) = ???
  44. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a while def getUserCity(user: User): Future[City] = ??? def doSomething(user: User, city: City) = ??? for { user1 <- getUser(1) city1 <- getUserCity(user1) } yield doSomething(user1, city1)
  45. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a while def getUserCity(id: Int): Future[City] = ??? def doSomething(user: User, city: City) = ???
  46. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a while def getUserCity(id: Int): Future[City] = ??? def doSomething(user: User, city: City) = ??? for { user1 <- getUser(1) city1 <- getUserCity(1) } yield doSomething(user1, city1)
  47. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a 10 seconds def getUserCity(id: Int): Future[City] = ??? // connects to db, takes a 10 seconds def doSomething(user: User, city: City) = ??? for { user1 <- getUser(1) city1 <- getUserCity(1) } yield doSomething(user1, city1)
  48. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a 10 seconds def getUserCity(id: Int): Future[City] = ??? def doSomething(user: User, city: City) = ??? val futUser: Future[User] = getUser(1).map(user => doSomething(user, ???)) val futCity: Future[City] = getUserCity(1).map(city => doSomething(???, city))
  49. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a 10 seconds def getUserCity(id: Int): Future[City] = ??? def doSomething(user: User, city: City) = ??? val futureApplicative: Applicative[Future] futureApplicative .map2(getUser(1), getUserCity(1))(doSomething)
  50. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a 10 seconds def getUserCity(id: Int): Future[City] = ??? def doSomething(user: User, city: City) = ??? (getUser(1) |@| getUserCity(1)).map(doSomething)
  51. def getUser(id: Int): Future[User] = ??? // connects to db,

    takes a while def getUserCity(user: User): Future[City] = ??? def doSomething(user: User, city: City) = ??? Applicative[Future] .map2(getUser(1), getUserCity(???))(doSomething)
  52. Functor on steroids def map [A, B ](fa: F[A]) (f:

    A => B) : F[B] def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C]
  53. trait Applicative[F[_]] { def map2[A,B, C](fa: F[A], fb: F[B])(f: (A,

    B) => C): F[C] def unit[A](a: A): F[A] } new Applicative[Option] { def unit[A](a: A): Option[A] = Option(a) }
  54. trait Applicative[F[_]] { def map2[A,B, C](fa: F[A], fb: F[B])(f: (A,

    B) => C): F[C] def unit[A](a: A): F[A] } new Applicative[Option] { def unit[A](a: A): Option[A] = Option(a) def map2[A, B, C](fa: Option[A], fb: Option[B])( f: (A, B) => C): Option[C] = (fa, fb) match { case (Some(a), Some(b)) => Some(f(a, b)) case _ => None } }
  55. trait Applicative[F[_]] extends Functor[F]{ def map2[A,B, C](fa: F[A], fb: F[B])(f:

    (A, B) => C): F[C] def unit[A](a: A): F[A] // yes we always can define map in terms of map2 def map[A, B](fa: F[A])(f: A => B): F[B] = ??? }
  56. trait Applicative[F[_]] extends Monad[F] { def flatMap[A, B](fa: F[A])(f: (A)

    => F[B]): F[B] = ??? def unit[A](a: A): F[A] = ??? def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = ???
  57. trait Applicative[F[_]] extends Monad[F] { def flatMap[A, B](fa: F[A])(f: (A)

    => F[B]): F[B] = ??? def unit[A](a: A): F[A] = ??? def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = flatMap(fa)(a => map(fb)(b => f(a, b))) }
  58. trait Applicative[F[_]] extends Monad[F] { def flatMap[A, B](fa: F[A])(f: (A)

    => F[B]): F[B] = ??? def unit[A](a: A): F[A] = ??? def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = for { a <- fa b <- fb } yield f(a, b) }
  59. for { a <- fa b <- fb } yield

    f(a, b) for { user1 <- getUser(1) city1 <- getUserCity(1) } yield doSomething(user1, city1) How can this than be parallel?
  60. Why use applicative over monad • special implementation like future

    for parallel • if we use futures flatMap -> no parallel • datatypes that satisfy Applicatives but not monads • Validation
  61. trait Applicative[F[_]] extends Functor[F]{ def map2[A,B, C](fa: F[A], fb: F[B])(f:

    (A, B) => C): F[C] def unit[A](a: A): F[A] def map3 ... // in terms of map2 def map4 ... def map5 ... }