如何收缩列表且保证非空?ScalaCheck收缩器问题求助
嘿,针对你的两个Scala/ScalaCheck问题,我来一步步帮你梳理解决方案:
问题1:实现带非空约束的列表收缩操作
收缩的核心目标是生成原列表的更小但始终非空的变体,用来定位最小的失败案例。要保证收缩后列表不为空,关键在于过滤掉所有会产生空列表的收缩候选。
给你一个Scala下的通用实现思路:
- 如果列表长度为1,那它已经是最小的非空单元了,没有可收缩的空间,返回空的候选流
- 如果列表长度大于1,我们可以生成多种非空的收缩候选,比如:
- 去掉最后一个元素的子列表
- 去掉第一个元素的子列表
- 把每个元素单独拆成单元素列表
对应的代码示例:
def nonEmptyListShrink[A](list: List[A]): Stream[List[A]] = list match { case _ :: Nil => Stream.empty // 长度为1,没法再缩小且保持非空 case _ => val candidates = Seq( list.init, // 截断尾部 list.tail, // 截断头部 list.map(List(_)) // 拆成单个元素的列表 ).filter(_.nonEmpty).distinct // 确保候选非空且去重 candidates.toStream }
问题2:修复ScalaCheck中非空列表的收缩约束问题
你遇到的问题根源在于:ScalaCheck的默认Shrink[List[A]]实现允许收缩到空列表,但你的nonEmptyListsOfString生成器明确保证了列表非空,两者的约束不匹配。解决方法是为你的非空列表定制一个遵守约束的收缩器。
方案1:直接为List[String]定制收缩器
我们可以基于上面的非空收缩逻辑,实现ScalaCheck的Shrink类型类实例:
import org.scalacheck.Shrink import org.scalacheck.Arbitrary import org.scalacheck.Prop import org.scalacheck.Gen // 为非空List[String]提供自定义收缩器 implicit val nonEmptyStringListShrink: Shrink[List[String]] = Shrink { list => list match { case _ :: Nil => Stream.empty case _ => val candidates = Seq( list.init, list.tail, list.map(List(_)) ).filter(_.nonEmpty).distinct candidates.toStream } } // 你的生成器和测试逻辑 val nonEmptyListsOfString = Gen.nonEmptyListOf(Arbitrary.arbitrary[String]) val failingProp = Prop.forAll(nonEmptyListsOfString) { strs => strs == Nil // 必然失败的测试 } failingProp.check()
现在运行测试,ScalaCheck会输出最小的非空失败示例(比如List("")),而不是空列表Nil,因为我们的收缩器会过滤掉所有空列表候选。
方案2:用类型标记实现更严格的非空约束
如果想从类型层面彻底避免空列表的混淆,可以定义一个专门的非空列表类型,然后为它提供Gen和Shrink实例:
import org.scalacheck.{Gen, Shrink, Prop, Arbitrary} // 定义非空列表的类型,从构造上保证非空 case class NonEmptyList[A](head: A, tail: List[A]) { def toList: List[A] = head :: tail } // 为NonEmptyList生成生成器 val nonEmptyListGen: Gen[NonEmptyList[String]] = for { head <- Arbitrary.arbitrary[String] tail <- Gen.listOf(Arbitrary.arbitrary[String]) } yield NonEmptyList(head, tail) // 为NonEmptyList生成收缩器 implicit val nonEmptyListShrink: Shrink[NonEmptyList[String]] = Shrink { nel => val fullList = nel.toList fullList match { case _ :: Nil => Stream.empty case _ => val candidates = Seq( fullList.init, fullList.tail, fullList.map(List(_)) ).filter(_.nonEmpty).distinct.map { case h :: t => NonEmptyList(h, t) } candidates.toStream } } // 测试属性 val failingProp = Prop.forAll(nonEmptyListGen) { nel => nel.toList == Nil } failingProp.check()
这种方式更类型安全,从根源上杜绝了空列表的出现,适合在需要严格非空约束的场景使用。
内容的提问来源于stack exchange,提问作者ashawley
相关产品推荐
相关产品推荐

