Delfin
Delfin

Reputation: 343

How can I use quicktest to test subsets of constrained types?

I have a Haskell datatype

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

which by convention, as Haskell does not allow constraining data types, to follow the laws:

  1. If a Document is DocumentKind1, then both Head and Body had to be of kind 1, id est, that they must be of HeadKind1 and BodyKind1 respectively.
  2. If a Document is DocumentKind2, then both Head and Body had to be of kind 2, id est, that they must be of HeadKind2 and BodyKind2a or HeadKind2 and BodyKind2b respectively.

Likewise I also had a function, which takes Document but only works when document is of Kind1

parseKind1 :: Parser Char Document

and want to test it with QuickCheck, but just defining Arbitrary to make the Documents and later decoding and comparing, would feed the function Documents that are not valid.

Is there a way to restrict Arbritary to the respective constrained data types and if so, there is a way to dedicate test for every constrained subset of the data type?

Upvotes: 0

Views: 75

Answers (2)

pxq
pxq

Reputation: 111

The other answer is good, but it's not always easy/possible to rewrite type hierarchies to their Heads and Bodys.

When in this situation, a cheap and easy way to take a provided Arbitrary instance and obtain a "filtered" version of it (i.e. so that each generated example satisfies desired conditions) is to use the function suchThat:

suchThat :: Gen a -> (a -> Bool) -> Gen a

For example:

newtype Kind1Doc = Kind1Doc Document

instance Arbitrary Kind1Doc
  where
  arbitrary = fmap Kind1Doc $ arbitrary @Document `suchThat` \case
                                                               { DocumentKind1
                                                                 { head = HeadKind1 {}
                                                                 , body = BodyKind1 {}
                                                                 } ->
                                                                   True
                                                               ; _ ->
                                                                   False
                                                               }

Possible reasons to avoid: inefficient at runtime, namespace pollution.

Upvotes: 1

Daniel Wagner
Daniel Wagner

Reputation: 153172

Your constraints are particularly simple. To me it seems very reasonable to enforce them at the type level.

data Document = Document1 Head1 Body1 | Document2 Head2 Body2
data Head1 = Head1 HeadBoth ...
data Head2 = Head2 HeadBoth ...
data HeadBoth = HeadBoth { head_fields :: Int }
data Body1 = Body1 BodyBoth ...
data Body2 = Body2 BodyBoth Body2Subkind
data BodyBoth = BodyBoth { body_fields :: Int }
data Body2Subkind = Body2a ... | Body2b ...

Upvotes: 2

Related Questions