▸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.