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!

08f642741fba006656cb86fb61c160b3?s=128

Jakub Kozłowski

April 03, 2020
Tweet

Transcript

  1. Irresistible party tricks with cats-tagless Photo by Aditya Chinchure on

    Unsplash Jakub Kozłowski |> ScalaUA |> 03.04.2020 speakerdeck.com/kubukoz
  2. cats-tagless teaser

  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
  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
  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
  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] = ???
  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]
  8. Some abstraction def changeEffect[F[_], G[_]](f: F => G): Sender[F] =>

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

    Sender[G] = sender => message => f(sender.send(message))
  10. What are we missing? trait =>[A, B] { def apply(a:

    A): B } type =>[A, B] = Function[A, B] Function from A to B
  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]
  12. Some abstraction def changeEffect[F[_], G[_]](f: F => G): Sender[F] =>

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

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

    => Alg[G] = alg => ???(f, alg)
  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]
  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)
  17. @typeclass trait Functor[F[_]] { def map[A, B]: (A => B)

    => F[A] => F[B] }
  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] }
  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)
  20. Time to party! Photo by NATHAN MULLET on Unsplash

  21. Change effects - apply transaction def transacted( example: UserService[ConnectionIO], transactor:

    Transactor[IO] ): UserService[IO] = { val transact: ConnectionIO ~> IO = transactor.trans example.mapK(transact) }
  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
  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) }
  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) }
  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[_]] { ... }
  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) } )
  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) }
  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[_]] { ... }
  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[_]] { ... }
  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) }
  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)))
  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, *]] }
  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 } }
  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 } } }
  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
  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] }
  37. More in the future? Join the conversation: https://github.com/typelevel/cats-tagless/issues/126

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

    for caching based on parameter values
  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
  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
  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
  42. Demo time?

  43. Thank you blog.kubukoz.com @kubukoz Slides: speakerdeck.com/kubukoz Code: git.io/JvFjC Find me

    on YouTube! (yt.kubukoz.com)