Scala DSL系统测试的实用免费Monad:并发性和错误处理

Scala DSL系统测试的实用免费Monad:并发性和错误处理,scala,scalaz,monad-transformers,free-monad,scala-cats,Scala,Scalaz,Monad Transformers,Free Monad,Scala Cats,我正在尝试编写一个DSL,用于在Scala中编写系统测试。在这个DSL中,我不想公开某些操作可能是异步进行的(因为它们是使用测试中的web服务实现的),或者可能发生错误(因为web服务可能不可用,我们希望测试失败)。这种方法是不被鼓励的,但在编写测试的DSL环境中,我并不完全同意这一点。我认为DSL会因为这些方面的引入而受到不必要的污染 为了解决这个问题,请考虑下面的DSL: type Elem = String sealed trait TestF[A] // Put an element

我正在尝试编写一个DSL,用于在Scala中编写系统测试。在这个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))
如果M是单子变换器,您将如何使用您选择的FP库(Cats、Scalaz)组合它们

任务
(scalaz或更好的fs2)应满足所有要求,它不需要monad变压器,因为它的内部已经有
用于fs2,或
\/
用于scalaz)。它还具有您需要的快速失败行为,与右偏析取/xor相同

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

  • Scalaz任务(原件):和
  • FS2任务:它还提供与
    scalaz
    cats
  • Monix任务:
  • “Cats”不提供任何
    任务
    或其他
    IO
    -单子相关操作(完全没有
    scalaz效应
    模拟),建议使用Monix或FS2
不管monad transformer是否存在,在使用
任务时仍然需要提升:

  • 从值到任务
  • 任一
    任务
  • 但是,是的,它似乎比monad transformer更简单,尤其是在monad几乎不可组合的事实方面——为了定义monad transformer,除了作为monad之外,您还必须了解一些关于您的类型的其他细节(通常需要类似于comonad的东西来提取值)

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

    然而,有一些项目专注于扩展的单子合成,如Emm monad:,因此您可以使用
    未来
    /
    任务
    选项
    列表
    等来合成单子变压器。但是,在我看来,与
    Applicative
    composition相比,它仍然有限-
    cats
    提供了通用的
    嵌套的
    数据类型,可以轻松地组合任何应用程序,您可以找到一些示例-这里唯一的缺点是难以使用Applicative构建可读的DSL。另一种选择是所谓的“自由单子”:,它基本上提供了更好的构图,并允许将不同的效果层分离到不同的口译员中

    例如,由于没有
    FutureT
    /
    TaskT
    transformer,您无法构建类似
    type E=Option |:Task |:Base
    Option
    来自
    任务
    )的效果,因此
    平面图
    需要从
    Future
    /
    任务
    中提取值

    总之,,我可以说,根据我的经验,
    Task
    对于基于do符号的DSL来说真的很有用:我有一个复杂的外部规则,比如用于异步计算的DSL,当我决定将它全部迁移到Scala嵌入式版本时,
    Task
    真的很有帮助-我将外部DSL转换为Scala的
    ,以便于理解。我们考虑的另一件事是有一些自定义类型,比如
    ComputationRule
    ,上面定义了一组类型类,以及到
    Task
    /
    Future
    或我们需要的任何转换,但这是因为我们没有明确使用
    Free
    -monad


    假设您不需要切换解释器的能力(这可能仅适用于系统测试),您甚至不需要免费的monad。在这种情况下,
    任务
    可能是您唯一需要的东西-它是惰性的(与未来相比),真正的功能性和堆栈安全性:

     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))
     }
    
    差异/缺点(与之相比):

    • 您一直使用
      任务
      类型,因此无法轻松将其折叠到其他monad
    • 当您传递DSL trait的实例(而不是自然转换)时,实现在运行时得到解决,您可以使用eta扩展轻松地对其进行抽象:
      test0\uu
      。Java/Scala自然支持多态方法(put、count),但poly函数不支持,因此传递包含
      t=>Task[Unit]
      (用于
      put
      操作)的
      DSL
      实例要比生成合成多态函数
      DSLEntry[t]=>Task[Unit]更容易
      使用自然变换
      DSLEntry~>任务

    • 在自然转换中没有显式AST而不是模式匹配——我们在DSL特性中使用静态分派(显式调用方法,它将返回惰性计算)

    实际上,您甚至可以在这里摆脱
    任务

     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
    }
    
    很简单。当然,您可以选择
    任务
    /
    未来
    /
    选项
    或任何组合(如果您愿意)。事实上,您可以使用
    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
    ,请注意任务的
    |@|
    不会并行执行(至少在较旧的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
    
    import cats.syntax_
    def test0[M[41;]:Monad](d:DSL[M])={
    为了{
    
    scala> test0(IdDsl)
    res2: cats.Id[List[(String, Int)]] = List((Apple,5), (Pears,5), (Bananas,5))
    
    def test0[F[_]: Applicative](dsl: DSL[F]) = 
      dsl.count("Apple") |@| dsl.count("Pinapple apple pen") map {_ + _ }
    
    scala> test0(IdDsl)
    res8: cats.Id[Int] = 10
    
    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