Scala DSL系统测试的实用免费Monad:并发性和错误处理
我正在尝试编写一个DSL,用于在Scala中编写系统测试。在这个DSL中,我不想公开某些操作可能是异步进行的(因为它们是使用测试中的web服务实现的),或者可能发生错误(因为web服务可能不可用,我们希望测试失败)。这种方法是不被鼓励的,但在编写测试的DSL环境中,我并不完全同意这一点。我认为DSL会因为这些方面的引入而受到不必要的污染 为了解决这个问题,请考虑下面的DSL: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
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