$30 off During Our Annual Pro Sale. View Details »

Irresistible party tricks with cats-tagless

Irresistible party tricks with cats-tagless

Are you writing effect-polymorphic code? Want to ease some of the pains involved? Say no more: this is the talk for you.

Meet cats-tagless: a library full of goodness that help F[_] fans achieve goals faster and with less code.

We will go through the most interesting type classes in cats-tagless, as well as ways to derive their instances for your algebras. We’ll also see some highly practical patterns of using it in a real codebase.

At the end, if you’ve been writing effect-polymorphic code in Scala without the library, you won’t be able to resist the temptation to use it in every upcoming project!

Jakub Kozłowski

April 03, 2020
Tweet

More Decks by Jakub Kozłowski

Other Decks in Programming

Transcript

  1. Irresistible party tricks
    with cats-tagless
    Photo by Aditya Chinchure on Unsplash
    Jakub Kozłowski |>
    ScalaUA |>
    03.04.2020
    speakerdeck.com/kubukoz

    View Slide

  2. cats-tagless teaser

    View Slide

  3. object OrderService {
    def make[F[_]: Monad](
    orderRepository: OrderRepository[F],
    messageSender: Sender[F]
    ): OrderService[F] = new OrderService[F] {
    def placeOrder(order: Order): F[Unit] =
    orderRepository.save(order) *>
    messageSender.send(OrderCreated(order))
    // def ...
    // def ...
    }
    }
    The ideal tagless final scenario

    View Slide

  4. object OrderService {
    def make[F[_]: Monad](
    orderRepository: OrderRepository[ConnectionIO],
    messageSender: Sender[F],
    transactor: Transactor[F]
    ): OrderService[F] = new OrderService[F] {
    def placeOrder(order: Order): F[Unit] =
    orderRepository.save(order).transact(transactor) *>
    messageSender.send(OrderCreated(order))
    // def ...
    // def ...
    }
    }
    The not-so-ideal scenario

    View Slide

  5. object OrderService {
    def make[F[_]: Monad](
    orderRepository: OrderRepository[ConnectionIO],
    messageSender: Sender[F],
    transactor: Transactor[F]
    ): OrderService[F] = new OrderService[F] {
    def placeOrder(order: Order): F[Unit] =
    orderRepository.save(order).transact(transactor) *>
    messageSender.send(OrderCreated(order))
    // def ...
    // def ...
    }
    }
    The not-so-ideal scenario

    View Slide

  6. What if...
    val module = {
    val repository: OrderRepository[ConnectionIO] = OrderRepository.doobieInstance
    val sender: Sender[IO] = Sender.instance
    val orders: OrderService[ConnectionIO] =
    OrderService.make(repository, changeEffect(sender))
    ...
    }
    val changeEffect: Sender[IO] => Sender[ConnectionIO] = ???

    View Slide

  7. The gist of it
    trait Sender[F[_]] {
    def send(message: AppMessage): F[Unit]
    }
    val changeEffect: Sender[IO] => Sender[ConnectionIO] =
    sender => message => sender.send(message).to[ConnectionIO]

    View Slide

  8. Some abstraction
    def changeEffect[F[_], G[_]](f: F => G): Sender[F] => Sender[G] =
    sender => message => f(sender.send(message))

    View Slide

  9. Some abstraction
    def changeEffect[F[_], G[_]](f: F => G): Sender[F] => Sender[G] =
    sender => message => f(sender.send(message))

    View Slide

  10. What are we missing?
    trait =>[A, B] {
    def apply(a: A): B
    }
    type =>[A, B] = Function[A, B]
    Function from A to B

    View Slide

  11. What are we missing?
    trait =>[A, B] {
    def apply(a: A): B
    }
    type =>[A, B] = Function[A, B]
    Function from A to B
    trait FunctionK[F[_], G[_]] {
    def apply[A](a: F[A]): G[A]
    }
    type ~>[F[_], G[_]] = FunctionK[F, G]
    For all A, a function from F[A] to G[A]

    View Slide

  12. Some abstraction
    def changeEffect[F[_], G[_]](f: F => G): Sender[F] => Sender[G] =
    sender => message => f(sender.send(message))

    View Slide

  13. Fixed!
    def changeEffect[F[_], G[_]](f: F ~> G): Sender[F] => Sender[G] =
    sender => message => f(sender.send(message))

    View Slide

  14. More abstraction!
    def changeEffect[Alg[_[_]], F[_], G[_]](f: F ~> G): Alg[F] => Alg[G] =
    alg => ???(f, alg)

    View Slide

  15. Familiar shape?
    def changeEffect[Alg[_[_]], F[_], G[_]](f: F ~> G): Alg[F] => Alg[G]
    def f[F[_], A, B](f: A => B): F[A] => F[B]

    View Slide

  16. Familiar shape?
    def changeEffect[Alg[_[_]]: FunctorK, F[_], G[_]](f: F ~> G): Alg[F] => Alg[G] = _.mapK(f)
    def f[F[_]: Functor, A, B](f: A => B): F[A] => F[B] = _.map(f)

    View Slide

  17. @typeclass
    trait Functor[F[_]] {
    def map[A, B]:
    (A => B) => F[A] => F[B]
    }

    View Slide

  18. @typeclass
    trait Functor[F[_]] {
    def map[A, B]:
    (A => B) => F[A] => F[B]
    }
    @typeclass
    trait FunctorK[A[_[_]]] {
    def mapK[F[_], G[_]]:
    (F ~> G) => A[F] => A[G]
    }

    View Slide

  19. @autoFunctorK
    trait Sender[F[_]] {
    def send(message: AppMessage): F[Unit]
    }
    val sender: Sender[IO] = msg => IO(println(msg))
    val fk: IO ~> ConnectionIO = LiftIO.liftK
    val cioSender: Sender[ConnectionIO] = sender.mapK(fk)
    val result: ConnectionIO[Unit] = cioSender.send(msg)

    View Slide

  20. Time to party!
    Photo by NATHAN MULLET on Unsplash

    View Slide

  21. Change effects - apply transaction
    def transacted(
    example: UserService[ConnectionIO],
    transactor: Transactor[IO]
    ): UserService[IO] = {
    val transact: ConnectionIO ~> IO = transactor.trans
    example.mapK(transact)
    }

    View Slide

  22. Change effects - send messages
    def sendingMessages[Messages](
    example: UserService[WriterT[IO, Messages, *]],
    sender: Messages => IO[Unit]
    ): UserService[IO] = {
    val send: WriterT[IO, Messages, *] ~> IO =
    λ[WriterT[IO, Messages, *] ~> IO] {
    _.run.flatMap(_.leftTraverse(sender).map(_._2))
    }
    example.mapK(send)
    }
    syntax using https://github.com/typelevel/kind-projector

    View Slide

  23. Apply a known effect before, after or around every method
    def withLogging(logger: Logger[IO], example: UserService[IO]): UserService[IO] = {
    val addLogging: IO ~> IO = λ[IO ~> IO] { action =>
    logger.info("Making call") *>
    action
    .attempt
    .flatTap(result => logger.info("Call resulted in " + result))
    .rethrow
    }
    example.mapK(addLogging)
    }

    View Slide

  24. Catch errors
    def liftErrors(
    example: UserService[IO]
    ): UserService[EitherT[IO, Throwable, *]] = {
    val catchToEither: IO ~> EitherT[IO, Throwable, *] =
    λ[IO ~> EitherT[IO, Throwable, *]](action => EitherT(action.attempt))
    example.mapK(catchToEither)
    }

    View Slide

  25. Fallback to another implementation
    def fallback[Alg[_[_]]: ApplyK](main: Alg[IO], backup: Alg[IO]): Alg[IO] =
    main.map2K(backup)(
    λ[Tuple2K[IO, IO, *] ~> IO]{ action =>
    action.first orElse action.second
    }
    )
    @autoApplyK
    trait UserService[F[_]] { ... }

    View Slide

  26. Choose fastest implementation's result every time
    def raceBoth(
    main: UserService[IO],
    backup: UserService[IO]
    )(
    implicit F: Concurrent[IO]
    ): UserService[IO] =
    main.map2K(backup)(
    λ[Tuple2K[IO, IO, *] ~> IO] { action =>
    action.first.race(action.second).map(_.merge)
    }
    )

    View Slide

  27. Lift stateless instance
    val liftInMemoryInstance: UserService[IO] = {
    type Id[A] = A
    val instance: UserService[Id] = UserService.constant
    val lift: Id ~> IO = λ[Id ~> IO](IO.pure(_))
    instance.mapK(lift)
    }

    View Slide

  28. Merge results of multiple implementations
    def mergeAll(
    redis: UserService[IO],
    postgres: UserService[ConnectionIO],
    inMemory: UserService[cats.Id]
    )(
    transactor: Transactor[IO]
    ): IO[List[User]] = {
    val combined = UserService.product3K(redis, postgres, inMemory)
    combined.findAll match {
    case (redisCall, postgresCall, constantResult) =>
    redisCall |+| postgresCall.transact(transactor) |+| IO.pure(constantResult)
    }
    }
    @autoProductNK @autoApplyK
    trait UserService[F[_]] { ... }

    View Slide

  29. Merge results of multiple implementations
    def mergeAll(
    redis: UserService[IO],
    postgres: UserService[ConnectionIO],
    inMemory: UserService[cats.Id]
    )(
    transactor: Transactor[IO]
    ): IO[List[User]] = {
    val combined = UserService.product3K(redis, postgres, inMemory)
    combined.findAll match {
    case (redisCall, postgresCall, constantResult) =>
    redisCall |+| postgresCall.transact(transactor) |+| IO.pure(constantResult)
    }
    }
    Can't combine all methods yet, need to choose first - more on that later
    @autoProductNK @autoApplyK
    trait UserService[F[_]] { ... }

    View Slide

  30. Repeat action as a stream in exponentially growing delay
    def repeatingExponentially(
    instance: UserService[IO]
    )(
    implicit timer: Timer[IO]
    ): UserService[fs2.Stream[IO, *]] = {
    val exponentialSleep: fs2.Stream[IO, Unit] =
    fs2.Stream.iterate(10.millis)(_ * 2).evalMap(IO.sleep)
    val liftToStream: IO ~> fs2.Stream[IO, *] = λ[IO ~> fs2.Stream[IO, *]] { action =>
    fs2.Stream.repeatEval(action).zipLeft(exponentialSleep)
    }
    instance.mapK(liftToStream)
    }

    View Slide

  31. Attach semaphore, circuit breaker, retries, etc.
    def withSemaphore(instance: UserService[IO])(sem: Semaphore[IO]): UserService[IO] =
    instance.mapK(λ[IO ~> IO](sem.withPermit(_)))
    def retryOnce(instance: UserService[IO]): UserService[IO] =
    instance.mapK(λ[IO ~> IO](action => action.orElse(action)))

    View Slide

  32. New thing - Instrument typeclass
    final case class Instrumentation[F[_], A](
    value: F[A],
    algebraName: String,
    methodName: String
    )
    @typeclass
    trait Instrument[Alg[_[_]]] {
    def instrument[F[_]](af: Alg[F]): Alg[Instrumentation[F, *]]
    }

    View Slide

  33. Log the called method's name
    def logMethodName(
    instance: UserService[IO],
    logger: Logger[IO]
    ): UserService[IO] =
    instance.instrument.mapK {
    λ[Instrumentation[IO, *] ~> IO] { inst =>
    logger.info(show"Running ${inst.algebraName}.${inst.methodName}") *>
    inst.value
    }
    }

    View Slide

  34. Fail methods by name
    def withDisabledMethods(
    instance: UserService[IO]
    )(
    methodNames: String*
    ): UserService[IO] = {
    def disabledMethod[A] =
    methodNames.toSet.compose[Instrumentation[IO, A]](_.methodName)
    instance.instrument.mapK {
    λ[Instrumentation[IO, *] ~> IO] {
    case inst if disabledMethod(inst) =>
    IO.raiseError(
    new Throwable(show"Disabled: ${inst.algebraName}.${inst.methodName}")
    )
    case inst => inst.value
    }
    }
    }

    View Slide

  35. Start new distributed tracing span for each method
    def traced[F[_]: Trace](instance: UserService[F]): UserService[F] =
    instance.instrument.mapK {
    λ[Instrumentation[F, *] ~> F] { inst =>
    Trace[F].span(inst.algebraName + "." + inst.methodName)(inst.value)
    }
    }
    Using https://github.com/tpolecat/natchez

    View Slide

  36. mapK on multiple algebras at once! (if each of them has a FunctorK)
    @autoFunctorK
    trait MainModule[F[_]] {
    def users: UserService[F]
    def mail: MailService[F]
    def main: MainService[F]
    }

    View Slide

  37. More in the future?
    Join the conversation: https://github.com/typelevel/cats-tagless/issues/126

    View Slide

  38. More in the future?
    Join the conversation: https://github.com/typelevel/cats-tagless/issues/126
    Generate keys for caching based on parameter values

    View Slide

  39. More in the future?
    Join the conversation: https://github.com/typelevel/cats-tagless/issues/126
    Generate keys for caching based on parameter values
    Add parameters as tags to a Span

    View Slide

  40. More in the future?
    Join the conversation: https://github.com/typelevel/cats-tagless/issues/126
    Generate keys for caching based on parameter values
    Add parameters as tags to a Span
    Log selected parameters in error handler

    View Slide

  41. More in the future?
    Join the conversation: https://github.com/typelevel/cats-tagless/issues/126
    Generate keys for caching based on parameter values
    Add parameters as tags to a Span
    Log selected parameters in error handler
    Merge return values of multiple algebras

    View Slide

  42. Demo time?

    View Slide

  43. Thank you
    blog.kubukoz.com
    @kubukoz
    Slides: speakerdeck.com/kubukoz
    Code: git.io/JvFjC
    Find me on YouTube! (yt.kubukoz.com)

    View Slide