用于系统测试 DSL 的实用免费 monad:并发性和错误处理
Practical free monads for system-tests DSL: concurrency and error handling
我正在尝试编写用于在 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
操作异步进行。此外,鉴于可能发生网络错误或服务器错误,我希望程序在发生错误时立即停止。为了说明我想要实现的目标,here 是一个通过 monad 转换器(我无法翻译成 Scala)在 Haskell 中混合不同方面的示例。
所以我的问题是,您会使用哪个 monad M
作为满足上述要求的解释器:
def interp[A](cmd: TestF[A]): M[A]
如果 M 是一个单子变换器,您将如何使用您选择的 FP 库(Cats、Scalaz)来组合它们。
Task
(scalaz 或更好的 fs2)应该满足所有要求,它不需要 monad-transformer,因为它内部已经有 Either
(Either
for fs2, \/
对于鳞鳞鱼)。它还具有您需要的快速失败行为,与右偏 disjunction/xor.
相同
以下是我所知道的几个实现:
- Scalaz 任务(原创):little outdated doc and new sources
- 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
或
- 从
Either
到 Task
但是,是的,它似乎确实比 monad 转换器更简单,尤其是在 monad 几乎不可组合的事实方面——为了定义 monad 转换器,除了是 monad 之外,你还必须知道关于你的类型的一些其他细节(通常它需要像 comonad 这样的东西来提取价值)。
出于广告目的,我还要补充一点,Task
表示堆栈安全的蹦床计算。
但是,有一些项目专注于扩展单子组合,例如 Emm-monad:https://github.com/djspiewak/emm, so you can compose monad transformers with Future
/Task
, Either
, Option
, List
and so on and so forth. But, IMO, it's still limited in comparison with Applicative
composition - cats
provides universal Nested
data type that allows to easily compose any Applicative, you can find some examples - the only disadvantage here is that it's hard to build a readable DSL using Applicative. Another alternative is so-called "Freer monad": https://github.com/m50d/paperdoll,它基本上提供了更好的组合,并允许将不同的效果层分离到不同的解释器中。
例如,由于没有 FutureT
/TaskT
转换器,您无法构建像 type E = Option |: Task |: Base
(Option
来自 Task
)这样的效果这样 flatMap
需要从 Future
/Task
.
中提取值
作为结论,我可以说根据我的经验 Task
确实适用于基于 do-notation 的 DSL:我有一个复杂的类似外部规则的 DSL 用于异步计算,当我决定迁移时这一切到 Scala 嵌入式版本 Task
真的很有帮助——我从字面上将外部 DSL 转换为 Scala 的 for-comprehension
。我们考虑的另一件事是有一些自定义类型,比如 ComputationRule
定义了一组类型 类 以及到 Task
/Future
或我们需要的任何类型的转换,但是这是因为我们没有明确地使用 Free
-monad。
你甚至可能不需要 Free
-monad 假设你不需要切换解释器的能力(这可能适用于 just 系统测试)。在那种情况下,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))
}
因此您可以通过在此处传递不同的 "interpreter" 来切换实现:
test0(Implementation1).unsafeRun
test0(Implementation2).unsafeRun
Differences/Disadvantages(与http://typelevel.org/cats/datatypes/freemonad.html相比):
- 你坚持使用
Task
类型,所以你不能轻易地将它折叠成其他一些 monad。
当你传递一个 DSL-trait 的实例(而不是自然转换)时,实现在运行时被解析,你可以使用 eta-expansion 轻松地抽象它:test0 _
。 Java/Scala 自然支持多态方法(put、count),但多函数不支持,因此传递包含 T => Task[Unit]
的 DSL
实例更容易(对于 put
操作)比使用自然变换 DSLEntry ~> Task
.
制作合成多态函数 DSLEntry[T] => Task[Unit]
没有显式 AST 而不是自然转换中的模式匹配 - 我们在 DSL trait
中使用静态分派(显式调用方法,这将 return 惰性计算)
实际上,您甚至可以在这里去掉 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
定义为 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
我正在尝试编写用于在 Scala 中编写系统测试的 DSL。在此 DSL 中,我不想暴露某些操作可能异步发生的事实(例如,因为它们是使用被测 Web 服务实现的),或者可能发生错误(因为 Web 服务可能不可用) ,我们希望测试失败)。
要构建问题,请考虑以下 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 是一个通过 monad 转换器(我无法翻译成 Scala)在 Haskell 中混合不同方面的示例。
所以我的问题是,您会使用哪个 monad M
作为满足上述要求的解释器:
def interp[A](cmd: TestF[A]): M[A]
如果 M 是一个单子变换器,您将如何使用您选择的 FP 库(Cats、Scalaz)来组合它们。
Task
(scalaz 或更好的 fs2)应该满足所有要求,它不需要 monad-transformer,因为它内部已经有 Either
(Either
for fs2, \/
对于鳞鳞鱼)。它还具有您需要的快速失败行为,与右偏 disjunction/xor.
以下是我所知道的几个实现:
- Scalaz 任务(原创):little outdated doc and new sources
- 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
或 - 从
Either
到Task
但是,是的,它似乎确实比 monad 转换器更简单,尤其是在 monad 几乎不可组合的事实方面——为了定义 monad 转换器,除了是 monad 之外,你还必须知道关于你的类型的一些其他细节(通常它需要像 comonad 这样的东西来提取价值)。
出于广告目的,我还要补充一点,Task
表示堆栈安全的蹦床计算。
但是,有一些项目专注于扩展单子组合,例如 Emm-monad:https://github.com/djspiewak/emm, so you can compose monad transformers with Future
/Task
, Either
, Option
, List
and so on and so forth. But, IMO, it's still limited in comparison with Applicative
composition - cats
provides universal Nested
data type that allows to easily compose any Applicative, you can find some examples
例如,由于没有 FutureT
/TaskT
转换器,您无法构建像 type E = Option |: Task |: Base
(Option
来自 Task
)这样的效果这样 flatMap
需要从 Future
/Task
.
作为结论,我可以说根据我的经验 Task
确实适用于基于 do-notation 的 DSL:我有一个复杂的类似外部规则的 DSL 用于异步计算,当我决定迁移时这一切到 Scala 嵌入式版本 Task
真的很有帮助——我从字面上将外部 DSL 转换为 Scala 的 for-comprehension
。我们考虑的另一件事是有一些自定义类型,比如 ComputationRule
定义了一组类型 类 以及到 Task
/Future
或我们需要的任何类型的转换,但是这是因为我们没有明确地使用 Free
-monad。
你甚至可能不需要 Free
-monad 假设你不需要切换解释器的能力(这可能适用于 just 系统测试)。在那种情况下,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))
}
因此您可以通过在此处传递不同的 "interpreter" 来切换实现:
test0(Implementation1).unsafeRun
test0(Implementation2).unsafeRun
Differences/Disadvantages(与http://typelevel.org/cats/datatypes/freemonad.html相比):
- 你坚持使用
Task
类型,所以你不能轻易地将它折叠成其他一些 monad。 当你传递一个 DSL-trait 的实例(而不是自然转换)时,实现在运行时被解析,你可以使用 eta-expansion 轻松地抽象它:
test0 _
。 Java/Scala 自然支持多态方法(put、count),但多函数不支持,因此传递包含T => Task[Unit]
的DSL
实例更容易(对于put
操作)比使用自然变换DSLEntry ~> Task
. 制作合成多态函数 没有显式 AST 而不是自然转换中的模式匹配 - 我们在 DSL trait
中使用静态分派(显式调用方法,这将 return 惰性计算)
DSLEntry[T] => Task[Unit]
实际上,您甚至可以在这里去掉 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
定义为 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