Tofu

Tofu

  • Docs
  • API

›Higher-Kind

Getting Started

  • Getting started
  • Installation

Documentation

    Core

    • Error management
    • Forking and Racing
    • Timeout
    • Typeclasses

    Logging

    • Home (Start here)
    • Core concepts
    • Key Features
    • tofu.syntax.logging
    • Loggable typeclass
    • Logback Layouts

    Logging Recipes

    • Recipe list
    • The simplest form
    • Logs for a service
    • Contextual Logging
    • Logging auto derivation
    • ZIO Logging

    Contextual

    • Env
    • WithContext
    • Cats MTL interop

    Concurrent

    • Agent
    • MakeRef

    Higher-Kind

    • Mid

    Optics

    • Tofu optics

    Utilities

    • Config
    • Console
    • Memo

    Streams

    • Streams

Mid

Installation

"tf.tofu" %% "tofu" % tofu-version
or as a standalone dependency:
"tf.tofu" %% "tofu-core-higher-kind" % tofu-version

Assumption

Consider some trait

trait MyBusinessModule[F[_]] {
  def doBusinessThing(entity: Entity, info: Info): F[Value]
  def undoBusinessThing(entity: Entity): F[Respect]
}

Often F presented like some IO, reader, or any transformer

But signature doesn't oblige to be strict. Moreover, there is no necessity to use a functor

Let's start with an example

type Pre[F[_], A] = F[Unit]

Despite Pre has type-parameter A, it doesn't put any information to the result

Apply MyBusinessModule[F[_]] to Pre[F[_], *]

trait MyBusinessModule[Pre[F, *]] {
  def doBusinessThing(entity: Entity, info: Info): F[Unit]
  def undoBusinessThing(entity: Entity): F[Unit]
}

Only the effect is produced without any result. It could be logging, input validation, or something like that

Now consider the following type

type Post[F[_], A] = A => F[Unit]

This is a contravariant type. The module takes the form

trait MyBusinessModule[Post[F, *]] {
  def doBusinessThing(entity: Entity, info: Info): Value => F[Unit]
  def undoBusinessThing(entity: Entity): Respect => F[Unit]
}

Such an implementation of a module can express logging or validation of a computation result

Completes the next type

type Mid[F[_], A] = F[A] => F[A]

With Monad[F] both Pre and Post can be turned into Mid

Applying this to the module

trait MyBusinessModule[Mid[F, *]] {
  def doBusinessThing(entity: Entity, info: Info): F[Value] => F[Value]
  def undoBusinessThing(entity: Entity): F[Respect] => F[Respect]
}

Mid provides capabilities of both Pre and Post, but also allows to run F multiple times or not to run it at all.

Such middleware can be caching, retrying, or another logic, which is not implemented in infrastructure but requires additional reflection

Usage

It turns out that ApplyK is enough. Via

def map2K[F[_], G[_], H[_]](af: A[F], ag: A[G])(f: Tuple2K[F, G, *]~> H]): A[H]

It makes it possible to compose the result of the main computation and the result of a plug-in computation. Hence, we can also compose the main module implementation and pluggable one

Calling map2K with F = F, G = Mid[F, *], H = F, then substituting MyBusinessModule[F] and plugin MyBusinessModule[Mid] as af and ag, only remains to implement Tuple2K[F, G, *]~> H] i.e. the polymorphic function [A] (F[A], F[A] => F[A]) => F[A] or (fa, f) => f(fa)

So plugin application is just the process of applying the function to the result of every method. The macro generating ApplyK[MyBusinessModule] will do the rest of all

Example

Example representableK can be found in the source

Example applyK for authorship of https://t.me/ppressives

import cats.{Applicative, FlatMap, Monad}
import cats.syntax.semigroup._
import derevo.derive
import derevo.tagless.applyK
import tofu.higherKind.Mid
import tofu.syntax.monadic._

trait Metrics[F[_]] {
  def timed[A](metricsKey: String)(f: F[A]): F[A]
}

trait Logger[F[_]] {
  def info(str: String): F[Unit]
}

@derive(applyK)
trait FooService[F[_]] {
  def foo(a: String): F[Int]
}

object FooService {
  def create[F[_] : Monad](metrics: Metrics[F], logger: Logger[F]): FooService[F] = {
    val mid = (new FooLogging(logger): FooService[Mid[F, *]]) |+| (new FooMetrics(metrics): FooService[Mid[F, *]])
    mid attach new FooImpl[F]
  }

  private final class FooImpl[F[_]: Applicative] extends FooService[F] {
    def foo(a: String): F[Int] = a.length.pure[F]
  }

  private final class FooLogging[F[_]: FlatMap](logger: Logger[F]) extends FooService[Mid[F, *]] {
    def foo(a: String): Mid[F, Int] =
      d => logger.info(s"Calling foo with a=$a") *> d.flatTap(res => logger.info(s"foo returned $res"))
  }

  private final class FooMetrics[F[_]](metrics: Metrics[F]) extends FooService[Mid[F, *]] {
    def foo(a: String): Mid[F, Int] = metrics.timed("timings.foo")(_)
  }
}
← MakeRefTofu optics →
  • Installation
  • Assumption
  • Usage
  • Example
Tofu
Docs
Getting Started
Community
User ShowcaseStack OverflowTelegram Group (EN|RU)Gitter Group (EN)
More
BlogGitHubStar
Copyright © 2021 Tinkoff.ru