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

Monads for the working class

Monads for the working class

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 ... }