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

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

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

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

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

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

    = sender => message => f(sender.send(message))
  13. 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]
  14. 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)
  15. @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] }
  16. @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)
  17. Change effects - apply transaction def transacted( example: UserService[ConnectionIO], transactor:

    Transactor[IO] ): UserService[IO] = { val transact: ConnectionIO ~> IO = transactor.trans example.mapK(transact) }
  18. 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
  19. 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) }
  20. 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) }
  21. 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[_]] { ... }
  22. 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) } )
  23. 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) }
  24. 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[_]] { ... }
  25. 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[_]] { ... }
  26. 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) }
  27. 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)))
  28. 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, *]] }
  29. 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 } }
  30. 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 } } }
  31. 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
  32. 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] }
  33. 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
  34. 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
  35. 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