You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用ScalaCheck生成带Numeric上下文绑定的AnyVal类型任意值

问题

如何用ScalaCheck的arbitrary API与生成器,生成带有Numeric[A]上下文绑定、且A为AnyVal类型的任意数值(结果为Arbitrary或Gen均可)?

具体场景

Squants库允许在Numeric上下文绑定下用任意数值构造Quantity,核心签名如下:

trait LengthUnit extends UnitOfMeasure[Length] with UnitConverter {
  def apply[A](n: A)(implicit num: Numeric[A]) = Length(n, this)
}

测试该apply方法时,尝试编写如下代码但出现编译错误:

import squants.space.Length

// 简化示例
class LengthSuite extends MyDefaultSpec:
    "Length" - {
        "can be constructed using any number" in:
            // 将Number改为Double可解决编译错误,但会排除非Double的AnyVal类型
            forAll( arbitrary[ Number ] ): // 生成的是java.lang.Number类型
                value =>
                    import squants.space.LengthConversions._
                    // 编译错误:Number没有meters方法
                    value.meters should be( value.meters )
    }
end LengthSuite

问题根源:ScalaCheck的arbitrary[Number]会生成多种AnyVal类型的java.lang.Number实例,这些AnyVal本身都有对应的Numeric实例,但java.lang.Number没有Numeric实例,导致编译失败。

补充:Gen提供的posNum等方法仅能生成单一类型的AnyVal,而arbitrary[Number]可生成所有类型。已尝试的失败方案包括手动写生成器导致联合类型问题、匹配类型获取Numeric实例但编译器无法统一类型、向下转型获取Numeric实例等。


解决方案

一、可以实现目标生成器:生成带Numeric约束的AnyVal数值

核心思路是创建一个封装具体数值类型及其对应Numeric实例的代数数据类型(ADT),通过ADT将数值和上下文绑定打包,让编译器能正确推导类型约束。

步骤1:定义ADT封装数值与Numeric实例

import scala.math.Numeric

sealed trait NumericAnyVal {
  type A <: AnyVal
  val value: A
  val num: Numeric[A]
}

case class NByte(value: Byte)(implicit val num: Numeric[Byte]) extends NumericAnyVal { type A = Byte }
case class NShort(value: Short)(implicit val num: Numeric[Short]) extends NumericAnyVal { type A = Short }
case class NInt(value: Int)(implicit val num: Numeric[Int]) extends NumericAnyVal { type A = Int }
case class NLong(value: Long)(implicit val num: Numeric[Long]) extends NumericAnyVal { type A = Long }
case class NFloat(value: Float)(implicit val num: Numeric[Float]) extends NumericAnyVal { type A = Float }
case class NDouble(value: Double)(implicit val num: Numeric[Double]) extends NumericAnyVal { type A = Double }

步骤2:编写Gen[NumericAnyVal]生成器

将ScalaCheck提供的各数值类型生成器组合,随机选择生成:

import org.scalacheck.Gen
import org.scalacheck.Arbitrary

val numericAnyValGen: Gen[NumericAnyVal] = Gen.oneOf(
  Gen.byte.map(NByte(_)),
  Gen.short.map(NShort(_)),
  Gen.int.map(NInt(_)),
  Gen.long.map(NLong(_)),
  Gen.float.map(NFloat(_)),
  Gen.double.map(NDouble(_))
)

// 可选:转为Arbitrary实例,方便直接用arbitrary调用
implicit val arbitraryNumericAnyVal: Arbitrary[NumericAnyVal] = Arbitrary(numericAnyValGen)

步骤3:在测试中使用

import squants.space.Length

class LengthSuite extends MyDefaultSpec:
    "Length" - {
        "can be constructed using any numeric AnyVal" in:
            forAll(numericAnyValGen): nav =>
                import squants.space.LengthConversions._
                // 利用ADT中携带的Numeric上下文调用构造方法
                val length = meters[nav.A](nav.value)(nav.num)
                // 验证构造结果与原始数值一致(以转换为Double为例)
                length.toMeters shouldBe nav.num.toDouble(nav.value)
    }
end LengthSuite

二、替代方案:针对每种数值类型单独测试

如果不想定义ADT,可直接针对所有支持的AnyVal数值类型逐一编写测试,覆盖所有场景:

class LengthSuite extends MyDefaultSpec:
    "Length" - {
        "can be constructed using Byte" in:
            forAll(Gen.byte): b =>
                import squants.space.LengthConversions._
                b.meters.toMeters shouldBe b.toDouble

        "can be constructed using Short" in:
            forAll(Gen.short): s =>
                import squants.space.LengthConversions._
                s.meters.toMeters shouldBe s.toDouble

        "can be constructed using Int" in:
            forAll(Gen.int): i =>
                import squants.space.LengthConversions._
                i.meters.toMeters shouldBe i.toDouble

        // 同理添加Long、Float、Double的测试案例
    }
end LengthSuite

为什么直接生成A <: AnyVal with Numeric[A]不可行?

Scala的类型系统中,Numeric[A]是上下文约束而非类型上界,无法直接定义Gen[A]同时满足A <: AnyVal和Numeric[A]。ScalaCheck生成器需要明确的具体类型,而Byte | Short | Int | ...这类联合类型无法自动推导每个分支对应的Numeric实例,导致编译器无法在测试代码中找到合适的上下文绑定。


内容的提问来源于stack exchange,提问作者Joost Papendorp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 08:28:14