有无可自动匹配类型约束生成测试用例的Haskell测试套件?
问题背景
我在学习Haskell时重写了不少标准库函数,代码能通过类型检查,但不确定行为是否完全符合原函数。比如我实现了自己的liftA2:
-- 自定义liftA2实现 liftA2' :: Applicative app => (a -> b -> c) -> app a -> app b -> app c liftA2' f aa ab = f <$> aa <*> ab
我想验证它在任意函数f和任意Applicative实例下都和标准liftA2表现一致,但用QuickCheck的话必须指定具体的函数和类型实例,比如:
-- 仅针对(,)函数和[Int]实例的测试 quickCheck $ \aa bb -> liftA2 (,) aa bb === liftA2' (,) (aa :: [Int]) (bb :: [Int])
这种方式不仅写法繁琐,还只能覆盖特定场景,无法做到通用验证。我希望能写出类似check $ liftA2 === liftA2'的代码,让测试工具自动生成符合类型约束的多种测试用例,覆盖边界情况并报告覆盖率。
可用工具与方案
1. QuickCheck + polycheck
polycheck是QuickCheck的扩展,专门解决多态函数的通用测试问题。它借助GHC的类型系统自动推导多态类型的各种实例,无需手动指定具体类型或函数。用法极简:
import Test.QuickCheck.Poly check $ liftA2 === liftA2'
它会自动挑选多种常用Applicative实例(如[]、Maybe、Either e)和不同类型的函数f生成测试用例,覆盖通用场景。
2. Hedgehog
Hedgehog是比QuickCheck更灵活的属性测试库,原生支持简洁的多态测试写法。虽然需要编写少量生成器代码,但它的多态生成器能自动适配不同类型,轻松扩展到各类Applicative实例:
import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range prop_liftA2_equiv :: Property prop_liftA2_equiv = property $ do f <- forAll $ Gen.fun (Gen.int Range.linearBounded) aa <- forAll $ Gen.list (Range.linear 0 10) Gen.int bb <- forAll $ Gen.list (Range.linear 0 10) Gen.int liftA2 f aa bb === liftA2' f aa bb
若要测试Maybe这类其他实例,只需把Gen.list换成Gen.maybe即可,无需大幅修改测试代码。
3. inspection-testing(编译时语义验证)
如果想直接证明两个函数语义完全等价,而非依赖测试用例抽样验证,可以使用inspection-testing这个GHC插件。它在编译阶段检查两个函数的核心中间表示是否一致,适合像liftA2'这种由标准组合子直接构成的实现:
{-# OPTIONS_GHC -fplugin Test.Inspection.Plugin #-} import Test.Inspection inspect $ 'liftA2 === 'liftA2'
若两个函数实现逻辑存在差异,编译时会直接报错,无需等到运行测试,效率更高。
总结
- 追求运行时自动生成多态测试用例:优先用
polycheck(基于QuickCheck)或Hedgehog; - 需要编译时严格验证语义等价:选择
inspection-testing; - 这些工具都能帮你摆脱手动指定类型和函数的繁琐,实现接近
check $ f === f'的极简测试写法。
内容的提问来源于stack exchange,提问作者Blue Nebula

