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")(_)
}
}