Search code examples
haskellquickcheck

Can I generate arbitrary strings, and avoid repeating specifications in QuickCheck?


Given

data MyType = MyType ...

makeMyType :: String -> String -> String -> MyType
-- ...

type StringThing = String

where the strings that makeMyType expects are (respectively):

  • a - delimited string of some custom strings (e.g., "Hilary-Jeb-David-Charles"),
  • a string of 4 capital letters, and
  • a . delimited string of integers between 1 and 26, each padded with a zero to two characters (e.g., "04.23.11.09")

I can use QuickCheck to generate adequate test cases with something like

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
import Test.QuickCheck
import Data.List (intercalate)
import Text.Printf (printf)

-- This really should be an arbitrary string of random length in some range
instance Arbitrary StringThing where
        arbitrary = elements ["FUSHFJSHF","KLSJDHFLSKJDHFLSKJDFHLSKJDHFLSKJOIWURURW","GHSHDHUUUHHHA"]

instance Arbitrary MyType where
        arbitrary = do
                -- This repetition feels unnecessary
                w1 <- elements ['A'..'Z']
                w2 <- elements ['A'..'Z']
                w3 <- elements ['A'..'Z']
                w4 <- elements ['A'..'Z']
                c1 <- elements someListOfCustomStuff
                c2 <- elements someListOfCustomStuff
                c3 <- elements someListOfCustomStuff
                c4 <- elements someListOfCustomStuff
                r1 <- choose (1,26)
                r2 <- choose (1,26)
                r3 <- choose (1,26)
                r4 <- choose (1,26)
                return $ makeMyType (intercalate "-" [c4,c3,c2,c1])
                                    [w1,w2,w3,w4]
                                    (intercalate "." $ (printf "%02d") <$> ([r1,r2,r3,r4] :: [Int]))

prop_SomeProp :: MyType -> StringThing -> Bool
prop_SomeProp mt st = ...

But StringThing really should take on arbitrary strings of capital letters of random length within some range, and the repetition of the same specification for all the w...s, c..s, and r....s seems unnecessary.

Is there a way in QuickCheck to:

  1. generate random strings with length within some bounds, restricted to certain characters, and
  2. "share" a specification using elements or choose among multiple values?

Solution

  • Yes. Haskell is great at being able to factor things out! You can certainly name and share subexpressions like elements ['A'..'Z']

    capitals = elements ['A'..'Z']
    
    instance Arbitrary StringThing where
            arbitrary = do
              l <- choose (1,50) -- this is your string length
              replicateM l capitals
    

    And for your MyType, you can also use replicateM quite a lot:

    instance Arbitrary MyType where
            arbitrary = do
                    ws <- replicateM 4 capitals
                    cs <- replicateM 4 (elements someListOfCustomStuff)
                    rs <- replicateM 4 (choose (1,26))
                    return $ makeMyType (intercalate "-" cs)
                                        ws
                                        (intercalate "." $ (printf "%02d") <$> (rs :: [Int]))