QuickCheck测试函数类型Combine的Semigroup结合律及相关疑问
1. Combine的Arbitrary实例与CoArbitrary作用原理
你写的Arbitrary (Combine a b)实例本质是复用了QuickCheck内置的函数类型a -> b的Arbitrary实例,只是在外层套了Combine的newtype包装器,Combine <$> arbitrary等价于把随机生成的a -> b函数封装成Combine类型的值,所以整个实例非常简洁。
CoArbitrary的作用是支撑随机函数的生成:
QuickCheck要生成符合逻辑的随机函数a -> b,核心要满足「不同的输入a可以得到不同的输出b」,这就需要一种能力:把a类型的值作为扰动因子,修改随机生成器的种子,从而得到不同的b类型随机值。CoArbitrary类型类就是定义这种能力的,它的核心方法是:
coarbitrary :: CoArbitrary a => a -> Gen r -> Gen r
这个方法接收一个a类型的值,把传入的生成器做一次种子扰动后返回。只要a实现了CoArbitrary,b实现了Arbitrary,QuickCheck就可以自动生成随机的a -> b函数:对于任意输入的a,先用coarbitrary a扰动生成器,再用扰动后的生成器生成b类型的输出,就得到了一个随机函数的行为。
你用到的Combine String String场景里,String已经内置了CoArbitrary实例,String也有Arbitrary实例,完全满足实例约束,所以可以正常生成随机的Combine值。
2. Combine类型的Show实例优化方案
Haskell的函数是运行时的闭包结构,默认不携带源代码、逻辑描述等元信息,所以不可能直接打印出函数的内部实现逻辑,但有几种实用的替代方案可以满足调试、测试需求:
- 方案1:利用QuickCheck的错误打印能力,不需要自定义Show实例
测试失败时你需要的其实是「调用参数、两边计算结果」这类调试信息,不需要打印Combine本身,你可以修改测试属性,用counterexample打印需要的信息:
测试失败时会自动打印出上面的所有信息,完全可以定位问题。import Test.QuickCheck (counterexample, Property, forAll, arbitrary) combineSemigroupAssoc :: (Eq b, Semigroup b, Show a, Show b) => a -> Combine a b -> Combine a b -> Combine a b -> Property combineSemigroupAssoc x a b c = let left = unCombine (a <> (b <> c)) x right = unCombine ((a <> b) <> c) x in counterexample ("输入参数x: " ++ show x) $ counterexample ("左边计算结果: " ++ show left) $ counterexample ("右边计算结果: " ++ show right) $ left == right - 方案2:侵入式带标签的Combine定义(仅用于测试场景)
如果你确实需要给Combine加可识别的Show信息,可以修改Combine的定义,新增一个标签字段存函数描述:
生成Arbitrary实例的时候可以自动生成随机的描述字符串,不过这种方式会修改原类型定义,只适合测试用,不建议生产环境用。newtype Combine a b = Combine { unCombine :: a -> b, desc :: String } instance Show (Combine a b) where show = desc - 方案3:导入
Text.Show.Functions模块
这个模块内置了函数类型的Show实例,会打印<function>,比你现在的实现更通用,不需要自己写Combine的Show实例,但也不会提供更多信息。
至于你说的查看实际调用参数,用方案1的counterexample就可以把所有用到的参数、计算结果都打印出来,不需要依赖Combine的Show实例。
内容的提问来源于stack exchange,提问作者Calin

