使用 OptionT 避免样板文件(自然变换?)

Avoiding boilerplate with OptionT (natural transform?)

我有以下方法:

trait Tr[F[_]]{

    def getSet(): F[Set[String]]

    def checksum(): F[Long]

    def value(): F[String]

    def doRun(v: String, c: Long, s: Set[String]): F[Unit]
}

现在我想写以下内容供大家理解:

import cats._
import cats.data.OptionT
import cats.implicits._

def fcmprhn[F[_]: Monad](F: Tr[F]): OptionT[F, Unit] =
  for {
    set <- OptionT {
      F.getSet() map { s =>
        if(s.nonEmpty) Some(s) else None
      }
    }
    checksum <- OptionT.liftF(F.checksum())
    v <- OptionT.liftF(F.value())
    _ <- OptionT.liftF(F.doRun(v, checksum, set))
    //can be lots of OptionT.liftF here
} yield ()

如您所见,OptionT 样板文件太多了。有办法避免吗?

我想我可以利用F ~> OptionT[F, ?]。你能推荐点什么吗?

一种方法是将 for-comprehension 的 "F only" 部分嵌套在单个 liftF:

def fcmprhn[F[_]: Monad](F: Tr[F]): OptionT[F, Unit] =
  for {
    set <- OptionT {
      F.getSet() map { s =>
        if(s.nonEmpty) Some(s) else None
      }
    }
    _ <- OptionT.liftF {
      for {
        checksum <- F.checksum()
        v <- F.value()
        _ <- F.doRun(v, checksum, set)
        // rest of F monad for-comprehension
      } yield ()
    }
  } yield ()

您可以将其写在 "mtl-style" 中。 mtl-style 指的是 haskell 中的 mtl 库,但实际上它只是意味着不是将效果编码为值(即 OptionT[F, ?]),而是将它们编码为具有抽象效果的函数 F[_] 和使用 classes 类型赋予 F 能力。这意味着我们可以使用 F[Unit] 作为我们的 return 类型,而不是使用 OptionT[F, Unit] 作为我们的 return 类型,因为 F 必须能够处理错误。

这使得编写像您这样的代码更容易一些,但是当您将 monad 转换器添加到堆栈时,它的效果会被放大。现在您只需举起一次,但如果您以后想要 StateT[OptionT[F, ?], S, Unit] 怎么办?使用 mtl-style,您需要做的就是添加另一种类型 class 约束。

这是您的代码以 mtl 风格编写的样子:

def fcmprhn[F[_]](F: Tr[F])(implicit E: MonadError[F, Unit]): F[Unit] =
  for {
    set <- OptionT {
      F.getSet() flatMap { s =>
        if(s.nonEmpty) E.pure(s) else E.raiseError(())
      }
    }
    checksum <- F.checksum()
    v <- F.value()
    _ <- F.doRun(v, checksum, set)
} yield ()

现在,当您 运行 程序时,您可以将 F[_] 指定为与​​之前 OptionT[F, ?]:

类似的东西
fcmprhn[OptionT[F, ?]](OptionT.liftF(yourOriginalTr))