You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用QuickCheck测试Haskell约束类型的子集?

Haskell QuickCheck 约束类型子集测试方案

首先是你定义的Haskell数据类型:

data Document where
  DocumentKind1 :: {
    head :: Head,
    body :: Body
  } -> Document
  DocumentKind2 :: {
    head :: Head,
    body :: Body
  } -> Document

data Head where
  HeadKind1 {
    head_fields :: Int
    -- 其他字段...
  } -> Head
  HeadKind2 {
    head_fields :: Int
    -- 其他字段...
  } -> Head

data Body where
  BodyKind1 {
    body_fields :: Int
    -- 其他字段...
  } -> Body
  BodyKind2a {
    body_fields :: Int
    -- 其他字段...
  } -> Body
  BodyKind2b {
    body_fields :: Int
    -- 其他字段...
  } -> Body

由于Haskell不支持约束数据类型,你通过约定遵循以下规则:

  1. 若Document为DocumentKind1,则其Head必须是HeadKind1,Body必须是BodyKind1;
  2. 若Document为DocumentKind2,则其Head必须是HeadKind2,Body必须是BodyKind2a或BodyKind2b。

你有一个仅处理Kind1类型Document的函数:

parseKind1 :: Parser Char Document

问题解答:可以限制生成符合约束的类型子集,且能为每个子集单独测试

下面是两种常用的实现方式:

方式1:用newtype包装约束子集,编写专属Arbitrary实例

为每个符合约束的Document子集定义newtype包装器,然后为这些包装器实现Arbitrary实例,确保生成的实例严格符合规则:

import Test.QuickCheck

-- 包装符合DocumentKind1约束的实例
newtype ValidDocumentKind1 = ValidDocumentKind1 Document deriving (Eq, Show)

-- 包装符合DocumentKind2约束的实例
newtype ValidDocumentKind2 = ValidDocumentKind2 Document deriving (Eq, Show)

-- 为ValidDocumentKind1编写Arbitrary实例
instance Arbitrary ValidDocumentKind1 where
  arbitrary = do
    -- 生成符合HeadKind1的Head实例
    headVal <- HeadKind1 <$> arbitrary
    -- 生成符合BodyKind1的Body实例
    bodyVal <- BodyKind1 <$> arbitrary
    return $ ValidDocumentKind1 (DocumentKind1 { head = headVal, body = bodyVal })
  
  -- 可选:定义shrink函数,确保收缩后的实例仍符合约束
  shrink (ValidDocumentKind1 doc) =
    [ ValidDocumentKind1 (DocumentKind1 { head = h', body = b' })
    | h' <- shrink (head doc)
    , b' <- shrink (body doc)
    ]

-- 为ValidDocumentKind2编写Arbitrary实例
instance Arbitrary ValidDocumentKind2 where
  arbitrary = do
    headVal <- HeadKind2 <$> arbitrary
    -- 随机生成BodyKind2a或BodyKind2b
    bodyVal <- oneof [BodyKind2a <$> arbitrary, BodyKind2b <$> arbitrary]
    return $ ValidDocumentKind2 (DocumentKind2 { head = headVal, body = bodyVal })

针对ValidDocumentKind1编写测试用例:

-- 假设你有将Document转为字符串的renderDoc函数
prop_parseKind1_correct :: ValidDocumentKind1 -> Bool
prop_parseKind1_correct (ValidDocumentKind1 doc) =
  case parse parseKind1 "" (renderDoc doc) of
    Success parsed -> parsed == doc
    Failure _ -> False

-- 运行测试
main :: IO ()
main = quickCheck prop_parseKind1_correct

方式2:直接编写自定义Gen生成器,无需额外类型

如果不想定义新类型,可以直接编写生成符合约束实例的Gen函数,在测试中用forAll指定生成器:

import Test.QuickCheck

-- 生成符合DocumentKind1约束的实例
genValidDocumentKind1 :: Gen Document
genValidDocumentKind1 = do
  headVal <- HeadKind1 <$> arbitrary
  bodyVal <- BodyKind1 <$> arbitrary
  return $ DocumentKind1 { head = headVal, body = bodyVal }

-- 生成符合DocumentKind2约束的实例
genValidDocumentKind2 :: Gen Document
genValidDocumentKind2 = do
  headVal <- HeadKind2 <$> arbitrary
  bodyVal <- oneof [BodyKind2a <$> arbitrary, BodyKind2b <$> arbitrary]
  return $ DocumentKind2 { head = headVal, body = bodyVal }

编写测试用例时,用forAll指定使用自定义生成器:

prop_parseKind1_correct :: Property
prop_parseKind1_correct = forAll genValidDocumentKind1 $ \doc ->
  case parse parseKind1 "" (renderDoc doc) of
    Success parsed -> parsed == doc
    Failure _ -> False

main :: IO ()
main = quickCheck prop_parseKind1_correct

额外说明

  • 如果你的Head、Body的构造器对应的字段需要更复杂的生成逻辑,可以在arbitrary或自定义Gen中调整,比如生成特定范围的Int、关联字段等。
  • 两种方式各有优劣:newtype方式更适合长期维护的测试(类型安全,约束明确);自定义Gen方式更灵活,适合快速编写测试用例。

内容的提问来源于stack exchange,提问作者Delfin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 08:55:20