4

In Haskell's QuickCheck, how to generate a string such that it contains only the characters ‘S’ and ‘C’, and the position of ‘S’ and ‘C’ is random?

For example: "SCCS", "SSSS", "CCCC", "CSSCCS", ""

My use case is this:

I have two functions countCAndS :: String -> (Int, Int) and countCAndS' :: String -> (Int, Int). They have the same type signature. I'd like to write a QuickCheck property such that I can pass the same string to these two different functions and check if the outputs are the same.

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
Leo Zhang
  • 3,040
  • 3
  • 24
  • 38

3 Answers3

6

Let your property take a list of booleans and convert them to cs and ss.

prop_CountersEqual :: [Bool] -> Bool
prop_CountersEqual bs = countCAndS css == countCAndS' css where
    css = map (\b -> if b then 'C' else 'S') bs

If you need this frequently, you may want to define a new type with a suitable Arbitrary instance.

newtype CAndS = CAndS String
instance Arbitrary CAndS where
    arbitrary = CAndS . map (\b -> if b then 'C' else 'S') <$> arbitrary

Then you can write your properties as e.g.

prop_CountersEqual' :: CAndS -> Bool
prop_CountersEqual' (CAndS css) = countCAndS css == countCAndS' css
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
3

QuickCheck provides a flurry of combinators for that, it's super easy and very elegant:

prop_CountersEqual = 
  forAll csString $ \s -> countCAndS s == countCAndS' s

csString :: Gen String
csString = listOf $ elements "CS"

It's also trivial to expand your dictionary, should you need it.

Regis Kuckaertz
  • 991
  • 5
  • 14
  • Thanks. I like your solution better. Feels very elegant, and it has the minimum knowledge required to understand how it works. – Leo Zhang Apr 09 '18 at 20:39
0

Here's another way to define the Arbitrary instance:

newtype CAndS = CAndS String
instance Arbitrary CAndS where
  arbitrary = CAndS <$> listOf (elements "CS")

Besides testing a function against a reference solution, you could test that the sum of 'C's and 'S'es is the same as the length of the string:

prop_CountersAddUp :: CAndS -> Bool
prop_CountersAddUp (CAndS css) = length css == cs + ss
  where (cs, ss) = countCAndS css

If the string is supposed to accept input that has characters other than 'C's and 'S'es (but not count them), you should perhaps create a generator that generates all kinds of characters, but with a higher probability of 'C' and 'S':

newtype CAndS = CAndS String
instance Arbitrary CAndS where
  arbitrary = CAndS <$> listOf (frequency [ (1, return 'C')
                                          , (1, return 'S')
                                          , (2, arbitrary) ])

Or you might generate a string that is annotated with the right answer:

newtype CAndS = CAndS (String, Int, Int)
instance Arbitrary CAndS where
    arbitrary = CAndS <$> sized (\n -> do
      cs <- choose (0, n)
      let ss = n - cs
      css <- shuffle (replicate cs 'C' ++ replicate ss 'S')
      return (css, cs, ss))

So you can test it:

prop_CountersEqual :: CAndS -> Bool
prop_CountersEqual (CAndS (css, cs, ss)) = cs == cs' && ss == ss'
  where (cs', ss') = countCAndS css
sshine
  • 15,635
  • 1
  • 41
  • 66