learn-ocaml

Step 4: Grading with generators for user-defined types

In the case of user-defined types, it is mandatory to define a sampler. Both two previous methods (using the ~sampler optional argument or defining a sampler function sample_my_type) can be used but required a little more work, especially for parametric types.

Non parametric type

For non-parametric type, it is exactly the same than in the previous step.

You can find the examples below in the exercises/sampler-user-defined-types directory (branch: step-4).

In the examples, we use the type color defined as :

type color = Green | Yellow | Red | Blue

Method 1: using the ~sampler argument

As in the previous step, you can add the argument ~sampler of type unit -> <arg1_type> * <arg2_type> * <arg3_type> * etc.

let exercise_1 =
	test_function_1_against_solution
		[%ty: color -> string] "color_to_string"
		~sampler: (fun () ->  match Random.int 4 with
		           | 0 -> Red | 1 -> Green | 2 -> Yellow | _ -> Blue)
		~gen:5
		[]

Method 2: Defining a sampler

You can also define your own sampler and not use the ~sampler argument with the following rule: a sampler of type unit -> my_type has to be named sample_my_type.

let sample_color () : color =
  match Random.int 4 with
    | 0 -> Red
    | 1 -> Green
    | 2 -> Yellow
    | _ -> Blue

let exercise_2 =
	test_function_1_against_solution
		[%ty: color -> string] "color_to_string"
		~gen:5
		[]

In this case, the grader will automatically use your sampler sample_color for the type color. Be careful to write sample_color and not sampler_color.

Parametric types

You can find the examples below in the exercises/sampler-user-defined-parametric-types directory (branch: step-4).

In the examples below, we use the types:

type col = R | B

type 'a tree =
  | Leaf
  | Node of 'a tree * 'a * 'a tree

Method 1: using the ~sampler argument

No change here, just don’t forget that the optional argument~sampler has type unit -> <arg1_type> * <arg2_type> * <arg3_type> * etc..

let sample_col () = match Random.int 2 with
  | 0 -> B
  | _ -> R

let sample_col_tree () =
  let rec builder h = match h with
    | 0 -> Leaf
    | n -> match Random.int 3 with
      | 0 -> Leaf
      | _ -> Node (builder (h-1), sample_col (), builder (h-1))
  in
  let h = Random.int 5 + 2 in
  builder h

let exercise_1 =
	test_function_2_against_solution
		[%ty: col tree -> col -> col tree] "monochrome"
		~sampler:(fun () -> sample_col_tree (), sample_col ())
		~gen:5
		[]

Method 2: Defining a sampler

A sampler of a parametric type ('a * 'b * ... ) my_type has a type : (unit -> 'a) -> (unit -> 'b) -> ... -> -> (unit -> ('a * 'b * ...) my_type and must be named sample_my_type.

So for example, if we want to test a function of type col tree -> int, so we need two samplers :

(* Not a parametric type *)
let sample_col () = match Random.int 2 with
  | 0 -> B
  | _ -> R

(* A parametric type *)
let sample_tree (sample: unit -> 'a) : unit -> 'a tree =
  let rec builder h = match h with
    | 0 -> Leaf
    | n -> match Random.int 3 with
      | 0 -> Leaf
      | _ -> Node (builder (h-1), sample (), builder (h-1))
  in
  let h = Random.int 5 + 2 in
  fun () -> builder h

The grading function is then simply :

let exercise_2 =
	test_function_1_against_solution
		[%ty: col tree -> int] "height"
		~gen:5
		[]

Note that if instead of [col tree], the input type is [int tree] (or another type with a predefined sampler), you need nothing more.

let exercise_2bis =
  test_function_1_against_solution
    [%ty: int tree -> int] "height"
    ~gen:5
    []

With these two samplers, we are also able, without more effort, to grade a function of type col tree -> col -> col tree for example. The grader is simply:

let exercise_3 =
	test_function_2_against_solution
		[%ty: col tree -> col -> col tree] "monochrome"
		~gen:5
		[]

Advanced examples

More advanced examples (but nothing new) can be found in exercises/advanced-examples-step-4 directory (branch: step-4).

There is nothing new in these examples, only more complexed types, in particular examples for functional types graded with both methods and using the predefined sampler of list.

The user-defined type is:

type position = {x: int ; y: int}

and its corresponding sampler:

let sample_position () = { x=sample_int () ; y=sample_int () }

First example: get_x

Exactly as shown previously, using method 2:

let exercise_1 =
  test_function_1_against_solution
    [%ty: position -> int ]
    "get_x"
    ~gen:5
    [{ x=0 ; y=0 }]

Second example: map (functional input type)

We want to grade the function ‘map’ for ‘int list’ so we need a sampler for function of type ‘int -> int’.

This is not possible to use the naming convention for a functional type without an alias (see method 2).

let sampler_fun () = match Random.int 3 with
  | 0 -> succ
  | 1 -> pred
  | _ -> fun x -> if x < 0 then -1 else 1
Method 1

For this method, we can just build the proper sampler for all the function arguments.

let sampler_2 () =
  (sampler_fun (), sample_list ~min_size:1 ~max_size:10 sample_int ())

let exercise_2 =
  test_function_2_against_solution
    [%ty:  (int -> int) -> int list -> int list ] "map"
    ~sampler:sampler_2
    ~gen:5
    [(succ, [])]
Method 2

For this method, we need to use an alias for type int -> int.

type f_int_int = int -> int

let sample_f_int_int = sampler_fun

let exercise_2bis =
  test_function_2_against_solution
    [%ty:  f_int_int -> int list -> int list ] "map"
    ~gen:5
    []

Third example: ‘first_elt’ (tuple)

In case you want to grade a function with a tuple as an input type, you can either use method 1 or define an alias and use method 2.

Method 1
let exercise_3 =
  test_function_1_against_solution
    [%ty:  int * int -> int ] "first_elt"
    ~sampler: (fun () -> sample_int (), sample_int ())
    ~gen:5
    []
Method 2
type pair_int = int * int
let sample_pair_int () = sample_int (), sample_int ()

let exercise_3bis =
  test_function_1_against_solution
    [%ty:  pair_int -> int ] "first_elt"
    ~gen:5
    []

Which method should I use ?

Both methods work well for a lot of exercises. However for functional types and tuples, you will need do give an alias to your types to be able do use the second method (see the examples in advanced_examples). This is useful if you need to grade several functions that share some input types.