Goals

  • Get a hand on serious type structures: records and variant datatypes.
  • Understand that, although mutable types exist in OCaml, you can avoid them most of the time.

Requirements

➪ You have completed OCaml - Lesson 3.

Mission 4

Type structures

Records

  • Records in OCaml must be declared using a type declaration:
    (* This defines a record type. *) type coordinates = { long: float ; lat: float } (* This defines only an alias. *) type path = coordinates list (* Another record *) type region = { region_name: string ; borders: path ; has_coastline: bool } (** Functions are first-class, they can appear in records. **) type test = { (* A function which should be tested. *) fonc: (int -> int) ; (* An argument, which will be given to the function. *) arg: int ; (* The expected result. *) expect: int }
  • Construction: a record is built like this let point1 = { long = -0.3 ; lat = 42.5 }
    or like this, by copying another record let point2 = { point1 with long = -1.0 }
  • Deconstruction: the standard notation .field, for example let get_lat c = c.lat
    • It is also possible to use a pattern: let get_lat { lat = some_name ; _ } = some_name lat is a field name, some_name is a variable which takes the value of lat.
    • The pattern { lat ; _ } is syntactic sugar for { lat = lat ; _ }, hence you can write let get_lat { lat ; _ } = lat
    • Does let get_lat { some_name ; _ } = some_name mean anything, given the types defined above?
    • A record pattern can bind several variables:
      Guess the type of let get_all { long ; lat } = (long, lat)
  • Advanced pattern matching: study this example
    (* Checks if a region is well defined: it must have a region name and at least one border. *) let is_good = function | { region_name = "" ; _ } -> false | { borders = [] ; _ } -> false | _ -> true

    By the way, it is possible to join patterns like this:

    let is_good = function | { region_name = "" } | { borders = [] } -> false | _ -> true
  • Practice with records:
    Exercise : Records
    • We use the type test defined above.
    • Write a function apply of type test → bool which returns true iff the function fonc applied to its argument arg returns the expected value expect. (No pattern-matching needed.)
    • Check your function apply with one or two tests of yours:
      let test1 = { ... } and test2 = { ... } let result1 = apply test1

Parameterized types

  • The type list is a parameterized type: it always has a type argument, e.g. int list. You can never have an expression of type list alone.
  • What is the type of the empty list?
  • 'a option is also a parameterized type.
  • We will now define our own parameterized type, using the type test of the previous section. The type parameter 'a (means α) stands for the type of the argument of fonc.
    (* A parameterized record type. *) type 'a test = { fonc: ('a -> int) ; arg: 'a ; expect: int }
    You should avoid having two versions of type test, so just modify the previous definition in place.
  • You do not need to modify your function apply ; you should see that:
    • the function compiles, its type is now polymorphic.
    • the tests you have written are still valid, their type is monomorphic: int test
    • write another test of a different type, e.g. bool test or string test
      (* Your tests, written in the previous section, now have type: int test. *) let test1 = { ... } and test2 = { ... } (* Add other tests with a different type, e.g. bool test. *) and test3 = { ... }
  • We can add another type parameter, standing for the type of the result of fonc (which does not need to be int).
    Enrich the type test as follows: type ('a, 'b) test = ...
    Update the type definition. 'b should occur twice in the record type.
  • The existing code for apply is now more polymorphic (the previous versions were just particular cases of this generic version).
  • Check the existing tests, and add another one with a different return type. You should have tests with types: (int, int) test, (bool, int) test, and for example (bool, string) test.
    (* Previous tests : (int, int) test. *) let test1 = { ... } and test2 = { ... } (* Tests added above: (bool, int) test. *) and test3 = { ... } (* Add other tests with type: (bool, bool) test or (bool, string) test, ... *) and test4 = { ... }
  • This was one promise of these lessons: you have written a polymorphic function without even noticing.

Arrays

  • Arrays exist in OCaml. Nothing new, so we study them very quickly.
    They are mutable structures (they have side-effects).
  • The array type is parameterized, like lists: int array
  • To build an array: let john = Array.make 100 true builds an array with 100 cells containing true.
    • All cells are always initialized.
    • Cells are indexed from 0 to 99, like in C.
    • What is the type of Array.make? Check in the standard library manual, look for the Array module.
  • To read a value: john.(index)
  • To set a value: john.(index) <- value
Exercise : Arrays
  • Guess the type of let foo a i = a.(i)
  • Guess the type of let bar a i v = a.(i) <- v
  • Both functions already exist. Find them in the Array module.

Mutable records

  • It is possible to define mutable records, that is records with modifiable fields. (Thus, it is also a structure with side effects.)
    type player = { name: string ; age: int ; mutable points: int }
    In this example, only the points field is mutable. Other fields (name, age) are immutable.
    Mutable fields can be modified like arrays: foo.points <- 800 which has type unit.
  • Write a function show_player with type player → unit which prints the name, age, and points of a player (using Printf). Remember to use %! in Printf to flush output.
  • Write a function new_player with type string → int → player which creates a new player with the given name and age, and 0 points.
  • Write a function add_points with type player → int → unit which adds the given number of points to the player.
  • Test these functions with two players. Add points at least twice on each player.You need the sequence operator ; .
Comparison with immutable records
  • Define the type iplayer with immutable fields iname, iage and ipoints.
  • Define the same functions:
    show_iplayer:iplayer → unit
    new_iplayer:string → int → iplayer
    add_ipoints:iplayer → int → iplayer
    Notice that, since iplayer is immutable, the type of add_ipoints must be different from the mutable version.
  • Write again some tests, following this model:
    let test () = (* Create an iplayer and add some points. *) let p1_a = new_iplayer ... in let p1_b = add_ipoints p1_a 50 in let p1_c = add_ipoints ... in show_iplayer p1_c
Exercise : Mutable types
  • By looking at the signature of the Set module, try to determine if a Set structure in OCaml is mutable or not.
    • Here, the type of a set is written t and elements have type elt.
    • Do not try to use this module. It is actually a functor which might be studied later, or not.
    • You only need to look at the type of function add.
  • By looking only at the type of function add, same question for the Queue module.
    Enjoy, it is a parameterized type.
  • Same question for the Hashtbl module which admits two type parameters.
  • And finally, same question for the Map module.
  • You have found that two of them are mutable and two are immutable. You conclude that, obviously, the standard library is not purely functional: some modules have structures with side-effects.
References
  • Consider the following type: type 'a ref = { mutable contents: 'a } (do not define it, it already exists)
  • Understand the following functions, and guess their types:
    let create x = { contents = x } let read rf = rf.contents let write rf v = rf.contents <- v
  • Write a short test which creates a reference with value 0 and increment it twice.
    let test () = ...
    You still need the sequence operator ; .
  • Those three functions already exist in OCaml:
    let ref x = { contents = x } let (!) rf = rf.contents let (:=) rf v = rf.contents <- v
    where ! and := are infix operators. Thus you can write !r to read the value of r, and r := 100 to set the value of r.
    Rewrite your previous example using these notations.
  • References are the common way to have mutable variables, like in other evil side-effect languages.
  • When programming in OCaml, you seldom need references. Here is a typical example, though, where references are useful.
Exercise : References
  • Consider these definitions. Guess their type (take your time).
    let gen1 = fun () -> 0 let gen2 = fun () -> let count = ref 0 in count := !count + 1 ; !count let gen3 = let count = ref 0 in fun () -> count := !count + 1 ; !count
  • Try to apply the three functions several times.
  • Is it possible to modify the reference count from outside the function gen3?

Variant datatypes

  • Remember how to define an enum type? type color = White | Yellow | Green | Blue | Red
    Constructors must start with a capital.
  • This is called a variant datatype, or also a union type (or abusively, just "datatype").
    They are more powerful than simple enumerations.
  • We define, further, a record type people representing people in a football (soccer) video game. Before that, we need a type to represent the different roles:
    type role = Player | Referee This is again a classical enumeration.
    • Players belong to a team, they have a colored jersey and a number, like this.
    • Referees do not belong to a team, they do not have a number, and have no hair, like this.
    • We can enrich the role datatype to add a color and number to Players only:
      type role = Player of color * int | Referee (def)
      Here a player has an attribute of type color * int
  • This type definition (def) actually defines three things:
    • A new type name:role
    • A constructor Referee, which has no argument. Try: Referee ;;
    • A constructor Player, which has two arguments. Try: Player ;; the type checker complains.
  • Construction: let role1 = Referee let role2 = Player (Green, 8) let role3 = Player (Yellow, 10)
  • Deconstruction, by pattern-matching:
    Guess the type of: let get_number = function | Referee -> 0 | Player (_, nb) -> nb
  • Finally, we define the type people:type people = { name: string ; role: role ; age: int }
StarExercise : Simple variants
  • Write a function same_team with type people → people → bool which returns true iff the two people are Players with the same color. Do not use other functions (pattern-matching does half the job).
    You have probably got it wrong, so test it!
  • Write a function is_number with type people → int → bool which returns true iff the people is a player with the given number. Do not use other functions.
    Check that your function does not always return true on players.

Parameterized and recursive variants

One of the main usage of variants is to define recursive, and usually parameterized, structures.

Custom lists
Lists are a first example. We can define our own list structure with textual constructors:
type 'a mylist = Empty | Cell of 'a * 'a mylist (Note that these constructors are completely distinct from the builtin list type.)
The type mylist is recursive: a cell contains a mylist which represents the tail (the following cells). Here is an example: Cell( 10, Cell(20, Empty) )
  • Write a function myhd with type 'a mylist → 'a which returns the first element of a list, or fails with failwith "empty list" .
    No if, only pattern-matching.
  • Similarly, define mytl with type 'a mylist → 'a mylist which returns the tail of a list, or fails.
  • By the way, can you guess the type of failwith "foo" ? Trying failwith "foo" ;; does not give its type, but you have other ways to find it.
  • Define mylength with type 'a mylist → int which returns the length of a list. Do not use mytl or myhd, use only pattern-matching.
  • Define the tail-recursive version of mylength, using an accumulator. Its type is int → α mylist → int.
  • In order to define the tail-recursive version of mylength while keeping the original type α mylist → int, we proceed as follows: let mylength l = let rec loop acu l = (* This is your tail-recursive function *) ... in loop 0 l
Builtin lists
The builtin list type is defined like this:
  • type 'a list = [] | (::) of 'a * 'a list (Do not redefine it)
  • :: is a prefix constructor, so you write a :: b instead of :: (a,b)
    It works also in pattern-matching (which you already know):
    let f = function | [] -> "empty list" | a :: _ -> "list starting with " ^ a
    (As usual, try to guess the type of f)
Option type

The following type is already defined in OCaml: type 'a option = None | Some of 'a
It can be used to return a result (Some x), or nothing (None) when nothing can be returned.

Exercise : Option type
  • Write a function ohd ("optional head") of type 'a list → 'a option which returns Some x if the list head is x, and returns None if the list is empty.
    Use only pattern-matching. No if, no other functions.
  • Similarly for otl ("optional tail").

Exercises on lists

StarStarExercise : Ad-hoc functions

We use the type people defined in the Variant section.

  • Write a function get_referees with type people list → people list which takes a list of people and returns the same list, keeping only the Referees.
  • Write a function get_younger with type people list → int → people list such that get_younger people_list 20 returns the list of people being at most 20 years old.
  • Write a function find_color with type people list → color → people option which returns the first player of the list with the given color (returns Some x), or None if nobody has the expected color.
  • You have thoroughly tested these functions.
StarStarExercise : Generic functions

We now write polymorphic higher-order functions on lists, and see that the previous exercise is only a particular case of them.

  • Write a polymorphic, higher-order function filter with type ∀α. (α → bool) → α list → α list such that filter pred alist returns the elements of alist for which the predicate pred returns true.
    filter (fun x -> x mod 2 = 0) [ 1 ; 2 ; 3 ; 4 ; 5 ; 6] [2 ; 4 ; 6]
    filter (fun x -> x < 10) [ 100 ; 5 ; 15 ; 6 ; 16 ; 7 ] [5 ; 6 ; 7]
    filter (fun x -> x < 0) [ 100 ; 5 ; 15 ; 6 ; 16 ; 7 ] []
    The tail-recursive version returns inverted lists, e.g. [6 ; 4 ; 2]
  • Rewrite the above functions get_referees and get_younger in one line.
  • Write a function find with type ∀α. (α → bool) → α list → α option such that find pred alist returns the first element of the list that satisfies pred (returns Some x), or None if no element satisfies pred.
  • Write a function has_color with type color → people → bool which returns true iff the people is a player with the expected color.
    Then, rewrite the above function find_color in one line.
  • The function filter already exists in the module List of the standard library. Find it.
  • The function find already exists, but with a different type. Understand how the List.find function is different.

Outcomes

Once the mission is completed, you must be able to:

  • Determine, by looking only at its signature, if a module uses mutable or immutable data structures.
  • Keep your upper lip stiff when faced with parameterized types and variant datatypes.
  • Write recursive functions working on lists.
  • Think in terms of generic, higher-order functions operating on lists.