Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/lysxia/test-fun

Representation of higher-order functions for property testing
https://github.com/lysxia/test-fun

functional-programming random testing

Last synced: 3 months ago
JSON representation

Representation of higher-order functions for property testing

Awesome Lists containing this project

README

        

# Testable functions [![Hackage](https://img.shields.io/hackage/v/test-fun.svg)](https://hackage.haskell.org/package/test-fun) [![Build Status](https://travis-ci.com/Lysxia/test-fun.svg)](https://travis-ci.com/Lysxia/test-fun)

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.

- QuickCheck: [quickcheck-higherorder](https://github.com/Lysxia/quickcheck-higherorder)

- Hedgehog: [hedgehog-higherorder](https://github.com/Lysxia/hedgehog-higherorder)

## 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`:

```haskell
newtype A = MkA { unA :: B }

cogenB :: Co Gen B r
```

Then `cogenEmbed` transforms `cogenB` into a cogenerator of `A`:

```haskell
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:

```haskell
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".

```haskell
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

- [*Shrinking and showing functions*](https://dl.acm.org/citation.cfm?id=2364516),
by Koen Claessen, Haskell Symposium 2012.

This package extends that work with support for higher-order functions.

Other implementations based on that paper can be found in:

+ [QuickCheck](https://hackage.haskell.org/package/QuickCheck-2.13.2); in
particular see the [`Fun`
type](https://hackage.haskell.org/package/QuickCheck-2.13.2/docs/Test-QuickCheck.html#g:14).

+ [hedgehog-fn](https://hackage.haskell.org/package/hedgehog-fn).

## 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.