Haskell如何无需逐实例编写测试验证类型类函数交换律
问题背景
假设类型类T中定义有函数f :: (T a) => a -> a -> a,该类型类已在大量类型上实现了对应实例。我们要求该函数无论针对哪种类型的具体实现都必须满足交换律,对应性质的形式化描述如下:
∀ a, b ∈ X: (T X, Eq X) => f a b == f b a
针对单个具体类型验证该性质的方式非常直接,比如验证Int类型上(+)运算的交换律,可以编写如下QuickCheck测试代码:
import Test.QuickCheck (quickCheck) commutativityProperty :: Eq a => (a -> a -> a) -> a -> a -> Bool commutativityProperty f a b = f a b == f b a main :: IO () main = do quickCheck (commutativityProperty (+) :: Int -> Int -> Bool)
但这种方式存在明显局限:QuickCheck无法处理歧义类型变量,因此无法直接编写覆盖所有符合约束类型的通用测试,以下两种写法都无法正常运行:
-- 无效:存在歧义类型变量 quickCheck (commutativityProperty (+) :: (Num a, Eq a) => a -> a -> Bool)
-- 无效:存在歧义类型变量 quickCheck (commutativityProperty f :: (T a, Eq a) => a -> a -> Bool)
核心诉求
在明确f的所有类型实例实现都必须满足交换律的前提下,实现该性质的全实例自动验证,不需要为每个实现了f的类型单独编写QuickCheck测试用例、维护对应测试代码,即后续新增T的类型实例时,不需要手动回头为新类型补充专属测试代码。
实现方案
核心思路是把定律校验逻辑和类型类本身绑定,借助GHC的类型类扩展能力、泛型或元编程能力实现测试逻辑的自动复用和实例自动发现,不需要为每个实例重复编写测试:
- 首先定义专门用于校验
T实例定律的类型类,约束覆盖测试需要的所有能力:T的实例约束、相等比较Eq约束、QuickCheck生成随机测试用例的Arbitrary约束。在该类中提供交换律测试的默认实现,所有实例直接复用该逻辑,不需要手动编写测试代码。
最小实现参考如下:{-# LANGUAGE DefaultSignatures, FlexibleContexts #-} import Test.QuickCheck (Arbitrary, quickCheck) -- 原有业务类型类 class T a where f :: a -> a -> a -- T的定律校验类 class (T a, Eq a, Arbitrary a) => TLawCheck a where checkFCommutativity :: IO () -- 默认测试实现,所有实例无需重写 default checkFCommutativity :: IO () checkFCommutativity = quickCheck $ \a b -> f a b == f b a - 给已有
T实例追加测试能力的成本极低:只需要声明空的TLawCheck实例即可,不需要写任何测试逻辑。如果开启DeriveAnyClass扩展,甚至可以在编写T实例时直接派生TLawCheck,不需要额外写实例声明。 - 如果要完全消除手动声明测试实例的成本,可以通过模板Haskell或者泛型库在编译期自动扫描所有
T的实例,为满足Eq、Arbitrary约束的实例自动生成TLawCheck实例,同时自动收集所有测试入口,连空实例声明都不需要手动编写。如果某个新增的T实例不满足测试需要的约束,编译期会直接抛出错误,不会出现漏测。 - 最后编写统一的测试执行入口,依次调用所有
TLawCheck实例的checkFCommutativity方法,就能一次性完成全实例的交换律校验。后续新增T类型实例时,只要满足必要约束,就会被自动纳入测试覆盖范围,完全不需要手动维护专属测试代码。
内容的提问来源于stack exchange,提问作者Chris Tiano
相关产品推荐
相关产品推荐

