2

我正在尝试编写用于在 Scala 中编写系统测试的 DSL。在这个 DSL 中,我不想暴露某些操作可能异步发生(例如,因为它们是使用正在测试的 Web 服务实现的),或者可能发生错误(因为 Web 服务可能不可用)的事实,并且我们希望测试失败)。在这个答案中,不鼓励这种方法,但在用于编写测试的 DSL 上下文中,我并不完全同意这一点。我认为 DSL 会因为这些方面的引入而受到不必要的污染。

为了解决这个问题,请考虑以下 DSL:

type Elem = String

sealed trait TestF[A]
// Put an element into the bag.
case class Put[A](e: Elem, next: A) extends TestF[A]
// Count the number of elements equal to "e" in the bag.
case class Count[A](e: Elem, withCount: Int => A) extends TestF[A]

def put(e: Elem): Free[TestF, Unit] =
  Free.liftF(Put(e, ()))

def count(e: Elem): Free[TestF, Int] =
  Free.liftF(Count(e, identity))

def test0 = for {
  _ <- put("Apple")
  _ <- put("Orange")
  _ <- put("Pinneaple")
  nApples <- count("Apple")
  nPears <- count("Pear")
  nBananas <- count("Banana")
} yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas))

现在假设我们要实现一个解释器,它利用我们的测试服务来放置和计算商店中的元素。由于我们使用网络,我希望put操作异步进行。此外,鉴于可能发生网络错误或服务器错误,我希望程序在发生错误时立即停止。为了说明我想要实现的目标,这里是一个通过 monad 转换器混合 Haskell 中不同方面的示例(我无法转换为 Scala)。

M所以我的问题是,你会为满足上述要求的解释器使用哪个 monad :

def interp[A](cmd: TestF[A]): M[A]

如果 M 是一个 monad 转换器,您将如何使用您选择的 FP 库(Cats、Scalaz)来组合它们。

4

1 回答 1

1

Task(scalaz 或更好的 fs2)应该满足所有要求,它不需要 monad-transformer,因为它已经在Either里面了(Either对于 fs2,\/对于 scalaz)。它还具有您需要的快速失败行为,与右偏析取/异或相同。

以下是我知道的几个实现:

  • Scalaz Task (original):很少过时的文档新资源
  • FS2 任务:https ://github.com/functional-streams-for-scala/fs2/blob/series/0.9/docs/guide.md它还提供与scalaz和的互操作性(类型类)cats
  • Monix 任务:https ://monix.io/docs/2x/eval/task.html
  • “Cats”不提供任何Task或其他IO与-monad 相关的操作(根本没有scalaz-effect模拟),建议使用 Monix 或 FS2。

不管 monad-transformer 没有,你在使用时仍然需要提升Task

  • 从值到Task
  • EitherTask

但是,是的,它似乎确实比 monad 转换器更简单,特别是考虑到 monad 几乎不可组合的事实 - 为了定义 monad 转换器,除了作为 monad 之外,您还必须了解有关您的类型的一些其他细节(通常它需要像 comonad 这样的东西提取价值)。

仅出于广告目的,我还要补充一点,它Task代表堆栈安全的蹦床计算。

但是,有一些项目专注于扩展 monad 组合,例如 Emm-monad:httpsFuture ://github.com/djspiewak/emm,因此您可以使用/ TaskEitherOption等来组合 monad 转换器List。但是,IMO,与组合相比,它仍然受到限制Applicative-提供了允许轻松组合任何 Applicative 的cats通用数据类型,您可以在这个答案中找到一些示例- 这里唯一的缺点是很难使用 Applicative 构建可读的 DSL。另一种选择是所谓的“Freer monad”:https ://github.com/m50d/paperdollNested,这基本上提供了更好的组合,并允许将不同的效果层分离到不同的解释器中。

例如,由于没有FutureT/TaskT转换器,您无法构建像type E = Option |: Task |: Base( Optionfrom 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 _DSLJava/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//FutureOption任何组合。事实上,您可以使用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
于 2016-11-12T04:22:29.147 回答