测试副作用monad的定律

时间:2014-09-01 09:48:17

标签: scala functional-programming monads equality scalaz

我正在编写一个库来通过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成为monad

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)

现在我有麻烦测试其monad法律

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吗?
  • 我应该如何测试其monad法律?

3 个答案:

答案 0 :(得分:4)

我将从简单的开始:是的,ApiAction成为monad是有道理的。是的,你已经以合理的方式设计了它 - 这个设计看起来有点像Haskell中的IO monad。

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

唯一有意义的平等关系是"在给定相同输入的情况下产生相同的输出"但这只在纸上非常有用,因为它不可能被计算机验证,它只对纯粹的功能有意义。事实上,Haskell的IO monad与你的monad有一些相似之处,并没有实现Eq。如果你没有实施Equal[ApiAction],那么你可能已经安全了。

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

另一种方法是忘记Scalaz,证明ApiAction满足使用铅笔和纸的monad定律,并编写一些测试用例来验证一切都按照你的想法运行(使用方法)你已经写过了,而不是来自Scalaz的那些。实际上,大多数人都会跳过铅笔纸步骤。

答案 1 :(得分:1)

归结为lambda是FunctionN的匿名子类,其中只有实例相等,所以只有相同的anonymus子类与它自身相同。

如何做到这一点的一个想法: 使您的操作成为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并像你一样使用run字段,但是这样做了最简单的示例代码。

答案 2 :(得分:0)

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

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