III. Establish data model

1. A little theory

So far, we’ve only used simple values, and these values use only the basic types in OCaml. But when we want to implement real programs, we need more complex types than just ints or strings. For example, suppose you want to create an application that sends and receives messages. You can model this message using a simple string. But what if you want to store it in memory or operate on the sender’s name? What if you want to manipulate the message and the date it was sent?

We can no longer model with these simple types when the situation is complicated by the increase in information. If you want to write a function that takes a dozen of these variables as arguments, it’s not very easy. This is especially true if you want to add another item, which will cause us to add another variable to the whole article.

Fortunately, OCaml allows us to create our own data types. In this tutorial, we’ll learn four ways to create new types:

  • Synonymous type;
  • Product type;
  • Addition type;
  • Type of structure (as an addition to the original course);

2. Synonymous types

The first way to create our own types is to create synonymous types. The goal is to create a nickname for an existing type. The syntax is as follows:

type NAME = TYPE
Copy the code

NAME is the NAME of the other NAME, and TYPE is the TYPE. In OCaml, type names can only be lowercase letters, and if different words need to be separated, they can be separated using _ (we would write my_type instead of myType). You can use primitive types to run the same functions and operators, and the two types are interchangeable.

Although the meaning of this type may not seem like much, such cognate types can make our code easier to read than the same type, and you can give them additional meaning. Compare these two examples:

(* no synonymous types * are used)

let s (d : float) (t : float) : float = d /. t
(* function of type float -> float -> float *)
Copy the code
(* We start by defining synonymous types *)
type speed = float
type time = float
type distance = float

(* then apply them to functions *)
let s (d : distance) (t : time) : speed = d /. t
(* This function is of type distance -> time -> speed, which makes it much clearer *)
Copy the code

With synonymous types, we are less likely to mix and reverse values.

But this is clearly not the most interesting way to create new types, nor is it the most practical way.

3. Product type

Let’s now look at another type of type (🤔️) : the product type. We also call this a multivariate group or tuple (that’s the name in Python). We just need to group together several values of different types. By analogy with the product of sets in mathematics, they are called product types.

To define a product type, we use * to separate the different types of values in the combination. For example, if we want to model the time of day, we need to use three ints (one for hours, one for minutes, and one for seconds). Let’s write:

int*int*int
Copy the code

In general, to make it easier to use newly defined types, we will define a synonym for these types.

type time = int * int * int
Copy the code

To create a value of this type, we combine it with parentheses and separate it with commas (as in Python) :

type time = int * int * int

(* This constant is of type time, which is int * int * int. These two are equivalent *)
let end_of_class = (16.45.0)
Copy the code

In addition to building values of this type, you can “deconstruct” them (or “decompose” them, depending on how sophisticated you are). To do this, we use a “compound” let, with different elements in the tuple:

(* let end_of_class = int * int * int *)

let (hours, minutes, seconds) = end_of_class
(* hours, minutes, seconds = three new constants, int *)
Copy the code

Also note that parentheses are actually optional when constructing or deconstructing these types. But usually for clarity and to avoid ambiguity, we put parentheses. It would be better for us and the compiler for OCaml.

Destructuring methods can also be used directly in function arguments:

let add_a_hour (hour, minutes, seconds: time) = (hour + 1, minutes, seconds)
Copy the code

We can also use deconstruction in pattern matching:

let meal (hour: time): string =
  match hour with
  | (7, _, _) | (8, _, _) - >"Breakfast"
  | (12.0.0) - >"Lunch"
  | (16.30.0) - >"Snack"
  | (19.30.0) - >"Dinner"
  | (h, _, _) -> (string_of_int h) ^ "hours? It's not time to eat!"
Copy the code

Again, it works in comparison. We can use a value with a product type in if:

(* a set of names + password *)
type identifiers = string * string

let message_secret id =
  if id = ("Philippe Poutou"."ilovecreps") then
    "Hello comrade Poutou !"
  else
    "Incorrect name or password."
Copy the code

4. Addition type

Without a doubt, this is the most practical and common way to define types in OCaml, as well as in functional programming. This time, we’ll use a different form to describe types. We restrict its values to a finite set, each of which has a different meaning. These methods for building different types are called constructor.

The general syntax for defining an addition type is as follows:

type NAME = CONSTRUCTORS_1 |CONSTRUCTORS_2
Copy the code

We use | to separate different constructor. Here, I only have two examples, but we can write as many as we want. Their names should start with a capital letter, and we will also use a capital letter to separate different words in constructor’s name: for example, we write MyConstructor instead of My_constructor.

Let’s look at a small example:

type size = Small | Medium | Large
type garnish = Vegetarian | Chicken | Beef
type sauce = Cheese | Algerian | Curry | Harissa | Samurai | Blanche

type tacos = size * garnish * sauce
Copy the code

Here we define three addition types: size, garnish, and sauce (tacos is the product function). The advantage of the addition type is that it is impossible to create a value other than constructor, which is already defined: with our defined type above, it is impossible to have a Tacos1 with the sign ‘XXL’.

To create a value of this type, we use the name of one of the Builders;

let my_sauce_favorite = Curry
Copy the code

We can also use matching patterns in values of enumerated types:

let meat_price (m: meat): float =
  match m with
  | Vegetarian -> 4.5
  | Chicken -> 5.0
  | Beef -> 6.0
Copy the code

The same is true in equations, so we can use if/else:

let price_size (s: size): float =
  if size = Small then
    4.0
  else if size = Medium then
    5.0
  else
    5.5
Copy the code

Note that we did not specify that each type definition must be a single line. But if there’s a lot of constructor, or suggest a newline before each | :

type region =
  | AuvergneRhoneAlpes
  | Bretagne
  | GrandEst
  | HautsDeFrance
  | IleDeFrance
  | NouvelleAquitaine
  | Occitanie
  | ProvenceAlpesCoteDAzur
(* and wait, I don't know all the place names, but you get the idea *)
Copy the code

Correlate data with Constructor

We can also choose to associate information with Constructor. To more accurately describe the type of information, we use the keyword “of” after the name of constructor, which can be followed by our target type. To create a new value using this constructor, we can put parentheses after the constructor name and indicate the value associated with it.

For example, if we wanted to create an enumeration type for the result of an error-prone function, we could do this:

type resultat =
  | Ok of float (* We associate float with Builder OK *)
  | Error

* We can use this type to implement a division function that does not crash. * If there is an Error, it will return Error. It is also necessary to handle division failures *)
let div (x : float) (d : float) : resultat =
  if d = 0.0 then
    Error (* Division by 0 impossible *)
  else
    Ok(x /. d) (* this is where you use the Data association Builder *)
Copy the code

We can also deconstruct this constructor value, so we can use the matching pattern:

let my_division = div 5. 2.

(* my_division is of type resultat, so we have to manipulate it to get the value it contains. * We've already accounted for error cases, so we can be sure our program won't crash. *)
match my_division with
| Ok(res) -> "Divide 5 by 2 and you get:" ^ (string_of_float res)
| Erreur -> "There's no way we can do that!"
Copy the code

The types we associate with Constructor can certainly be more complex: we can use addition types, product types, and so on.

Differences between additive types, enumerated types, and algebraic types

Additive types are sometimes called enumerated types or algebraic types. It doesn’t make much difference in real life, but if you’re asked a question like this on an INF201 quiz, you need to know:

  • If your type is just “simple” constructor (with no associated value), it is an enumeration type;
  • It is an addition type if it has at least one constructor with an associated value.

5. Add-on: Structure type

This method of creating these types is not within the scope of the original course, but it is still a valid method.

The structure type looks similar to the product type because both methods combine several values together. The difference is that we give each value a name so that we can more easily identify them. We still add the name we want to give the keyword type and an equal sign after it.

We then use a curly brace, after which we can define them with different values (also called “fields”) names and types. We can use: to separate names from types, and semicolons to separate values.

Let’s take an example of a model that builds contact information (say, a messaging application) :

type contact = {
  name : string;
  firstname : string;
  age : int;
  telephone : int * int * int * int * int; (* Five separate numbers are easier to read than a long string of numbers *)
  email : string;
}
Copy the code

To create a value of this type, we use a curly brace and enter an equals sign between the field name and value. We then write data after each of the defined names.

Here’s an example:

let manon = {
  name = "Sélon" ;
  firstname = "Manon" ;
  age = 18 ;
  telephone = (06.06.66.66.06);(* This is fake, do not call this number, thank you *)
  email = "[email protected]" ;
}
Copy the code

We can also deconstruct values of structure types in this way:

let { name; firstname; age  } = manon
(* We now have three new constants: name, firstName, and age *)
Copy the code

It is important to note that we do not have to list all the values in the target field during the structuring process.

We can also use other names with the following statement:

let { name = name_of_manon ; age = age_of_manon } = manon
(* We get two new constants: nom_of_manon and age_of_manon *)
Copy the code

Since we can deconstruct these values, we can also match them:

let iscalled_manon (cont : contact) : bool =
  match cont with
  | { firstname = "Manon"} - >true| _ - >false
Copy the code

We can also use VALEUR.CHAMP to read the values in a field separately:

let full_name (cont : contact) : string =
  cont.firstname ^ "" ^ cont.name
Copy the code

6. Practice after class

Here are some exercises to verify that you have understood the section on types.

We defined these types:

(* We model the course *)

type ue =
  | Inf201
  | Inf203
  | Mat201
  | Mat203
  | Phy201
  | Phy202
  | break

type schedule = int * int (* hours and minutes *)
type course = schedule * schedule * ue (* Start time, end time and course UE *)
Copy the code

What is the type of the following expression (only the “simplest” version is accepted)?

MAT201
Copy the code
< Answer point me >
    ue
Copy the code
((13.30), (15.00), Inf201)
Copy the code
< Answer point me >
    course
Copy the code
(13.30.00)
Copy the code
< Answer point me >
    int*int*int
Copy the code
'Z'
Copy the code
< Answer point me >
    char
Copy the code
(15.32)
Copy the code
< Answer point me >
    int*intOr the scheduleCopy the code

In order for the code to work correctly, what should be put in _____?

type tram = A | B | C | D | E
type transport =
  | Bus of int (* Line Number *)
  | Tram of______ |... | ______let name_tram (t : tram) : string =
  match t with
  | A -> "A"
  | B -> "B"
  | C -> "C"
  | D -> "D"
  | E -> "E"

let name_transport (tra : transport) : string =
  match tra with
  | Bus(line) -> "Line " ^ (string_of_int line) ^ " of bus"
  | Tram(t) -> "Tram " ^ (name_tram t)
  | Car -> "Car"
  | Bicycle -> "Bicycle"
Copy the code
< Answer point me >
    tram
Copy the code

And then the

< Answer point me >
    Car
Copy the code

Finally,

< Answer point me >
    Bicycle
Copy the code

We’re going to model the music collection now. To do this, we create the following types: music, genre, album, type_album (single, EP, or album). You can do whatever you want, but make it as complete a variety as possible. You can also add more types (such as Artist) if you need to.

< Reference answer >
type genre =
  | Classic
  | Electro
  | KPop
  | Pop
  | Rap
  | RnB
  | Rock

type artist = string (* Singer's name *)

type type_album = Single | EP | Album

type album = artiste * type_album * string (* string is the album name *)

type music = string * genre * album * artist

(* If you want, we can also use structure types to define albums and music: *)
type album = {
  alb : album;
  alb_type : type_album;
  name : string;
}

type music = {
  title : string;
  genre : genre;
  alb : album;
  art : artiste;
}
Copy the code

I recommend that you use functions to continue manipulating these types.

  • same_album: Two songs on the same album.
  • same_genre: Both pieces of music belong to the same category.
  • long_title: If the title is long (more than 20 letters). We can useString.length : string -> intFunction.
< Reference answer >
let same_album (a : music) (b : music) : bool =
  let (_, _, alb_a, _) = a in
  let (_, _, alb_b, _) = b in
  alb_a = alb_b

let same_genre (a : music) (b : music) : bool =
  let (_, genre_a, _, _) = a in
  let (_, genre_b, _, _) = b in
  genre_a = genre_b

let long_title (m : music) : bool =
  let (title, _, _, _) = m in
  (String.length title) > 20

(* If you are using a structure type, do this: *)
let same_album (a : music) (b : music) : bool =
  a.alb = b.alb

let same_genre (a : music) (b : music) : bool =
  a.genre = b.genre

let long_title (m : music) : bool =
  (String.length m.title) > 20
Copy the code

  1. Unfortunately, maybe we should revise itsizeThe definition of…↩