【问题标题】:Deriving arbitrary function instances using shapeless-scalacheck使用 shapeless-scalacheck 派生任意函数实例
【发布时间】:2016-11-04 07:16:21
【问题描述】:

升级到 scalacheck 1.13.3 后,我遇到了一个奇怪的问题,即派生 A => B Or C 的实例(其中 Or 本质上是一个轻量级 Either)几乎总是会失败。

这是我可以编写的重现问题的最简单代码:

import org.scalatest.FunSuite
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalacheck.Shapeless._

class Testor extends FunSuite with GeneratorDrivenPropertyChecks {
  sealed trait Or[+A, +B] extends Product with Serializable
  case class Left[A](a: A) extends Or[A, Nothing]
  case class Right[B](b: B) extends Or[Nothing, B]

  test("reproduce") {
    forAll { (i: Int, f: Int ⇒ Float Or Boolean) ⇒
      f(i)
    }
  }
}

这失败了:

RetrievalError was thrown during property evaluation.
  Message: couldn't generate value
  Occurred when passed generated values (
    arg0 = 0, // 30 shrinks
    arg1 = <function1>
    )

请注意,提供显式 Arbitrary[Float Or Boolean] 可以解决问题,因此很明显问题出在泛型派生中。

我不相信问题出在shapeless-scalacheck - 我尝试编写自己的泛型派生来查看它是否有帮助,但它以完全相同的方式失败了。

有些奇怪,但可能是由于任意函数的工作方式,函数实际上是生成的,但在评估时失败。

不胜感激任何帮助/建议,因为我有点卡住了。

【问题讨论】:

标签: scala shapeless scalacheck


【解决方案1】:

这里的问题似乎是双向的:

  • scalacheck (org.scalacheck.Test.Parameters.default) 和 scalatest 中的默认测试参数有一个 minSize0Prop.check 尝试在启动时生成具有此大小的值。

  • scalacheck-shapeless 中的MkCoproductArbitrary.ccons 在生成任意联积时将大小为零解释为终止条件,因此会失败。

作为临时解决方法,在 Travis 响应的示例中,可以通过以下方式检查属性:

prop.check(Test.Parameters.default.withMinSize(1))

使用问题中使用的 scalatest,也可以更改最小尺寸参数,可能通过放置类似的东西

implicit val config = PropertyCheckConfiguration(minSize = PosZInt(1))

在测试用例之前(警告:我没有尝试/检查 scalatest 解决方案,与之前的纯 scalacheck 不同)。

【讨论】:

  • 这可以通过更改/调整MkCoproductArbitrary.ccons 中的终止条件在 scalacheck-shapeless 中修复。我不记得在什么情况下大小会变成负数。有可能它实际上总是正数,并且终止条件可以更改为size &lt; 0(由于 scalacheck-shapeless 派生的任意值,大小会变为负数)。
  • 我试过implicit override val generatorDrivenConfig = PropertyCheckConfiguration(minSize = PosZInt(1)) 进行scalatest,这似乎不起作用。但这很好 - 你打算在scalacheck-shapeless 级别解决这个问题吗?我完全可以禁用这些测试,并在修复可用后立即重新启用它们
【解决方案2】:

这是一个很好的问题,我并没有真正的解决方案,但它与我遇到的其他一些问题类似,因为我们在 ScalaCheck 1.13 中有很好的 Arbitrary 函数实例。

首先,这是一个不依赖于 ScalaTest 的最小化:

import org.scalacheck._, Shapeless._

sealed trait Foo; case object Bar extends Foo

val prop = Prop.forAll { (f: Int => Foo) => f(0); true }

然后:

scala> prop.check
! Exception raised on property evaluation.
> ARG_0: <function1>
> Exception: org.scalacheck.Gen$RetrievalError: couldn't generate value
org.scalacheck.Gen.loop$1(Gen.scala:57)
org.scalacheck.Gen.doPureApply(Gen.scala:58)
...

当我之前在 1.13 中遇到过这样的事情时,核心问题一直是生成器在给定大小为零时出现故障,实际上罪魁祸首似乎是 this line 中的 case 0 =&gt; Gen.fail在 scalacheck-shapeless 中。

我们需要询问 Alexandre 是否确定,但这似乎是事后避免递归 ADT 上的堆栈溢出的尝试。这是上下文:

Gen.sized {
  case 0 => Gen.fail
  case size =>
    val sig = math.signum(size)

    Gen.frequency(
      1   -> Gen.resize(size - sig, Gen.lzy(headArbitrary.value.arbitrary)).map(Inl(_)),
      n() -> Gen.resize(size - sig, Gen.lzy(tailArbitrary.arbitrary.arbitrary)).map(Inr(_))
    )
}

在当前的实现中sig 将始终是1,所以我猜最初case 0 不存在并且意图是做类似math.min(0, size - 1) 的事情(它总是等于@ 987654332@ 用于非负整数)。

如果您删除 case 0 =&gt; Gen.fail 行(或者如果您将最小大小设置为大于零的任何值),您的代码就可以正常工作。问题是如果没有case 0 行,这样的 ADT 的派生实例可能会溢出堆栈:

sealed abstract class Tree
final case class Node(left: Tree, right: Tree, v: Int) extends Tree
case object Leaf extends Tree

…因为即使size0,您也可能会继续选择递归的Node 分支。

size 为零时,您想要的是能够说“给我一个值,但不是来自递归构造函数”之类的话,而且乍一看我看不到实现这一点的方法。

显然case 0 =&gt; Gen.fail 在我们新的Cogen-ful 世界中很糟糕。如果 scalacheck-shapeless 是我的库,我会很想做一些可怕的事情,比如删除该行,然后捕获 StackOverflowErrorGen.fail 用于递归 ADT。那将是一个 hack,但它仍然比目前的情况要好。

我们可能应该在 scalacheck-shapeless 问题中处理这个问题。与此同时,我只是手动为Or 编写Arbitrary(你应该可以让RightLeft 派生出来,所以这还不错)。

【讨论】:

  • 好的,我开了一个issue
猜你喜欢
  • 2018-10-28
  • 1970-01-01
  • 1970-01-01
  • 2016-09-17
  • 1970-01-01
  • 2011-05-21
  • 2020-12-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多