当我们使用 QuickCheck 检查我们的程序时,我们需要为我们的数据定义生成器,有一些通用的方式来定义它们,但是当我们需要生成的数据满足某些约束才能工作时,通用的方式通常变得无用。
例如
data Expr
= LitI Int
| LitB Bool
| Add Expr Expr
| And Expr Expr
data TyRep = Number | Boolean
typeInfer :: Expr -> Maybe TyRep
typeInfer e = case e of
LitI _ -> Number
LitB _ -> Boolean
Add e1 e2 -> case (typeInfer e1, typeInfer e2) of
(Just Number, Just Number) -> Just Number
_ -> Nothing
And e1 e2 -> case (typeInfer e1, typeInfer e2) of
(Just Boolean, Just Boolean) -> Just Boolean
_ -> Nothing
现在我需要定义 Expr 的生成器(即Gen Expr
或instance Arbitrary Expr
),但也希望它生成正确的类型(即isJust (typeInfer generatedExpr)
)
一种天真的方法是使用suchThat
过滤掉无效的方法,但这显然是低效的,Expr
并且TyRep
在更多情况下变得复杂。
另一个类似的情况是关于参考完整性,例如
data Expr
= LitI Int
| LitB Bool
| Add Expr Expr
| And Expr Expr
| Ref String -- ^ reference another Expr via it's name
type Context = Map String Expr
在这种情况下,我们希望生成的所有引用名称Expr
都包含在某些特定的名称中Context
,现在我必须Expr
为特定的名称生成Context
:
arbExpr :: Context -> Gen Expr
但是现在shrink会成为一个问题,而要解决这个问题,我必须定义一个特定版本的shrink,并且forAllShrink
每次使用时都要使用arbExpr
,这意味着很多工作。
所以我想知道,有没有做这些事情的最佳实践?