Final tagless and cats-mtl
Dublin 2019
Alexander Zaidel
Agenda
Examples
Examples
Examples
2
3
def validateUser(userToValidate: User): User = {
validateAge(userToValidate)
validatePassword(userToValidate)
}
4
private def validateAge(user: User): User =
if(user.age >= 18) user else throw new Exception("Age is not valid")
private def validatePassword(user: User): User =
if(user.password.length >= 6) user else throw new Exception("Password is not valid")
5
Use Option?
def validateUser(userToValidate: User): Option[User] = for {
_ <- validateAge(userToValidate)
_ <- validatePassword(userToValidate)
} yield userToValidate
6
private def validateAge(user: User): Option[User] =
if(user.age >= 18) Some(user) else None
private def validatePassword(user: User): Option[User] =
if(user.password.length >= 6) Some(user) else None
7
Use Try?
def validateUser(userToValidate: User): Try[User] = for {
_ <- validateAge(userToValidate)
_ <- validatePassword(userToValidate)
} yield userToValidate
8
private def validateAge(user: User): Try[User] =
if(user.age >= 18) Success(user) else Failure(new Exception("Age is not valid"))
private def validatePassword(user: User): Try[User] =
if(user.password.length >= 6) Success(user) else Failure(new Exception("Password is not
valid"))
9
Use Either?
def validateUser(userToValidate: User): Either[String, User] = for {
_ <- validateAge(userToValidate)
_ <- validatePassword(userToValidate)
} yield userToValidate
10
private def validateAge(user: User): Either[String, User] =
if(user.age >= 18) Right(user) else Left("Age is not valid")
private def validatePassword(user: User): Either[String, User] =
if(user.password.length >= 6) Right(user) else Left("Password is not valid")
11
def validateUser(userToValidate: User): F[User] = for {
_ <- validateAge(userToValidate)
_ <- validatePassword(userToValidate)
} yield userToValidate
12
private def validateAge(user: User): F[User] =
if(user.age >= 18) wrap[F](user) else error[F]
private def validatePassword(user: User): F[User] =
if(user.password.length >= 6) wrap[F](user) else error[F]
13
private def validateAge(user: User): F[User] =
if(user.age >= 18) pure[F](user) else raiseError[F]
private def validatePassword(user: User): F[User] =
if(user.password.length >= 6) pure[F](user) else raiseError[F]
14
MonadError
trait MonadError[F[_], E] extends ApplicativeError[F, E] with Monad[F] {
def raiseError[A](e: E): F[A]
def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]
def pure[A](x: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B]
}
15
type ValidationError[F[_]] = MonadError[F, String]
class Validator[F[_]: ValidationError] {
val mE = implicitly[ValidationError[F]]
def validateUser(userToValidate: User): F[User] = for {
_ <- validateAge(userToValidate)
_ <- validatePassword(userToValidate)
} yield userToValidate
16
private def validateAge(user: User): F[User] =
if (user.age >= 18) mE.pure(user) else mE.raiseError("Age is not valid")
private def validatePassword(user: User): F[User] =
if (user.password.length >= 6) mE.pure(user) else mE.raiseError("Password is not valid")
}
17
private def validateAge(user: User): F[User] =
mE.ensure[User](mE.pure(user))("Age is not valid")(_.age >= 18)
private def validatePassword(user: User): F[User] =
mE.ensure(mE.pure(user))("Password is not valid")(u => u.password.length >= 6)
}
18
implicit val monadErrorOpt = new MonadError[Option, String] {
override def raiseError[A](e: String): Option[A] = None
override def handleErrorWith[A](fa: Option[A])(f: String => Option[A]): Option[A] =
fa match {
case r@Some(_) => r
case None => None
}
override def pure[A](x: A): Option[A] = Some(x)
19
override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = for {
a <- fa
b <- f(a)
} yield b
@tailrec
override def tailRecM[A, B](a: A)(f: A => Option[Either[A, B]]): Option[B] = f(a) match {
case None => None
case Some(Left(a1)) => tailRecM(a1)(f)
case Some(Right(b)) => Some(b)
}
} 20
val validatorOpt = new Validator[Option]()
validatorOpt.validateUser(User("user1", 0, "strong"))
val validatorEither = new Validator[Either[String, ?]]()
validatorEither.validateUser(User("user2", 18, "weak"))
21
Learning scala
22
case class CreditCard(balance: Long)
sealed abstract class Item(valprice: Long)
case object ScalaCourse extends Item(100)
case object RedBook extends Item(200)
case object ScalaDaysTickets extends Item(1000)
23
def learnScala(creditCard: CreditCard): Unit = {
val (creditCard1, courseraScalaCourse) = buyCourseraSubscription(creditCard)
val (creditCard2, scalaRedBook) = buyRedBook(creditCard1)
val (creditCard3, scaladaysTickets) = buyScalaDaysTickets(creditCard1)
postTweet(List(courseraScalaCourse,
scalaRedBook,
scaladaysTickets), creditCard3)
}
24
case class State[S, A](run: S => (S, A)) {
def flatMap[B](f: A => State[S, B]): State[S, B] = State[S, B] { s0 =>
val (s1, a) = run(s0)
f(a).run(s1)
}
def map[B](f: A => B): State[S, B] = State[S, B] { s0 =>
val (s1, a) = run(s0)
s1 -> f(a)
}
}
25
def buyCourseraSubscription(creditCard: CreditCard): (CreditCard, Item) = {
creditCard.copy(balance = creditCard.balance - ScalaCourse.price) -> ScalaCourse
}
def buyRedBook(creditCard: CreditCard): (CreditCard, Item) = {
creditCard.copy(balance = creditCard.balance - RedBook.price) -> RedBook
}
def buyScalaDaysTickets(creditCard: CreditCard): (CreditCard, Item) = {
creditCard.copy(balance = creditCard.balance - ScalaDaysTickets.price) -> ScalaDaysTickets
}
26
def buyCourseraSubscription() = State[CreditCard, Item](creditCard =>
creditCard.copy(balance = creditCard.balance - ScalaCourse.price) -> ScalaCourse
)
def buyRedBook() = State[CreditCard, Item](creditCard =>
creditCard.copy(balance = creditCard.balance - RedBook.price) -> RedBook
)
def buyScalaDaysTickets() = State[CreditCard, Item](creditCard =>
creditCard.copy(balance = creditCard.balance - ScalaDaysTickets.price) -> ScalaDaysTickets
)
27
def learnScala(): State[CreditCard, Unit] = for {
scalaCourse <- buyCourseraSubscription()
redBook <- buyRedBook()
scalaDaysTickets <- buyScalaDaysTickets()
creditCard <- State.get
} yield postTweet(List(scalaCourse,
redBook,
scalaDaysTickets), creditCard)
28
def postTweet(items: List[Item], creditCard: CreditCard): Unit =
println(s"Check this out ${items.mkString(",")}. And I have ${creditCard.balance} left on my
account")
def postTweetIO(items: List[Item], creditCard: CreditCard): IO[Unit] =
IO(postTweet(items, creditCard))
29
def learnScalaIO(): StateT[IO, CreditCard, Unit] = for {
scalaCourse <- buyCourseraSubscription().mapK(evalToIO)
redBook <- buyRedBook().mapK(evalToIO)
scalaDaysTickets <- buyScalaDaysTickets().mapK(evalToIO)
creditCard <- StateT.get[IO, CreditCard]
_ <- StateT.liftF[IO, CreditCard, Unit](postTweetIO(List(scalaCourse,
redBook,
scalaDaysTickets), creditCard))
} yield ()
30
type State[S, A] = StateT[Eval, S, A]
val evalToIO = new arrow.FunctionK[Eval, IO] {
override def apply[A](fa: Eval[A]): IO[A] = IO(fa.value)
}
31
MonadState
trait MonadState[F[_], S] extends Serializable {
val monad: Monad[F]
def get: F[S]
def set(s: S): F[Unit]
def inspect[A](f: S => A): F[A]
def modify(f: S => S): F[Unit]
}
32
type CCState[F[_]] = MonadState[F, CreditCard]
class RoadToMonads[F[_] : CCState : Monad]() {
val ms = implicitly[CCState[F]]; val m = implicitly[Monad[F]]
def learnScala(): F[Unit] = for {
scalaCourse <- buyCourseraSubscription()
redBook <- buyRedBook()
scalaDaysTickets <- buyScalaDaysTickets()
creditCard <- ms.get
} yield postTweet(List(scalaCourse, redBook, scalaDaysTickets), creditCard)
33
private def postTweet(items: List[Item], creditCard: CreditCard): F[Unit] =
m.pure(println(s"Check this out ${items.mkString(",")}. And I have ${creditCard.balance} left on my
account"))
34
private def buyCourseraSubscription(): F[Item] =
ms.modify(cc => cc.copy(balance = cc.balance - ScalaCourse.price))
.map(_ => ScalaCourse)
private def buyRedBook(): F[Item] =
ms.modify(cc => cc.copy(balance = cc.balance - RedBook.price))
.map(_ => RedBook)
private def buyScalaDaysTickets(): F[Item] =
ms.modify(cc => cc.copy(balance = cc.balance - ScalaDaysTickets.price))
.map(_ => ScalaDaysTickets)
}
35
Wanna go crazy?
36
So why should I use MTL?
37
Q/A
38

Final tagless and cats mtl

  • 1.
    Final tagless andcats-mtl Dublin 2019 Alexander Zaidel
  • 2.
  • 3.
  • 4.
    def validateUser(userToValidate: User):User = { validateAge(userToValidate) validatePassword(userToValidate) } 4
  • 5.
    private def validateAge(user:User): User = if(user.age >= 18) user else throw new Exception("Age is not valid") private def validatePassword(user: User): User = if(user.password.length >= 6) user else throw new Exception("Password is not valid") 5
  • 6.
    Use Option? def validateUser(userToValidate:User): Option[User] = for { _ <- validateAge(userToValidate) _ <- validatePassword(userToValidate) } yield userToValidate 6
  • 7.
    private def validateAge(user:User): Option[User] = if(user.age >= 18) Some(user) else None private def validatePassword(user: User): Option[User] = if(user.password.length >= 6) Some(user) else None 7
  • 8.
    Use Try? def validateUser(userToValidate:User): Try[User] = for { _ <- validateAge(userToValidate) _ <- validatePassword(userToValidate) } yield userToValidate 8
  • 9.
    private def validateAge(user:User): Try[User] = if(user.age >= 18) Success(user) else Failure(new Exception("Age is not valid")) private def validatePassword(user: User): Try[User] = if(user.password.length >= 6) Success(user) else Failure(new Exception("Password is not valid")) 9
  • 10.
    Use Either? def validateUser(userToValidate:User): Either[String, User] = for { _ <- validateAge(userToValidate) _ <- validatePassword(userToValidate) } yield userToValidate 10
  • 11.
    private def validateAge(user:User): Either[String, User] = if(user.age >= 18) Right(user) else Left("Age is not valid") private def validatePassword(user: User): Either[String, User] = if(user.password.length >= 6) Right(user) else Left("Password is not valid") 11
  • 12.
    def validateUser(userToValidate: User):F[User] = for { _ <- validateAge(userToValidate) _ <- validatePassword(userToValidate) } yield userToValidate 12
  • 13.
    private def validateAge(user:User): F[User] = if(user.age >= 18) wrap[F](user) else error[F] private def validatePassword(user: User): F[User] = if(user.password.length >= 6) wrap[F](user) else error[F] 13
  • 14.
    private def validateAge(user:User): F[User] = if(user.age >= 18) pure[F](user) else raiseError[F] private def validatePassword(user: User): F[User] = if(user.password.length >= 6) pure[F](user) else raiseError[F] 14
  • 15.
    MonadError trait MonadError[F[_], E]extends ApplicativeError[F, E] with Monad[F] { def raiseError[A](e: E): F[A] def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A] def pure[A](x: A): F[A] def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B] } 15
  • 16.
    type ValidationError[F[_]] =MonadError[F, String] class Validator[F[_]: ValidationError] { val mE = implicitly[ValidationError[F]] def validateUser(userToValidate: User): F[User] = for { _ <- validateAge(userToValidate) _ <- validatePassword(userToValidate) } yield userToValidate 16
  • 17.
    private def validateAge(user:User): F[User] = if (user.age >= 18) mE.pure(user) else mE.raiseError("Age is not valid") private def validatePassword(user: User): F[User] = if (user.password.length >= 6) mE.pure(user) else mE.raiseError("Password is not valid") } 17
  • 18.
    private def validateAge(user:User): F[User] = mE.ensure[User](mE.pure(user))("Age is not valid")(_.age >= 18) private def validatePassword(user: User): F[User] = mE.ensure(mE.pure(user))("Password is not valid")(u => u.password.length >= 6) } 18
  • 19.
    implicit val monadErrorOpt= new MonadError[Option, String] { override def raiseError[A](e: String): Option[A] = None override def handleErrorWith[A](fa: Option[A])(f: String => Option[A]): Option[A] = fa match { case r@Some(_) => r case None => None } override def pure[A](x: A): Option[A] = Some(x) 19
  • 20.
    override def flatMap[A,B](fa: Option[A])(f: A => Option[B]): Option[B] = for { a <- fa b <- f(a) } yield b @tailrec override def tailRecM[A, B](a: A)(f: A => Option[Either[A, B]]): Option[B] = f(a) match { case None => None case Some(Left(a1)) => tailRecM(a1)(f) case Some(Right(b)) => Some(b) } } 20
  • 21.
    val validatorOpt =new Validator[Option]() validatorOpt.validateUser(User("user1", 0, "strong")) val validatorEither = new Validator[Either[String, ?]]() validatorEither.validateUser(User("user2", 18, "weak")) 21
  • 22.
  • 23.
    case class CreditCard(balance:Long) sealed abstract class Item(valprice: Long) case object ScalaCourse extends Item(100) case object RedBook extends Item(200) case object ScalaDaysTickets extends Item(1000) 23
  • 24.
    def learnScala(creditCard: CreditCard):Unit = { val (creditCard1, courseraScalaCourse) = buyCourseraSubscription(creditCard) val (creditCard2, scalaRedBook) = buyRedBook(creditCard1) val (creditCard3, scaladaysTickets) = buyScalaDaysTickets(creditCard1) postTweet(List(courseraScalaCourse, scalaRedBook, scaladaysTickets), creditCard3) } 24
  • 25.
    case class State[S,A](run: S => (S, A)) { def flatMap[B](f: A => State[S, B]): State[S, B] = State[S, B] { s0 => val (s1, a) = run(s0) f(a).run(s1) } def map[B](f: A => B): State[S, B] = State[S, B] { s0 => val (s1, a) = run(s0) s1 -> f(a) } } 25
  • 26.
    def buyCourseraSubscription(creditCard: CreditCard):(CreditCard, Item) = { creditCard.copy(balance = creditCard.balance - ScalaCourse.price) -> ScalaCourse } def buyRedBook(creditCard: CreditCard): (CreditCard, Item) = { creditCard.copy(balance = creditCard.balance - RedBook.price) -> RedBook } def buyScalaDaysTickets(creditCard: CreditCard): (CreditCard, Item) = { creditCard.copy(balance = creditCard.balance - ScalaDaysTickets.price) -> ScalaDaysTickets } 26
  • 27.
    def buyCourseraSubscription() =State[CreditCard, Item](creditCard => creditCard.copy(balance = creditCard.balance - ScalaCourse.price) -> ScalaCourse ) def buyRedBook() = State[CreditCard, Item](creditCard => creditCard.copy(balance = creditCard.balance - RedBook.price) -> RedBook ) def buyScalaDaysTickets() = State[CreditCard, Item](creditCard => creditCard.copy(balance = creditCard.balance - ScalaDaysTickets.price) -> ScalaDaysTickets ) 27
  • 28.
    def learnScala(): State[CreditCard,Unit] = for { scalaCourse <- buyCourseraSubscription() redBook <- buyRedBook() scalaDaysTickets <- buyScalaDaysTickets() creditCard <- State.get } yield postTweet(List(scalaCourse, redBook, scalaDaysTickets), creditCard) 28
  • 29.
    def postTweet(items: List[Item],creditCard: CreditCard): Unit = println(s"Check this out ${items.mkString(",")}. And I have ${creditCard.balance} left on my account") def postTweetIO(items: List[Item], creditCard: CreditCard): IO[Unit] = IO(postTweet(items, creditCard)) 29
  • 30.
    def learnScalaIO(): StateT[IO,CreditCard, Unit] = for { scalaCourse <- buyCourseraSubscription().mapK(evalToIO) redBook <- buyRedBook().mapK(evalToIO) scalaDaysTickets <- buyScalaDaysTickets().mapK(evalToIO) creditCard <- StateT.get[IO, CreditCard] _ <- StateT.liftF[IO, CreditCard, Unit](postTweetIO(List(scalaCourse, redBook, scalaDaysTickets), creditCard)) } yield () 30
  • 31.
    type State[S, A]= StateT[Eval, S, A] val evalToIO = new arrow.FunctionK[Eval, IO] { override def apply[A](fa: Eval[A]): IO[A] = IO(fa.value) } 31
  • 32.
    MonadState trait MonadState[F[_], S]extends Serializable { val monad: Monad[F] def get: F[S] def set(s: S): F[Unit] def inspect[A](f: S => A): F[A] def modify(f: S => S): F[Unit] } 32
  • 33.
    type CCState[F[_]] =MonadState[F, CreditCard] class RoadToMonads[F[_] : CCState : Monad]() { val ms = implicitly[CCState[F]]; val m = implicitly[Monad[F]] def learnScala(): F[Unit] = for { scalaCourse <- buyCourseraSubscription() redBook <- buyRedBook() scalaDaysTickets <- buyScalaDaysTickets() creditCard <- ms.get } yield postTweet(List(scalaCourse, redBook, scalaDaysTickets), creditCard) 33
  • 34.
    private def postTweet(items:List[Item], creditCard: CreditCard): F[Unit] = m.pure(println(s"Check this out ${items.mkString(",")}. And I have ${creditCard.balance} left on my account")) 34
  • 35.
    private def buyCourseraSubscription():F[Item] = ms.modify(cc => cc.copy(balance = cc.balance - ScalaCourse.price)) .map(_ => ScalaCourse) private def buyRedBook(): F[Item] = ms.modify(cc => cc.copy(balance = cc.balance - RedBook.price)) .map(_ => RedBook) private def buyScalaDaysTickets(): F[Item] = ms.modify(cc => cc.copy(balance = cc.balance - ScalaDaysTickets.price)) .map(_ => ScalaDaysTickets) } 35
  • 36.
  • 37.
    So why shouldI use MTL? 37
  • 38.