I have a higher-order function that I want to test, and one of the properties I want to test is what it does with the functions that are passed in. For purposes of illustration, here is a contrived example:
gen :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
The idea is roughly that this is an example generator. I'm going to start with a single a
, create a singleton list of [a]
, then make new lists of [a]
until a predicate tells me to stop. A call might look like this:
gen init next stop
where
init :: a
next :: [a] -> [a]
stop :: [a] -> Bool
Here's the property I'd like to test:
On any call to
gen init next stop
,gen
promises never to pass an empty list tonext
.
Can I test this property using QuickCheck, and if so, how?
In Javascript, functions can be assigned to variables in the same way that strings or arrays can. They can be passed into other functions as parameters or returned from them as well. A “higher-order function” is a function that accepts functions as parameters and/or returns a function.
In high order function, a function can act as an instant of an object type. In high order function, we can return a function as a result of another function. In high order function, we can pass a function as a parameter or argument inside another function.
Higher order functions are also commonly used to abstract how to operate on different data types. For instance, . filter() doesn't have to operate on arrays of strings. It could just as easily filter numbers, because you can pass in a function that knows how to deal with a different data type.
While it would help if you gave the implementation of gen
, I am
guessing that it goes something like this:
gen :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
gen init next stop = loop [init]
where
loop xs | stop xs = head xs
| otherwise = loop (next xs)
The property you want to test is that next
is never supplied an
empty list. An obstacle to test this is that you want to check an
internal loop invariant inside gen
, so this needs to be available from
the outside. Let us modify gen
to return this information:
genWitness :: a -> ([a] -> [a]) -> ([a] -> Bool) -> (a,[[a]])
genWitness init next stop = loop [init]
where
loop xs | stop xs = (head xs,[xs])
| otherwise = second (xs:) (loop (next xs))
We use second
from
Control.Arrow.
The original gen
is easily defined in terms of genWitness:
gen' :: a -> ([a] -> [a]) -> ([a] -> Bool) -> a
gen' init next stop = fst (genWitness init next stop)
Thanks to lazy evaluation this will not give us much overhead. Back to
the property! To enable showing generated functions from QuickCheck,
we use the module
Test.QuickCheck.Function.
While it is not strictly necessary here, a good habit is to
monomorphise the property: we use lists of Int
s instead of allowing
the monomorphism restriction making them to unit lists. Let us now state
the property:
prop_gen :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Bool
prop_gen init (Fun _ next) (Fun _ stop) =
let trace = snd (genWitness init next stop)
in all (not . null) trace
Let us try the running it with QuickCheck:
ghci> quickCheck prop_gen
Something seems to loop... Yes of course: gen
loops if stop
on the
lists from next
is never True
! Let us instead try to look at finite prefixes of the input trace
instead:
prop_gen_prefix :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Int -> Bool
prop_gen_prefix init (Fun _ next) (Fun _ stop) prefix_length =
let trace = snd (genWitness init next stop)
in all (not . null) (take prefix_length trace)
We now quickly get a a counter-example:
385
{_->[]}
{_->False}
2
The second function is the argument next
, and if it returns the empty list,
then the loop in gen
will give next
an empty list.
I hope this answers this question and that it gives you some insight in how to test higher-order functions with QuickCheck.
It is possibly in bad taste to abuse this, but QuickCheck does fail a function if it throws an exception. So, to test, just give it a function that throws an exception for the empty case. Adapting danr's answer:
import Test.QuickCheck
import Test.QuickCheck.Function
import Control.DeepSeq
prop_gen :: Int -> (Fun [Int] [Int]) -> (Fun [Int] Bool) -> Bool
prop_gen x (Fun _ next) (Fun _ stop) = gen x next' stop `deepseq` True
where next' [] = undefined
next' xs = next xs
This technique does not require you to modify gen
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With