Task
(scalaz 或更好的 fs2)应该满足所有要求,它不需要 monad-transformer,因为它已经在Either
里面了(Either
对于 fs2,\/
对于 scalaz)。它还具有您需要的快速失败行为,与右偏析取/异或相同。
以下是我知道的几个实现:
不管 monad-transformer 没有,你在使用时仍然需要提升Task
:
但是,是的,它似乎确实比 monad 转换器更简单,特别是考虑到 monad 几乎不可组合的事实 - 为了定义 monad 转换器,除了作为 monad 之外,您还必须了解有关您的类型的一些其他细节(通常它需要像 comonad 这样的东西提取价值)。
仅出于广告目的,我还要补充一点,它Task
代表堆栈安全的蹦床计算。
但是,有一些项目专注于扩展 monad 组合,例如 Emm-monad:httpsFuture
://github.com/djspiewak/emm,因此您可以使用/ Task
、Either
、Option
等来组合 monad 转换器List
。但是,IMO,与组合相比,它仍然受到限制Applicative
-提供了允许轻松组合任何 Applicative 的cats
通用数据类型,您可以在这个答案中找到一些示例- 这里唯一的缺点是很难使用 Applicative 构建可读的 DSL。另一种选择是所谓的“Freer monad”:https ://github.com/m50d/paperdollNested
,这基本上提供了更好的组合,并允许将不同的效果层分离到不同的解释器中。
例如,由于没有FutureT
/TaskT
转换器,您无法构建像type E = Option |: Task |: Base
( Option
from Task
) 这样的效果,flatMap
因此需要从Future
/中提取值Task
。
作为一个结论,我可以说,根据我的经验Task
,基于 do-notation 的 DSL 确实有用:我有一个复杂的外部规则,如 DSL 用于异步计算,当我决定将它全部迁移到 Scala 嵌入式版本时,Task
真的很有帮助 -我从字面上将 external-DSL 转换为 Scala 的for-comprehension
. 我们考虑的另一件事是拥有一些自定义类型,例如ComputationRule
在其上定义一组类型类以及转换为Task
/Future
或我们需要的任何内容,但这是因为我们没有Free
明确使用 -monad。
Free
假设您不需要切换解释器的能力(这可能仅适用于系统测试) ,您甚至可能不需要-monad 。在这种情况下,Task
可能是您唯一需要的东西 - 它是惰性的(与 Future 相比),真正的功能和堆栈安全:
trait DSL {
def put[E](e: E): Task[Unit]
def count[E](e: E): Task[Int]
}
object Implementation1 extends DSL {
...implementation
}
object Implementation2 extends DSL {
...implementation
}
//System-test script:
def test0(dsl: DSL) = {
import dsl._
for {
_ <- put("Apple")
_ <- put("Orange")
_ <- put("Pinneaple")
nApples <- count("Apple")
nPears <- count("Pear")
nBananas <- count("Banana")
} yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas))
}
因此,您可以通过在此处传递不同的“解释器”来切换实现:
test0(Implementation1).unsafeRun
test0(Implementation2).unsafeRun
差异/缺点(与http://typelevel.org/cats/datatypes/freemonad.html相比):
- 你坚持
Task
类型,所以你不能轻易地将它折叠到其他单子。
当您传递 DSL-trait 的实例(而不是自然转换)时,实现在运行时解决,您可以使用 eta-expansion: 轻松抽象它test0 _
。DSL
Java/Scala 自然支持多态方法(put、count),但 poly 函数不支持,因此传递包含 实例T => Task[Unit]
(用于操作)比使用 natural-transform 生成put
合成多态函数更容易。DSLEntry[T] => Task[Unit]
DSLEntry ~> Task
在自然转换中没有显式 AST 而不是模式匹配 - 我们在 DSL trait 中使用静态调度(显式调用一个方法,这将返回惰性计算)
实际上,您甚至可以摆脱Task
这里:
trait DSL[F[_]] {
def put[E](e: E): F[Unit]
def count[E](e: E): F[Int]
}
def test0[M[_]: Monad](dsl: DSL[M]) = {...}
所以在这里它甚至可能成为一个偏好问题,尤其是当您不编写开源库时。
把它们放在一起:
import cats._
import cats.implicits._
trait DSL[F[_]] {
def put[E](e: E): F[Unit]
def count[E](e: E): F[Int]
}
def test0[M[_]: Monad](dsl: DSL[M]) = {
import dsl._
for {
_ <- put("Apple")
_ <- put("Orange")
_ <- put("Pinneaple")
nApples <- count("Apple")
nPears <- count("Pear")
nBananas <- count("Banana")
} yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas))
}
object IdDsl extends DSL[Id] {
def put[E](e: E) = ()
def count[E](e: E) = 5
}
请注意,猫有一个Monad
定义 for Id
,所以:
scala> test0(IdDsl)
res2: cats.Id[List[(String, Int)]] = List((Apple,5), (Pears,5), (Bananas,5))
简单有效。当然,您可以根据自己的喜好选择Task
//Future
或Option
任何组合。事实上,您可以使用Applicative
代替Monad
:
def test0[F[_]: Applicative](dsl: DSL[F]) =
dsl.count("Apple") |@| dsl.count("Pinapple apple pen") map {_ + _ }
scala> test0(IdDsl)
res8: cats.Id[Int] = 10
|@|
是一个并行运算符,因此您可以使用cats.Validated
而不是Xor
,请注意|@|
for Task 不会并行执行(至少在较旧的 scalaz 版本中)(并行运算符不等于并行计算)。您还可以使用两者的组合:
import cats.syntax._
def test0[M[_]:Monad](d: DSL[M]) = {
for {
_ <- d.put("Apple")
_ <- d.put("Orange")
_ <- d.put("Pinneaple")
sum <- d.count("Apple") |@| d.count("Pear") |@| d.count("Banana") map {_ + _ + _}
} yield sum
}
scala> test0(IdDsl)
res18: cats.Id[Int] = 15