MIT licensed by Li-yao Xia
Maintained by [email protected]
This version can be pinned in stack with:test-fun-0.1.0.0@sha256:f0315ca26b8af27ec177c89a5ab2e25b535db36029cee428d8f5eafa66407296,1251
Depends on 1 package(full list with versions):
Used by 1 package in nightly-2024-11-30(full list with versions):

Testable functions

A representation of functions for property testing, featuring random generation, shrinking, and printing.

This package implements the core functionality. Separate packages integrate it with existing testing frameworks.

Summary

This package defines a type of testable functions a :-> r, representing functions a -> r.

  • To interpret a testable function into a function a -> r, use applyFun :: (a :-> r) -> a -> r.

  • To pretty-print a testable function, use show :: Show r => (a :-> r) -> String.

  • To shrink a testable function, given a shrinker for r, use shrinkFun :: (r -> [r]) -> (a :-> r) -> [a :-> r].

  • To randomly generate a testable function a :-> r, apply a cogenerator of a to a generator of r. Cogenerators can be defined using combinators from this library.

Cogenerators

The type of cogenerators of a is Co Gen a r, where Gen is QuickCheck’s monad of random generators and r is an abstract parameter (it’s really forall r. Co Gen a r).

That type Co Gen a r is literally defined as a type synonym of Gen r -> Gen (a :-> r). Given both a cogenerator c :: Co Gen a r, and a generator g :: Gen r, we can construct the generator of testable functions c g :: Gen (a :-> r).

(Users can just think of Co Gen as a whole, even though the implementation defines a more general Co which may be applied to any monad. Similarly, the parameter r can be ignored most of the time; it matters to cogenerators of parameterized types.)

There are several combinators to define cogenerators, covering the following scenarios.

Newtypes and embeddings

If we have a newtype A around some old type B, and we also have a cogenerator of B:

newtype A = MkA { unA :: B }

cogenB :: Co Gen B r

Then cogenEmbed transforms cogenB into a cogenerator of A:

cogenEmbed "unA" unA cogenB :: Co Gen A r

This is actually not restricted to newtypes: any “embedding” function A -> B (here, unA) can be used to convert a Co Gen B r to a Co Gen A r. (Yes, there is a contravariant functor hiding there.) Note that cogenEmbed expects a name for that function as a String in its first argument, for pretty-printing.

Generic data types

To define a cogenerator of a type which is an instance of Generic (from GHC.Generics), use cogenGeneric. For example, consider this type:

data Small a = Zero | One a | Two a a
  deriving Generic

The function cogenGeneric takes a heterogeneous list of cogenerators, one for each constructor of the generic type. This is cs in the example below.

The heterogeneous list is constructed using (:+) to append elements and () for the end of the list.

For constructors with multiple fields, use (.) to compose cogenerators for individual fields.

For nullary constructors, use id as the “nullary cogenerator”.

cogenSmall ::
  forall a.
  (forall r. Co Gen a r) ->
  (forall r. Co Gen (Small a) r)
cogenSmall cogenA = cogenGeneric cs where
  cs
    =  id                 -- Nullary cogenerator, for the constructor Zero
    :+ cogenA             -- Cogenerator of a, for the constructor One
    :+ (cogenA . cogenA)  -- A cogenerator of a, once for each field of the constructor Two
    :+ ()                 -- End of the list

Functions

To generate higher-order testable functions (a -> b) :-> r, we need a cogenerator of functions a -> b, which we can define using cogenFun.

To a first approximation, the function cogenFun transforms a cogenerator of b into a cogenerator of (a -> b), provided a way to generate, shrink, and show a.

This is actually generalized further by allowing one to provide a way to generate, shrink, and show a representation a0 of a, which can be equal to a in simple cases, but this generalization makes it possible to generate functions of arbitrarily high order.

Hence, to construct a cogenerator of a -> b, the function cogenFun takes the following arguments, in this order:

  1. Concrete a0: a dictionary containing a shrinker and a pretty-printer of representations a0;
  2. Gen (Maybe a0): a random generator of a0, it must generate Nothing once in a while (say with probability 1/5 if you have no clue);
  3. a0 -> a: a function from representations to actual values (id in simple cases);
  4. forall r. Co Gen b r: a cogenerator of b.

References

Internal module policy

Modules under Test.Fun.Internal are not subject to any versioning policy. Breaking changes may apply to them at any time.

If something in those modules seems useful, please report it or create a pull request to export it from an external module.