我在服务中使用的效果类型定义如下:
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)
)
}
但是不符合要求的形状。有什么想法吗?
这并不是因为你想做的事情没有意义。
如果我们去掉所有 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)
}
}