Skip to content

Add QCheck2.Gen.recursive - #420

Open
tmcgilchrist wants to merge 1 commit into
c-cube:mainfrom
tmcgilchrist:feature/gen-recursive
Open

tmcgilchrist wants to merge 1 commit into
c-cube:mainfrom
tmcgilchrist:feature/gen-recursive

Conversation

@tmcgilchrist

Copy link
Copy Markdown
Contributor

Build a generator for a recursive type from its base and recursive cases, combined with oneof or oneof_weighted. Each recursive case is handed a generator for the same type at a smaller size.

The size is multiplied by the reciprocal of the golden ratio (~0.618) per level rather than halved, so the expected depth grows with the drawn size instead of following a fixed division schedule. Recursion stops once the size reaches 1, and the optional ?scale argument is clamped to [0, n - 1] so a schedule that fails to decrease cannot make generation diverge.

I had this convenience function defined in a project where I needed to write a number of recursive generators.

Before:

type tree = Leaf of int | Node of tree * tree

let leaf x = Leaf x                                                
let node x y = Node (x, y)
let golden n = int_of_float (Float.round (float_of_int n *.
  0.618033988749895))

let tree_gen =
  QCheck2.Gen.(sized @@ fix                                                
    (fun self n ->
      if n <= 1
      then map leaf nat
      else
        let smaller = delay (fun () -> self (max 0 (min (n - 1)
          (golden n)))) in                                                   
        oneof [ map leaf nat;
          map2 node smaller smaller ]))

After:

type tree = Leaf of int | Node of tree * tree

let tree_gen =
  QCheck2.Gen.(recursive oneof
    [ map (fun i -> Leaf i) nat ]
    [ (fun self -> map2 (fun l r -> Node (l, r)) self self) ])

Build a generator for a recursive type from its base and recursive
cases, combined with oneof or oneof_weighted. Each recursive case is
handed a generator for the same type at a smaller size.

The size is multiplied by the reciprocal of the golden ratio (~0.618)
per level rather than halved, so the expected depth grows with the
drawn size instead of following a fixed division schedule. Recursion
stops once the size reaches 1, and the optional ?scale argument is
clamped to [0, n - 1] so a schedule that fails to decrease cannot make
generation diverge.

@jmid jmid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Tim, this does seem like a handy short-hand combinator! 😃👍

For context, I've previously lined up to "a big QCheck 1.0" by aligning combinator names across QCheck.Gen, QCheck.arbitrary, and QCheck2.Gen (see PRs #367 to #396). As such I'd very much like to see something like this in the other APIs for consistency.

As a short-hand I initially thought that recursive may be overly general, thinking that

  • recursive (specialized to oneof) and
  • recursive_weighted (specialized to oneof_weighted)
    may be worth considering, since these two would be the primary use cases AFAICS (do you have a use case for the more generalized interface?).
    This would make the usage even shorter:
let tree_gen =
  QCheck2.Gen.(recursive
    [ map (fun i -> Leaf i) nat ]
    [ (fun self -> map2 (fun l r -> Node (l, r)) self self) ])

Furthermore, the split x and x_weighted would fit reasonably with how we've (re)named the other library combinators going forward. WDYT?

Having a second look at the current proposed signature:

 val recursive :
    ?scale:(int -> int) ->
    ('a t list -> 'a t) ->
    'a t list ->
    ('a t -> 'a t) list ->
    'a t
  (** [recursive combine base rec_cases] builds a generator for a recursive
      type out of its non-recursive cases [base] and its recursive cases
      [rec_cases], combined with [combine] - typically {!oneof}, or
      {!oneof_weighted} composed with a weighting function.
   [...] *)

I can't see how to instantiate this to using oneof_weighted (composed with a weighting function). Can you share an example? (or even better: a test!) 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants