Skip to content

QCheck2.Gen design considerations #162

Open
@jmid

Description

@jmid

With QCheck2 still not yet released (and still being tested) we have a brief opportunity to discuss the design of its Gen combinators.

Optional parameters

We have had problems with optional parameters in the past (see #75). These may cause problems when they are the only parameters. For QCheck this could usually be solved with type annotations:

utop # QCheck.Gen.small_string;;
- : ?gen:char QCheck.Gen.t -> string QCheck.Gen.t = <fun>
utop # QCheck.Gen.([small_string; string]);;
- : (?gen:char QCheck.Gen.t -> string QCheck.Gen.t) list = [<fun>; <fun>]
utop # (QCheck.Gen.([small_string; string]) : string QCheck.Gen.t list);;
- : string QCheck.Gen.t list = [<fun>; <fun>]
utop # QCheck.make QCheck.Gen.small_string;;
- : string QCheck.arbitrary =
{QCheck.gen = <fun>; print = None; small = None; shrink = None; collect = None;
 stats = []}

For Check2 however because QCheck2.Gen.t is opaque, even with the fix in PR #161, the type annotation trick doesn't work anymore:

utop # QCheck2.Gen.small_string;;
- : ?gen:char Gen.t -> string Gen.t = <fun>
utop # QCheck2.Gen.([small_string; string]);;
Error: This expression has type string t but an expression was expected of type
         ?gen:char t -> string t
utop #  (QCheck2.Gen.([small_string; string]) : string QCheck2.Gen.t list);;
Error: This expression has type ?gen:char t -> string t
       but an expression was expected of type string t
utop # QCheck2.Test.make QCheck2.Gen.small_string;;
Error: This expression has type ?gen:char Gen.t -> string Gen.t
       but an expression was expected of type 'a Gen.t

Effectively, this means the (labeled) parameter is required, not optional.
As such, we should seriously consider changing QCheck2.Gen.small_string to one of the following:

  val small_string : string t                   (* just use char *)
  val small_string : char t -> string t         (* make it a regular argument *) 
  val small_string : gen:char t -> string t     (* make it an actual labelled argument *)
  ...

Of the three above, I would prefer one of the first two.
Based on our choice, we should consider whether to update string_size accordingly following the principle of least surprise:

 val string_size : ?gen:char t -> int t -> string t

Looking at the Gen module's signature I can see a similar problem with Gen.pint : ?origin:int -> int t which we also need to address:

utop # QCheck2.Test.make QCheck2.Gen.pint;;
Error: This expression has type ?origin:int -> int Gen.t
       but an expression was expected of type 'a Gen.t

Here, we probably should go with a regular or labelled argument. Again, from the principle of least surprise,
we should consider updating the remaining origin-taking combinators to take the same kind of origin argument:

  val int_range : ?origin:int -> int -> int -> int t
  val float_bound_inclusive : ?origin:float -> float -> float t
  val float_bound_exclusive : ?origin:float -> float -> float t
  val float_range : ?origin:float -> float -> float -> float t
  val char_range : ?origin:char -> char -> char -> char t

Besides theGen.generate* bindings, only two bindings now stand a bit out:

  val opt : ?ratio:float -> 'a t -> 'a option t
  val make_primitive : gen:(Random.State.t -> 'a) -> shrink:('a -> 'a Seq.t) -> 'a t

I think for opt the use of an optional parameter is warranted.
For make_primitive we may consider whether labelled arguments are needed.

Generator names:

Naming-wise, we should consider the consistency. Here are the string generators:

  val string_size : ?gen:char t -> int t -> string t
  val string : string t
  val string_of : char t -> string t
  val string_readable : string t
  val small_string : ?gen:char t -> string t

Wouldn't string_small be nice and consistent - and easier to find using tab-completion?

I think the same goes for the list generators (small_list -> list_small):

  val list : 'a t -> 'a list t
  val small_list : 'a t -> 'a list t
  val list_size : int t -> 'a t -> 'a list t
  val list_repeat : int -> 'a t -> 'a list t

and the array generators (small_array -> array_small):

  val array : 'a t -> 'a array t
  val array_size : int t -> 'a t -> 'a array t
  val small_array : 'a t -> 'a array t
  val array_repeat : int -> 'a t -> 'a array t

For int*, float, and char generators it is harder...

Use of underscore in name suffixes

I spotted an unfortunate variation in the use of _l, _a or l, a suffixes:

  val oneof : 'a t list -> 'a t
  val oneofl : 'a list -> 'a t
  val oneofa : 'a array -> 'a t
  val frequency : (int * 'a t) list -> 'a t
  val frequencyl : (int * 'a) list -> 'a t
  val frequencya : (int * 'a) array -> 'a t
  val shuffle_a : 'a array -> 'a array t
  val shuffle_l : 'a list -> 'a list t
  val shuffle_w_l : (int * 'a) list -> 'a list t
  ...
  val flatten_l : 'a t list -> 'a list t
  val flatten_a : 'a t array -> 'a array t
  val flatten_opt : 'a t option -> 'a option t
  val flatten_res : ('a t, 'e) result -> ('a, 'e) result t

I know a lot of this is legacy, but with a clean QCheck2 now is a good time to consider cleaning these things a bit up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions