Reputation: 348
I have the following definition of effect types that I use in my service:
type Traced[F[_], A] = ReaderT[F, TracingCtx, A]
type TracedErrorHandling[F[_], E, A] = Traced[EitherT[F, E, *], A]
type ServiceIO[A] = TracedErrorHandling[IO, ServiceError, A]
type ClientIO[A] = TracedErrorHandling[IO, ClientError, A]
type DatabaseIO[A] = TracedErrorHandling[IO, DatabaseError, A]
I would like to define a FunctionK
mapping between some of these types, as such I've defined the following helper methods:
def providing[F[_], A](a: A): Kleisli[F, A, *] ~> F = λ[Kleisli[F, A, *] ~> F](_.run(a))
def folding[F[_]: Monad, E, E1](f: E1 => E)(implicit F: Raise[F, ? >: E]): EitherT[F, E1, *] ~> F =
λ[EitherT[F, E1, *] ~> F](_.foldF(e1 => F.raise(f(e1)), a => Applicative[F].pure(a)))
def translating[F[_]: Functor, E2, E1](translator: E1 => E2): EitherT[F, E1, *] ~> EitherT[F, E2, *] =
λ[EitherT[F, E1, *] ~> EitherT[F, E2, *]](_.leftMap(translator(_)))
def leftTapping[F[_]: FlatMap: Handle[*[_], E], E](f: E => F[Unit]): F ~> F =
λ[F ~> F](_.handleWith[E](e => f(e) >> e.raise))
def errorLogging[F[_]: Sync: Ask[*[_], TracingCtx]: Handle[*[_], E], E: ToThrowable](message: String): F ~> F =
leftTapping[F, E](e =>
Ask.ask.flatMap(implicit ctx => Sync[F].delay(logger.error(message, ToThrowable[E].throwable(e))))
and tie them together like this:
implicit lazy val svcToIo: ServiceIO ~> IO =
errorLogging[ServiceIO, ServiceError]("Database Error:") andThen
providing(TracingCtx.noop) andThen
folding[IO, Throwable, ServiceError](ToThrowable[ServiceError].throwable)
implicit lazy val dbToSvc: DatabaseIO ~> ServiceIO =
errorLogging[DatabaseIO, DatabaseError]("Database Error:") andThen
providing(TracingCtx.noop) andThen
translating[IO, ServiceError, DatabaseError](databaseToService) andThen
Kleisli.liftK
implicit lazy val clientToSvc: ClientIO ~> ServiceIO =
errorLogging[ClientIO, ClientError]("Database Error:") andThen
providing(TracingCtx.noop) andThen
translating[IO, ServiceError, ClientError](clientToService) andThen
Kleisli.liftK
// Later in Main
// client <- myClient[ClientIO].mapK(clientToService)
// service <- myService[ServiceIO](client)
The issue I have is that I always provide a noop
tracing ctx in the transformation. I am struggling a bit to define that - how would I get the TracingCtx
out of the environment and use it in the transformation?
I had hoped to be able to use something like this
implicit def svcToIo: ServiceIO ~> IO = new ~>[ServiceIO, IO] {
override def apply[A](fa: ServiceIO[A]): IO[A] = Ask[ServiceIO, TracingCtx]
.map(tracingCtx =>
errorLogging[ServiceIO, ServiceError]("Unhandled Service Error:") andThen
providing[EitherT[IO, ServiceError, *], TracingCtx](tracingCtx) andThen
folding[IO, Throwable, ServiceError](ToThrowable[ServiceError].throwable)
)
}
But it does not conform to the required shape. Any ideas?
Upvotes: 2
Views: 50
Reputation: 27535
It doesn't because what you want to do doesn't make sense.
If we get rid of all the CT gibberish what you have here is
// I'll use Scala 3's polymorphic function notation
[A] =>
(TracingCtx => IO[Either[ServiceError, A]]) => IO[A]
Your working attempt:
TracingCtx
(e.g. noop) and applies itIO[Either[ServiceError, A]]
ServiceError
to Throwable
and moves it to IO.raiseError
, while the right values goes to IO.pure
All MTL does is hides all of that under the type classes. Ask[F, A].ask: F[A]
is just accessing that argument of the function, available only when F
is some function (ReaderT
).
You cannot use Ask
to obtain a TracingCtx
if you are not calling this function somehow, and your code tries to obtain the argument in a function, outside that function, to call itself, so that it would be just IO
. It cannot work. There are some weird cases in Haskell which looks similarly, but they usually only work when there is recursion and laziness on the table. Here you'd have to somehow extract TracingCtx
out of IO[A]
(potentially possible using IOLocal
, if one understands how it works, not supported OOTB using Ask[IO, TracingCtx]
), and then you could pass it inside. Something similar to (just a draft I don't want to spend next 40 minutes fixing code):
val ioLocal: IOLocal[TrancingCtx] = ...
new (ServiceIO ~> IO) {
private def withCtx(ctx: TracingContext) =
errorLogging[ServiceIO, ServiceError]("Database Error:") andThen
providing(ctx) andThen
folding[IO, Throwable, ServiceError](ToThrowable[ServiceError].throwable)
def apply[A](fa: ServiceIO[A]): IO[A] =
ioLocal.flatMap { ctx =>
withCtx(ctx)(fa)
}
}
Obviously, that ioLocal
isn't making things magical - you'd still have to set it up somewhere to something, providing the default value at ioLocal
creation. And you'd have to be careful how it works in concurrent context with fibers.
Upvotes: 3