The generator is the shrinker: a falsify deep read

The generator is the shrinker: a falsify deep read

A deep read of Edsko de Vries's 17 July explanation of falsify 0.4.0: why integrated shrinking changes generator design, and when its new context-aware sizing helps find counterexamples.

The article at a glance

On 17 July 2026, Edsko de Vries published New falsify release, a tour of falsify-0.4.0, the Haskell property-testing library he developed for the Haskell Symposium. The release adds getContext and its derived sized helper, but the deeper subject is how a property-testing library should represent shrinking: as part of the generator rather than as a second piece of test code. 1
That makes this a useful post for anyone who has written a generator that can produce a failing case but cannot reduce it to a case a human can understand. De Vries is not presenting falsify as a universal replacement for QuickCheck or Hedgehog. He is showing where its design buys something, where the new release helps, and where the library is still young.

The argument, end to end

A workshop supplies the pressure test

The post begins at QBayLogic, the company behind Clash, a functional hardware-design language that translates Haskell into VHDL or Verilog. Martijn Bastiaan invited Well-Typed to run a one-day falsify workshop there. The group first built mini-falsify, a small implementation intended to expose the library's ideas without making readers work through the full codebase. They then used the actual library in the Clash ecosystem. 1 2
That setting matters because it gives the release a concrete reason to exist. falsify-0.4.0 was partly the result of workshop-driven work and partly overdue maintenance. The article is therefore two things at once: an account of a new API and a report on what became clearer when the library was reduced to its basic machinery.

The new API makes the test run visible

The central addition is getContext:
getContext :: Property Context
The returned Context describes the current test case. Its documented fields include static run information, the zero-based current test number, and whether the current execution is an initial run, a shrinking step, or a final run. The static part includes the configured number of tests, the maximum number of shrinks, and the maximum discarded-test ratio. 3
The article uses the most accessible part of that information: the test iteration. Suppose a property claims that no generated Word64 equals 5. A primitive generator over all Word64 values has an enormous search space, so the one forbidden value is unlikely to appear quickly. getContext lets the property react to how far the run has progressed. Early iterations can use a small upper bound; later iterations can widen it. 1
falsify packages that pattern as:
sized :: (ProperFraction -> a) -> Property a
ProperFraction is a value in the half-open interval [0, 1). A property can scale that fraction into a domain-specific bound, then use the bound to construct a generator. In the article's example, the range grows toward 100, making the value 5 easy to encounter while retaining a simple way to vary the search domain across iterations. 1 4
The resemblance to QuickCheck's sized is intentional but limited. QuickCheck's helper receives an Int size and returns a generator. falsify's helper receives a normalized fraction and returns a property-level value. Both expose a notion of scale, but falsify's version is tied to the iteration context that the new API makes available. 4 5

The post does not turn staged generation into a default

This is where the article is more careful than a release note. Starting with small values can make a narrow counterexample easier to find, but it can also bias the test run toward cases that are too simple. De Vries recommends generating values of arbitrary size and shrinking a discovered failure in the general case. If the test has a particular edge case it must cover, the better response is usually a generator that targets that edge case explicitly, with labels to check whether the intended region was reached. 1
That warning gives sized a precise role. It is a search-control tool, not a substitute for a good generator. Use it when the distribution should change with iteration, and keep the property independent of the context when possible. The API documents an important constraint here: execution context must not affect whether a test passes or fails, because doing so would make shrinking behavior undefined. 3

The larger idea is integrated shrinking

The release's new feature is easy to name. The design that makes falsify interesting is harder to see until the post is read alongside its API documentation.
In QuickCheck, a generator and a shrinker are separate concerns. That can work well, but every new data type creates another obligation: the author must describe both how to produce values and how to simplify them after a failure. In Hedgehog, shrinking is integrated with the generator. falsify takes that integrated approach and adds an internal mechanism intended to keep shrinking effective across monadic bind, a place where a later generator depends on an earlier generated value. 1 6
The official documentation describes the distinction with a small example. If genList produces lists of numbers, then applying filter even through the generator produces only even numbers, and the evenness constraint remains in force while the failing case is shrunk. The generator is carrying a restriction that the shrinker does not have to rediscover later. 4
The monadic case is the more consequential one. Imagine generating a length n, then using n to decide how many values to generate. Shrinking only the values may leave an unnecessarily large structure; shrinking only n may invalidate the dependent part. The falsify documentation says its internal strategy can shrink n and the values produced afterward in either order, while also recommending dedicated compound generators when they express the relationship more directly. 4
This is the post's most transferable point. A generator is not merely a random-data factory. In a property-testing system with integrated shrinking, it is a description of a search space together with a path toward simpler members of that space. That is a distinctly functional-programming way to make the test specification carry more of the behavior you want.

The cleanup is part of the story

0.4.0 is also a major cleanup release and is backward-incompatible with 0.3 in several ways. The changelog records a new top-level Test.Falsify module for the common unqualified-import surface, a clearer split between Test.Falsify.Generator and the new Data.Falsify.* hierarchy, and the removal of tasty integration from the main package into tasty-falsify. It also lists removed deprecated functions, renamed functions, and several type aliases replaced by newtype definitions. 7
The package page reflects that reorganization. It points users who want a Tasty entry point to Test.Tasty.Falsify in the separate tasty-falsify package, while falsify now exposes Test.Falsify.Driver for driver integration. The package is documented as property-based testing with internal integrated shrinking, and Hackage lists version 0.4.0 as the current version in the package history. 6
For library users, this cleanup has a practical consequence: the conceptual API is easier to locate, but upgrading is not a drop-in edit. The release note's new feature and migration work belong together. A clearer module boundary is useful only after the test suite has made the version change explicit.

The conclusion is deliberately uneven

De Vries closes by comparing three different answers to the shrinker problem. QuickCheck remains the strongest choice when a team is willing to write good shrinkers for its own types. Hedgehog offers a polished integrated approach, but monadic bind can introduce cut-points that reduce shrinking quality. falsify is aimed at the same pain point, using an internal strategy inspired by Hypothesis while retaining a Haskell-oriented model of generators, including support for infinite data types and functions. 1
He also draws a hard boundary around the recommendation: falsify is experimental and not as battle-tested as Hedgehog, let alone QuickCheck. The QBayLogic work has made the project more mature, but the article does not present that as proof that the trade-offs have disappeared. 1

The technical details that matter

A generator defines valid reductions

The phrase "integrated shrinking" can sound like an implementation convenience. It is better understood as an invariant about the test space.
If a generator produces values with a structural promise, shrinking should preserve that promise. For an even-number generator, a smaller value that is odd is not a simpler test case; it is a different test case. When the generator and shrink behavior are designed together, every reduction stays inside the space the property was meant to explore. That is why the documentation's filter even <$> genList example is more significant than its size suggests. 4
The same invariant becomes more difficult when generation is dependent. A list whose length comes from one generated value is a simple example, but the pattern also appears in syntax trees, protocol messages, typed expressions, and state-machine traces. The more a later value depends on an earlier choice, the more a separate shrinker has to reconstruct about the generator's decisions.
falsify's answer is not to remove the dependency. It makes the dependency part of the structure that shrinking can inspect. The trade-off is that generator design becomes more important: a generator that hides meaningful structure behind awkward monadic steps may still shrink poorly, and the package docs point users toward dedicated combinators when they better represent the relationship. 4

Context is a controlled escape hatch

A property normally reads like a statement that should hold for all generated inputs. getContext introduces information about the test runner into that property. Used carelessly, that can make the property depend on its position in the run rather than on the generated input.
The API's structure makes the intended use narrower. Context distinguishes static run configuration, the current iteration, and execution phase. sized exposes only a normalized scale, which is enough for staged generation without forcing every property to inspect the runner's entire state. The docs also say that execution context must not decide pass or fail. 3
That suggests a useful engineering rule: use context to choose how thoroughly to search, never to change the meaning of the property. A later iteration may generate larger trees or broader numeric ranges. The assertion itself should still describe the same behavior.

Why the small-counterexample story is tempting

The article's forbidden value 5 is deliberately artificial. It isolates a common testing tension: a counterexample can be statistically valid but operationally hard to find. Sampling Word64 uniformly makes 5 vanishingly unlikely; searching small ranges first makes it obvious. 1
In real systems, the equivalent failure might be a malformed length field, a one-element collection, an empty branch, or a boundary timestamp. A staged generator can put those regions early in the search. But the article's warning still applies: a test that only explores the easy cases can create false confidence. The better long-term design is to encode the important cases as part of the generator's distribution and let shrinking simplify failures that occur elsewhere.

What to take into a Haskell test suite

The post supports a short review checklist:
  1. Treat generation and shrinking as one design problem when the validity of a value depends on how it was built.
  2. Use getContext or sized when the search domain should expand across iterations, not as a way to make the assertion position-dependent.
  3. Keep targeted edge cases explicit. Labels can tell you whether a desired region was actually exercised; they are more honest than assuming a broad random generator will hit it.
  4. Prefer a dedicated compound generator when it expresses a dependency more clearly than a chain of monadic binds.
  5. Read 0.4.0 as a migration as well as a feature release. The package split and module cleanup are backward-incompatible in places. 7
  6. Keep the maturity warning visible in adoption decisions. For production-critical test infrastructure, QuickCheck or Hedgehog may still be the less speculative choice, depending on the team's existing generators and shrinking experience. 1

Lines worth keeping

"The falsify library takes its main inspiration from the Python Hypothesis library, though it is not a direct translation."
"shrinking is never truly free."
The first line sets the right expectation: the interesting part is the reinterpretation for Haskell, not a port with new names. The second is the practical correction to every attractive shrinking story. Integrated machinery reduces one kind of maintenance, but generator choices still determine what failures can be found and how intelligibly they can be reduced. Both lines are from de Vries's original post. 1
falsify-0.4.0 is therefore best read as a sharper statement of a testing philosophy. Let the generator describe the values worth exploring, let its structure carry the path toward simpler failures, and use iteration-aware sizing only when the search itself needs staging. That is a smaller claim than "the next QuickCheck," but it is a much more useful one.

Related content

  • Sign in to comment.
More from this channel