2016-11-11 44 views
2

我正在尝试编写用于在Scala中编写系统测试的DSL。在这个DSL中,我不想公开某些操作可能异步发生的事实(因为它们是使用被测试的Web服务实现的),或者可能发生错误(因为Web服务可能不可用,我们希望测试失败)。 In this answer这种方法令人沮丧,但我并不完全同意在编写测试的DSL环境中。我认为DSL会因这些方面的介绍而受到不必要的污染。用于系统测试的实用免费monads 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操作异步发生。另外,考虑到网络错误或服务器错误可能发生,我希望程序在发生错误时立即停止。为了说明我想实现的目标,here是一个通过单变换器(我无法翻译为Scala)在Haskell中混合不同方面的示例。

所以我的问题是,你会用满足上述要求的解释这单子M

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

而如果M是单子变压,你会如何使用的FP库撰写他们你的选择(猫,斯卡拉兹)。

+1

'Task'(scalaz或更好FS2)应满足所有的要求,它不需要单子变压器,因为它是已经有了要么内(无论是FS2, \/for scalaz)。它也具有你需要的快速失败行为,与正确的偏向/异或行相同。 – dk14

+0

我不知道“Task”,很好。这种方法似乎也暗示了人们如何在Scala世界中构建monad,忘记Haskell中的'lift'运算符,定义您需要混合的所有方面(例如并发和错误处理)的自己的类,并定义一个monad实例。 –

+0

当使用'Task'时,你仍然需要解除,从值提升到Task,或者从Either到Task。但是,是的,它似乎比monad变换器更简单,尤其是在monad几乎不可组合的情况下(为了定义monad变换器,除了作为monad外,你还需要知道关于你的类型的一些其他细节 - 通常它需要像comonad提取价值)。仅仅为了广告的目的,我还要补充一点,'Task'表示堆栈安全的蹦床计算。但是有一些项目着重于一元组合,比如'Emm-monad' – dk14

回答

1

Task(scalaz或更好FS2)应满足所有的要求,它不需要单子变压器,因为它是已经Either内(Either对于FS2,\/为scalaz)。它也具有您需要的快速失败行为,与右偏分离/异或相同。

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

不管单子变压器没有,你还在使用Task时还挺需要提升:

  • 从价值Task
  • EitherTask

但是,是的,它似乎比单子变压器更简单,尤其是单子几乎不可组合的事实 - 为了定义m onad变压器,你必须知道一些关于你的类型的其他细节,除了是一个单子(通常它需要像comonad提取价值)。

仅用于广告目的,我还会补充说Task表示堆栈安全的蹦床计算。

不过,也有一些项目专注于扩展单子组成,像EMM-单子:https://github.com/djspiewak/emm,这样你就可以撰写单子变压器与Future/TaskEitherOptionList等等等等。但是,国际海事组织与Applicative相比仍然有限 - cats提供了通用的Nested数据类型,可以轻松组成任何应用程序,您可以找到一些示例in this answer - 这里唯一的缺点是使用Applicative很难构建可读的DSL。另一种选择是所谓的“自由单体”:https://github.com/m50d/paperdoll,它基本上提供了更好的构图,并且允许将不同的效果层分成不同的解释器。

例如,如没有FutureT/变压器你不能建立像type E = Option |: Task |: BaseOptionTask)的效果,例如flatMap将需要从Future/Task的值提取。

作为一个结论,我可以说从我的经验Task真的出现在基于do-notation的DSL中:我有一个复杂的外部规则 - 如异步计算的DSL,当我决定将它全部迁移到Scala-嵌入式版本Task真的帮助 - 我从字面上将外部DSL转换为Scala的for-comprehension。我们考虑的另一件事是自定义类型,例如ComputationRule,其中定义了一组类型类以及Task/Future或任何我们需要的转换,但这是因为我们没有明确使用Free -monad。


你可能甚至不需要在这里Free -monad假设你不需要切换口译的能力(可能为只是系统测试是真实的)。在这种情况下Task可能是你唯一需要的东西 - 这是懒惰的(与未来比较),真正做到了功能和堆栈安全:

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类型,所以你不能把它崩来点它很容易monad。
  • 实现在运行时通过DSL特性的实例(而不是自然转换)解决,您可以使用eta-expansion:test0 _轻松进行抽象。 Java/Scala自然支持多态方法(put,count),但poly函数并不是那么容易通过包含T => Task[Unit](对于put操作)的DSL实例,而不是使用自然变换DSLEntry ~> Task来生成合成多态函数DSLEntry[T] => Task[Unit]

  • 没有明确AST作为替代模式自然转化内匹配 - 我们使用静态调度(显式调用的方法,这将返回懒计算)DSL特质里面

事实上,你甚至可以摆脱这里的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 
} 

注意,猫有一个MonadId定义的,因此:

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,注意|@|任务不执行(至少在旧的斯拉拉版本)并行(并行运算符不等于并行计算)。您也可以使用两者的结合:

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