【问题标题】:Flattening mixed nested monads in cats (composing complex Kleislis)扁平化猫中的混合嵌套单子(组成复杂的 Kleislis)
【发布时间】:2022-01-09 07:53:41
【问题描述】:

我想了解我应该如何编写返回“复杂”类型的 Kleislis(例如 F[Either[A,B]])。这是我的意思的一个例子:

这是一个简单的案例 - 给定两个 Kleislis

val parseRegistration: Kleisli[Result, Auth.Registration, Auth.Registration] = ???
val hashPassword: Kleisli[Result, Auth.Registration, Auth.Registration] = ???

我可以这样组合它们:

val validateAndHash = parseRegistration andThen hashPassword

一切都很好。

但是,考虑到更复杂的 Kleisli 类型(返回值为 Either[A, B]),我无法直接组合它们(因为我最终得到了 F[Either[A,B]],而 Kleisli 只期望 F[A])。

// relevant type definitions
final case class ServiceError(kind: ErrorKind, message: String = "", cause: Option[Throwable] = None)
final type Result[A] = Either[ServiceError, A]
final case class Registration(email: String, password: String)
final case class Registered(session: Session)


val createUser: Kleisli[F, Auth.Registration, Result[Long]] = ???
val createSession: Kleisli[F, Long, Result[Session]] = ???


// does not compile (no overloaded alternatives match the arguments) - I expected this as  
// createUser returns F[Result[Long]] and the createSession Kleisli expects only F[Long]
val createUserAndSession = createUser andThen createSession

好的,正如预期的那样。然后,我没有编写它们,而是尝试简单地执行嵌套地图:

val r1 = validateAndHash(info) // Result[Auth.Registration]
val r2 = r1.map(i => createUser(i)) // Either[ServiceError, F[Result[Long]]]
val r3 = r2.map(_.map(_.map(uid => createSession(uid).map(_.map(Auth.Registered.apply)))))
// Now I've got an Either[ServiceError, F[Either[ServiceError, F[Either[ServiceError, Auth.Registered]]]]]
// in r3! I wanted to end up with F[Result[Auth.Registered]]

所以我最终导致嵌套增加,flatten 似乎无济于事,因为 F 和 Result 不能相互扁平化。

我确信这是经验丰富的 scala 猫程序员经常遇到的问题,所以如果您能帮助我了解如何避免让自己陷入这种混乱,我将不胜感激?

编辑:

我现在定义了一个函数来反转嵌套的包装器类型,以便它们可以被展平:

  def invert[S, F[_]: Monad, V](e: Either[S, F[V]]): F[Either[S, V]] =
    e match {
      case Left(s) => Monad[F].pure(Left(s))
      case Right(fv) => fv.map(Right(_))
    }

这让我可以这样做:

val r1 = validateAndHash(info)
val r2 = invert(r1.map(i => createUser(i))).map(_.flatten)
val r3 = r2.map( i => invert(i.map(uid => createSession(uid).map(_.map(Auth.Registered.apply)))).map(_.flatten))
val r4 = Monad[F].flatten(r3)

这看起来……笨拙?而且对Either 来说太具体了。有没有更简洁的方法可以使用猫内置的东西来做到这一点?

【问题讨论】:

  • Kleisli 在为不同的输入或效果提供自动合成方面很差。 (有mapF和mapK之类的东西)这是ZIO的卖点之一,也是它通过类型实现DI的方法。有一个 monad 来统治它们,所以它们组合得很好,但代价是用特定的效果感染你的整个代码库。
  • 谢谢@MateuszKubuszok。我希望了解如何仅使用猫来做到这一点,但总是有兴趣了解替代方案。我会调查 ZIO
  • 是的,如果你想和 Cats 呆在一起,那么不幸的是,你必须手动使用 mapK 和 mapF 来对齐 F 类型自己,所以它更适合一些 ad-hoc具有相同输入或全局具有相同输入的多个服务的组合。使用 ZIO,事情应该更加自动化,但您将致力于一个 monad 来处理所有事情。

标签: scala scala-cats


【解决方案1】:

我宁愿编写值也不愿玩Kleisli,尤其是EitherT 让这变得非常简单。

首先,让我们将createUser & createSession 重构为普通方法:

def createUser[F[_]](registration: Registration): F[Result[Long]] = ???
def createSession[F[_]](long: Long): F[Result[Session]] = ???

现在让我们实现createUserAndSession

import cats.Monad
import cats.data.EitherT

def createUserAndSession[F[_]](registration: Registration)(implicit ev: Monad[F]): F[Result[Session]] =
  EitherT(createUser(registration)).flatMapF(createSession).value

【讨论】:

  • 谢谢!我认为问题是我无法理解 monad 转换器是什么/它们如何适合这里。我现在已经从 kleislis 重构为 MT,并且看到我的代码得到了相当大的简化。稍微超出范围,但也希望您将EitherT 推入函数签名中的想法 - 如果我控制较低级别的函数并且它们对调用范围是私有的,感觉就像我可以让它们返回 EitherT 而不是强制转换使用,还是不赞成?
  • @Orphid 嘿,很高兴它帮助了你 :) 关于你的问题,我实际上不需要大量使用转换器 (主要是因为我不使用很多类型的错误) 所以我没有正确的见解。然而,像 Fabio 和 Rob 这样更有经验的人建议将转换器保留为实现细节(例如,您可以看到我不需要将 createSession 包装成 EitherT) - 无论如何,你为什么不加入我们的typelevel discord server 并在那里问(以及未来的问题) :) 很多人会很乐意提供他们的建议
猜你喜欢
  • 2011-04-24
  • 1970-01-01
  • 2017-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多