Make similar QuickCheck properties DRY
-
21-12-2019 - |
Question
I have a bunch of QuickCheck
properties defined as follows:
...
prop_scaleData3 d n = n > 1 ⇛ length (scaleData d n) ≡ n
prop_scaleData4 d n = n > 1 ⇛ head (scaleData d n) ≡ -d
prop_scaleData5 d n = n > 1 ⇛ last (scaleData d n) ≡ d
...
That's a lot of repetition. What would be the right way to DRY it up?
Solution
prop_scaleData3 d n = n > 1 ==> length (scaleData d n) == n
prop_scaleData4 d n = n > 1 ==> head (scaleData d n) == -d
prop_scaleData5 d n = n > 1 ==> last (scaleData d n) == d
Just observe what is similar about these three functions and make a new helper function that extracts out the commonalities. For example:
scaleProp :: Int -> Int -> ([Int] -> Int) -> Int -> Bool
scaleProp d n op res = n > 1 ==> op (scaleData d n) == res
Then you can express your original props in terms of the helper:
prop_scaleData3 d n = scaleProp d n length n
prop_scaleData4 d n = scaleProp d n head (-d)
prop_scaleData4 d n = scapeProp d n last d
At this point the repetition isn't about logic so much as syntax (naming functions and applying arguments). In such cases I don't feel the DRY principle is really helpful - you can make less syntactic repetition but you'll lose readability or modularity. For example, Toxaris combined the solutions into one function; we can do the same but lets do it in a simpler way with just lists of booleans:
prop_scaleData345 d n =
let sp = scaleProp d n
in and [sp length n, sp head (-d), sp last d]
-- or instead:
-- in all (uncurry sp) [(length, n), (head, negate d), (last, d)]
OTHER TIPS
What about something like
gt1 :: (Integer -> Prop) -> Prop
gt1 f = forAll $ \(Positive n) -> f $ n + 1
Then your properties become
prop_scaleData3 d = gt1 $ \n -> length (scaleData d n) ≡ n
prop_scaleData4 d = gt1 $ (≡ -d) . head . scaleData d
prop_scaleData5 d = gt1 $ (≡d) . last . scaleData d
This avoids the duplicated logic. Whether or not you like the pointfree stuff is up to you :)
If you put this pragma at the top of your file:
{-# LANGUAGE ParallelListComp #-}
You might be able to do something like this with GHC:
prop_scaleData345 d n = n > 1 => conjoin
[ f (scaleData d n) == x
| f <- [length, head, last]
| x <- [n , -d , d ]
]
This should generate a list of three properties, and then say that all of them have to be true. The first property uses f = length and x = n, the second property uses f = head and x = -d, and the last property uses f = last and x = d.