QuickCheck

Files : use51.m
Back to main

# QuickCheck Tutorial 5

## Generators - Basic

The invariant function is of the form

            :- func Invariant_Function_X(T,  T1, T2 ...) = property
            :- mode Invariant_Function_X(in, in, in ...) = out.

Quickcheck generates random values for each input argument at run time. The following types have default generators:

- int
- char
- float
- string
- some functions (more detail in Tutorial 8)
- any type defined as a discriminated union, provided that all types in the body of the definition have default/custom generators
- any type defined as being equivalent to a type with a default/custom generator

There is no code written to handle equivalent types. But it works as if the compiler replaced all the equivalent types with their real type before compiling.

The default generator for int is rand_int/2, which has distribution:

50% | even distribution in the range [-100, 100]
50% | even distribution in the range (-2^31, 2^31) (This will probably change to [int__min_int, int__max_int] in a future version.)

    :- func rand_int(rnd, rnd) = int.
    :- mode rand_int(in, out) = out is det.
    rand_int(BS0, BS) = Int :-
            Temp = rand_allint(BS0, BS1) rem 2,
            (if     Temp = 0
             then
                    irange(-100, 100, Int, BS1, BS)
             else
                    Int = rand_allint(BS1, BS)
            ).

    :- func rand_allint(rnd, rnd) = int.
    :- mode rand_allint(in, out) = out is det.
    rand_allint(BS0, BS) = Int :-
            next(1, Sign, BS0, BS1),
            next(31, TempInt, BS1, BS),
            ( Sign > 0 ->
                Int =  TempInt
            ;
                Int = -TempInt
            ).

Default for char is rand_char/2, with even spread over char__to_int(Char, X) where X is (-1000, 1000).

    :- func rand_char(rnd, rnd) = char.
    :- mode rand_char(in, out) = out is det.
    rand_char(RS0, RS) = Char :-
                    Int = rand_allint(RS0, RS1) rem 1000,
                    (if     char__to_int(Char0, Int)
                     then
                            Char = Char0,
                            RS = RS1
                     else
                            Char = rand_char(RS1, RS)
                    ).

Default for float is rand_float/2, which has roughly even distribution over all discrete values for a 32-bit float. If machine is less then 32bits, overfloat occurs. It should still cover all possible values, but may alter distribution. If machine is more than 32bits, rand_float/2 will miss some values but retains even distribution.

    :- func rand_float(rnd, rnd) = float.
    :- mode rand_float(in, out) = out is det.
    rand_float(BS0, BS) = Flt :-
            next(31, Mant0, BS0, BS1),
            next(1, Sign, BS1, BS2),
            ( Sign > 0 ->
                Mant = Mant0
            ;
                Mant = -Mant0
            ),
            next(7, Exp, BS2, BS3),
            next(1, ExpSign, BS3, BS),
            Flt0 = float(Mant) * pow(2.0, Exp),
            ( ExpSign > 0, Flt0 \= 0.0 ->
                Flt = 1.0/Flt0
            ;
                Flt = Flt0
            ).

Default for string is rand_string/2, each element is generated by rand_char/2.

0.9^0 * 0.1 | chance being string length == 0
0.9^1 * 0.1 | chance being string length == 1
0.9^2 * 0.1 | chance being string length == 2
...etc...

So the mean string length is Sum(0.1* 0.9^N * N) where N ← {0, infinity}, which converges to 9.

    :- func rand_string(rnd, rnd) = string.
    :- mode rand_string(in, out) = out is det.
    rand_string(RS0, RS) = X :-
                    gen(Charlist, [], [ {type_of(['A']), [{10,[]},{90,[]}]} ],
                        [], RS0, RS),
                    string__from_char_list(Charlist,X).

Note that the default generator for stirng actually uses the default generator for discriminated union (indirectly).

use51.m gives an illustration of generating int, char, float, string.

    :- module use51.

    :- interface.

    :- type marks == int.

    :- use_module io.

    :- pred main(io__state, io__state).
    :- mode main(di, uo) is det.

    %---------------------------------------------------------------------------%

    :- implementation.

    :- import_module int, char, float, string, list.
    :- import_module qcheck.

    %---------------------------------------------------------------------------%

    main -->
            qcheck(qcheck__f(junk), "just to show the inputs", 5, [], []).

    :- func junk(marks, char, float, string) = property.
    junk(A, B, C, D) =
            {A,B,C,D} `>>>` [yes].

There are a few thing to note about use51.m :

            qcheck(qcheck__f(junk), "just to show the inputs", 5, [], []).

The 3rd argument is an int, which specifies how many times to run. In this example 5 tests are run , but the default is 100. The other auguments will be described later.

            junk(A, B, C, D) =
                    {A,B,C,D} `>>>` [yes].

the invariant function doesn't do any testing, it always succeeds. But `>>>` collects all the inputs. A Sample output

            Test Description : just to show the inputs
            Number of test cases that succeeded : 5
            Number of trivial tests : 0
            Number of tests cases which failed the pre-condition : 0
            Distributions of selected argument(s) :
            1     {-50, '4', -3.55475907854864e+25, "\241r\371~~\316\002LJ~\204\246"}
            1     {27, '\342', -311727734390784., "\377g.\001"}
            1     {1389908257, '8', 2.63071847153664e+15, "\342"}
            1     {-90973704, '<', -2.10559053720692e-22, ""}
            1     {-896549770, 's', 7.72155851221736e+30, "[\230m\304\2561\254Q"}

The char and string output doesn't look pretty, since most are not printable, eg: \342, \254