Recently Martijn Bastiaan, QBayLogic’s COO,
invited Well-Typed to come to QBayLogic HQ and give a one-day workshop on
falsify, the new property based
testing library that I developed for the Haskell Symposium back in 2023
(paper,
presentation).
QBayLogic is the company behind
Clash: a purely functional language for hardware
design. In case you haven’t heard of it, Clash translates Haskell to VHDL or
Verilog, which can in turn be translated to actual hardware. It literally uses
ghc as its frontend so you
have essentially the full power of Haskell available. It’s a great project, I
recommend checking it out.
The falsify library takes its main inspiration from the Python
Hypothesis library, though it is not a direct
translation: it takes the same core idea (“parse, don’t generate”) but
reinterprets it in a way that better suits the Haskell way of thinking: more
axiomatic approach, support for generating infinite data types (including
functions), etc. For more information on falsify, see also the original blog
post that announced it, falsify: Hypothesis-inspired shrinking for
Haskell.
At QBayLogic HQ we spent the morning developing
mini-falsify from scratch, so
that we could focus on the main ideas without getting bogged down in the details
of the full library; similar in spirit to for example
TinyServant,
or perhaps Stephen Diehl’s Typechecker
Zoo. In the afternoon we hacked on
falsify and its application within the clash ecosystem. Partly as a result
of that work and partly as a result of me taking this opportunity to do some
long overdue maintenance on falsify, there is now a new falsify release:
falsify-0.4.0. In the
remainder of this blog post we give a brief overview of the main changes.
New feature: Context
Most of this falsify release is just cleanup, but there is one important new
feature, spear-headed by Peter Lebbing and Martijn Bastiaan from QBayLogic. The
Property monad now has an important new function, called
getContext:
getContext :: Property ContextThe most important information that the context of a property provides is how many tests we are running for each property (how hard are we trying to falsify this property), and which iteration this particular attempt is.
This can be quite useful, for example when you want to start by looking at small
test cases and then slowly broaden the scope. Just as a trivial example,
consider the property that “no number is equal to 5”. If we use
prim
to generate the number to test, producing an arbitrary Word64, the chances
that we will find the one counter-example (5) in that enormous search space
are essentially non-existent:
demo :: Property ()
demo = do
x <- gen Gen.prim
assert $ P.ne .$ ("forbidden", 5)
.$ ("x", x :: Word64)However, we could start with a small range and slowly grow that range; falsify
now also offers a convenience function, defined in terms of getContext, for
this specific purpose:
sized,
so-named because it is somewhat similar in spirit to sized in
QuickCheck:
sized :: forall e a. (ProperFraction -> a) -> Property aA
ProperFraction
is in the half-open interval [0,1); that is, between 0 (inclusive) and 1
(exclusive). We can use this to refine our demo property:
demo2b :: Property ()
demo2b = do
l <- sized $ ProperFraction.scaleIntegral 100
x <- gen $ Gen.inRange $ Range.inclusive (0, l)
assert $ P.ne .$ ("forbidden", 5)
.$ ("x", x :: Word64)This property is easily falsified.
As an aside, I would be somewhat cautious in using this approach. While it is
sometimes unavoidable, in general I would recommend generating test case of
arbitrary size and then shrinking them down; this is often more likely to
actually find counter-examples. If there are specific edge cases that you want
to hit, write a generator that covers those edge cases specifically, and perhaps
use labelling
(label
and co) to check that those edge cases are indeed covered. However, as this demo
shows, for some properties explicitly searching for small domains first can be
very helpful.
Cleanup
The most important change in this release is a cleanup of the code base:
- There is now a separate
tasty-falsifypackage that provides integration with thetastytest framework;falsifyitself now providesTest.Falsify.Driver. - The module hierarchy has been significantly cleaned up. For example, there are now a bunch of new
Data.Falsify.*modules for specific datatypes; previously some of these were exported byTest.Falsify.Geninstead; now that module only contains the generators for those datatypes. - Deprecated functions have been removed
- Some functions have been renamed and some
typealiases have been replaced bynewtypedefinitions for increased API clarity. - All Haddock warnings have been addressed.
For a full list of changes, please refer to the changelog.
Conclusions
Shrinking can be handled manually, in the style of
QuickCheck; or
automatically, in the style of
hedgehog or in the style of
falsify. If you are willing to
put effort into writing good shrinkers for all your types, then QuickCheck is
still your best bet.
If you consider the cost of manually writing shrinkers too large, then within
the Haskell ecosystem you have a choice between hedgehog and falsify. The
former has the considerable advantage that it’s been around for quite a long
time and is a very polished library. The downside is that every time you use
monadic bind you introduce a cut-point, which can often result in poor quality
shrinking. With Hypothesis showing the way,
falsify solves that cut-point problem, though even with falsify it still
matters how you write your generators: shrinking is never truly free.
Moreover, falsify should be considered an experimental library: it’s nowhere
near as battle-tested as Hedgehog, never mind QuickCheck. That said, thanks to
the interest of QBayLogic falsify is now a little more mature, so thank you
QBayLogic!
For clients who are looking for professional support for the use of falsify,
or indeed any other Haskell library, don’t hesitate to contact us at
info@well-typed.com.