Cours alternatif d’OCaml

Logo OCaml
III — Modéliser des données

Les types sommes

Cette façon de définir des types est sans doute la plus utile et la plus courante en OCaml, et en programmation fonctionnelle en général. Cette fois, on décrit différentes formes que peut prendre notre type. On limite donc ses valeurs à un ensemble restreint, ayant chacun un sens différent. Ces différentes façons de construire notre type s’appellent des constructeurs.

La syntaxe générale pour définir un type somme est la suivante :

type NOM = CONSTRUCTEUR_1 | CONSTRUCTEUR_2

On sépare donc les différents constructeurs par des |. Ici je n’en ai mis que deux, mais on peut en avoir autant qu’on veut. Leurs noms sont écrits avec une majuscule, et on utilise aussi des majuscules pour séparer les mots dans un même nom de constructeur : on écrira MonConstructeur et pas Mon_constructeur par exemple.

Voyons un petit exemple :

type taille = Petit | Moyen | Grand
type garniture = Vegetarien | Poulet | Boeuf
type sauce = Fromage | Algerienne | Curry | Harissa | Samurai | Blanche

type tacos = taille * garniture * sauce

Ici on a défini trois types sommes : taille, garniture et sauce (tacos est un type produit). L’avantage des types sommes est qu’il est impossible de créer des valeurs en dehors des constructeurs qu’on a définis : avec les types qu’on a implémentés au-dessus, il n’y a pas moyen d’avoir un tacos « XXL »1

Pour créer une valeur de ce type, on utilise ensuite le nom d’un des constructeurs :

let ma_sauce_favorite = Curry

On peut faire du pattern-matching sur une valeur qui a un type énuméré :

let prix_viande (v : viande) : float =
  match v with
  | Vegetarien -> 4.5
  | Poulet -> 5.0
  | Boeuf -> 6.0

Les égalités aussi fonctionnent, donc on peut utiliser les if/else :

let prix_taille (t : taille) : float =
  if taille = Petit then
    4.0
  else if taille = Moyen then
    5.0
  else
    5.5

Notez qu’on n’est pas obligé d’écrire la définition du type sur une seule ligne. Dès qu’il commence à y avoir beaucoup de constructeurs, faire un retour à la ligne avant chaque | est même conseillé :

type region =
  | AuvergneRhoneAlpes
  | Bretagne
  | GrandEst
  | HautsDeFrance
  | IleDeFrance
  | NouvelleAquitaine
  | Occitanie
  | ProvenceAlpesCoteDAzur
(* etc. je les connais pas toutes, mais vous avez compris *)

Associer des données aux constructeurs

On peut aussi, de manière optionelle, associer des informations à un constructeur. Pour préciser le type de ces informations, on utilise le mot-clé of après le nom du constructeur, suivi du type qu’on veut. Pour créer des valeurs avec ce constructeur, on précisera entre parenthèse après le nom du constructeur la valeur associée.

Par exemple, si on veut créer un type énuméré pour le résultat d’une fonction qui peut échouer, on pourrait faire :

type resultat =
  | Ok of float (* On devra associer un float avec le constructeur Ok *)
  | Erreur

(* On peut utiliser ce type pour implémenter une fonction de division qui ne plante pas.
 * En cas d’erreur, elle retournera juste Erreur, et on sera obligé de gérer le cas où la division a échoué ensuite *)
let div (x : float) (d : float) : resultat =
  if d = 0.0 then
    Erreur (* Division par 0 impossible *)
  else
    Ok(x /. d) (* C’est ici qu’on utilise notre constructeur avec des données associées *)

On peut aussi déconstruire les valeurs de ce genre de constructeurs, et donc les utiliser avec du pattern-matching :

let ma_division = div 5. 2.

(* ma_division est de type resultat, donc on ne peut pas récupérer la valeur potentielle sans gérer
 * le cas où on a eu une Erreur. On peut donc être sûr que notre programme ne plantera pas. *)
match ma_division with
| Ok(res) -> "5 divisés par 2 donne : " ^ (string_of_float res)
| Erreur -> "On ne peut pas faire cette division !"

Les types qu’on associe au constructeur peuvent bien sûr être plus complexes : on peut utiliser des types sommes, produits, etc.

Différence entre type somme, énuméré et algébrique

Les types sommes sont aussi parfois appelés types énumérés ou types algébriques. Il n’y a pas vraiment de différence en réalité, mais si elle vous est demandé lors d’un contrôle d’INF201, sachez que :


  1. ce qui est bien dommage, il faudrait peut-être changer la définition de taille…