两个 ReaderT 实例之间的 FunctionK 转换,为结果提供环境

问题描述 投票:0回答:1

我在服务中使用的效果类型定义如下:

  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]

我想在其中一些类型之间定义一个

FunctionK
映射,因此我定义了以下辅助方法:

  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))))

然后将它们像这样绑在一起:

  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)

我遇到的问题是我总是在转换中提供

noop
跟踪 ctx。我有点难以定义它 - 我如何将
TracingCtx
从环境中取出并在转换中使用它?

我本来希望能够使用这样的东西

  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)
      )
  }

但是不符合要求的形状。有什么想法吗?

scala transformation typeclass scala-cats tagless-final
1个回答
0
投票

这并不是因为你想做的事情没有意义。

如果我们去掉所有 CT 乱码,你现在看到的就是

// I'll use Scala 3's polymorphic function notation
[A] =>
  (TracingCtx => IO[Either[ServiceError, A]]) => IO[A]

您的工作尝试:

  • 采用一些已知的
    TracingCtx
    (例如noop)并应用它
  • 获得
    IO[Either[ServiceError, A]]
  • ServiceError
    转换为
    Throwable
    并将其移动到
    IO.raiseError
    ,而正确的值则转到
    IO.pure

MTL 所做的就是将所有这些隐藏在类型类下。

Ask[F, A].ask: F[A]
只是访问函数的参数,仅当
F
是某个函数 (
ReaderT
) 时才可用。

如果您没有以某种方式调用此函数,并且您的代码尝试获取函数中的参数,将其传递给调用完全相同的函数,则无法使用

Ask
来获取
TracingCtx
,这样它就只是
 IO
。这是行不通的。 Haskell 中有一些奇怪的情况看起来很相似,但它们通常只在存在递归和惰性时才起作用。在这里,您必须以某种方式从
TracingCtx
中提取
IO[A]
(可能可以使用
IOLocal
,如果人们了解它是如何工作的,不支持使用
Ask[IO, TracingCtx]
进行 OOTB),然后您可以将其传递到内部。类似的东西(只是一个草稿,我不想花接下来的 40 分钟修复代码):

val ioLocal: IOLocal[TrancingCtx] = ...

new (ServiceIO ~> ServiceError) {
   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)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.