【问题标题】:Testing laws of side-effecting monad测试副作用单子的规律
【发布时间】:2014-10-25 12:27:55
【问题描述】:

我正在编写一个库来通过 API 访问 Web 服务。我已经定义了简单的类来表示 API 操作

case class ApiAction[A](run: Credentials => Either[Error, A])

以及一些执行 Web 服务调用的函数

// Retrieve foo by id
def get(id: Long): ApiAction[Foo] = ???

// List all foo's
def list: ApiAction[Seq[Foo]] = ???

// Create a new foo
def create(name: String): ApiAction[Foo] = ???

// Update foo
def update(updated: Foo): ApiAction[Foo] = ???

// Delete foo
def delete(id: Long): ApiAction[Unit] = ???

我还把 ApiAction 做成了一个单子

implicit val monad = new Monad[ApiAction] { ... }

所以我可以做类似的事情

create("My foo").run(c)
get(42).map(changeFooSomehow).flatMap(update).run(c)
get(42).map(_.id).flatMap(delete).run(c)

现在我在测试它的单子定律时遇到了麻烦

val x = 42
val unitX: ApiAction[Int] = Monad[ApiAction].point(x)

"ApiAction" should "satisfy identity law" in {
  Monad[ApiAction].monadLaw.rightIdentity(unitX) should be (true)
}

因为monadLaw.rightIdentity 使用equal

def rightIdentity[A](a: F[A])(implicit FA: Equal[F[A]]): Boolean = 
  FA.equal(bind(a)(point(_: A)), a)

并且没有Equal[ApiAction]

[error] could not find implicit value for parameter FA: scalaz.Equal[ApiAction[Int]]
[error]     Monad[ApiAction].monadLaw.rightIdentity(unitX) should be (true)
[error]                                            ^

问题是我什至无法想象如何定义Equal[ApiAction]ApiAction 本质上是一个函数,我不知道函数上有任何相等关系。当然可以比较运行ApiAction的结果,但是不一样。

我觉得我做的事情非常错误或不理解一些重要的事情。所以我的问题是:

  • ApiAction 是一个 monad 有意义吗?
  • ApiAction 是我设计的吗?
  • 我应该如何测试它的单子定律?

【问题讨论】:

  • 无限域的函数没有可计算的等式关系。您可以使 run 成为密封特征的抽象方法,然后将您的 ApiActions 实现为派生案例类和案例对象。

标签: scala functional-programming monads equality scalaz


【解决方案1】:

我将从简单的开始:是的,ApiAction 是一个单子是有意义的。是的,你以合理的方式设计了它——这个设计看起来有点像 Haskell 中的 IO monad。

棘手的问题是你应该如何测试它。

唯一有意义的相等关系是“在给定相同输入的情况下产生相同的输出”,但这仅在纸面上真正有用,因为计算机无法验证,并且仅对纯函数有意义。实际上,Haskell 的 IO monad 与您的 monad 有一些相似之处,但没有实现 Eq。因此,如果您不实施Equal[ApiAction],您可能会处于安全状态。

仍然可能有一个参数用于实现一个特殊的 Equal[ApiAction] 实例,仅用于测试,它使用硬编码的 Credentials 值(或少量硬编码值)运行操作并比较结果。从理论的角度来看,这很糟糕,但从实用的角度来看,它并不比使用测试用例测试更糟糕,并且可以让您重用 Scalaz 中现有的辅助函数。

另一种方法是忘记 Scalaz,使用铅笔和纸证明 ApiAction 满足 monad 定律,并编写一些测试用例来验证一切是否按照您认为的方式运行(使用您认为的方法)我写的,不是来自 Scalaz 的)。事实上,大多数人会跳过纸笔步骤。

【讨论】:

  • 很好的答案!你说的太对了,平等只对纯函数有意义,apiAction.run == apiAction.run 通常不成立,因为有副作用,为了用铅笔和纸证明定律,我们应该假设动作永远不会失败。
【解决方案2】:

归结为 lambdas 是 FunctionN 的匿名子类,其中只有实例相等,因此只有相同的匿名子类与自身相等。

关于如何做到这一点的一个想法: 使您的操作成为 Function1 的具体子类,而不是实例:

abstract class ApiAction[A] extends (Credentials => Either[Error, A])
// (which is the same as)
abstract class ApiAction[A] extends Function1[Credentials, Either[Error, A]]

这将允许您例如为您的实例创建案例对象

case class get(id: Long) extends ApiAction[Foo] {
  def apply(creds: Credentials): Either[Error, Foo] = ... 
}

这反过来应该允许您以适合您的方式为 ApiAction 的每个子类实现 equals,例如在构造函数的参数上。 (你可以像我一样免费获得操作案例类)

val a = get(1)
val b = get(1)
a == b

您也可以这样做,而无需像我一样扩展 Function1 并像您一样使用运行字段,但这种方式提供了最简洁的示例代码。

【讨论】:

  • 那么不可能在ApiAction上定义monad,因为point是未定义的(你不能创建抽象类的实例)。
  • 但是你可以为它创建一个身份/点吗? case class point(a: A) extends ApiAction[A] { def apply(creds: Credentials): Either[Error, Foo] = Right(a) }
【解决方案3】:

我认为您可以通过必须使用宏或反射将您的函数包装到包含 AST 的类中的缺点来实现它。然后,您可以通过比较它们的 AST 来比较两个函数。

What's the easiest way to use reify (get an AST of) an expression in Scala?

【讨论】:

  • 我认为这不是一个好的解决方案。我只是想以一种功能性的方式设计库,并认为我做错了。不管怎样,谢谢你。与您的答案相关 - 可以以不同的方式实现具有相同行为的功能;)
猜你喜欢
  • 2014-03-29
  • 1970-01-01
  • 2011-04-05
  • 2020-03-19
  • 2012-07-18
  • 2020-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多