Adloun

Le langage OCaml

Cours complet · informatique (MP2I/MPI), chapitre 2 · MP2I et MPI

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

2.1 Le contraire de C, et c'est voulu

Le chapitre précédent s'achevait sur cinq points où C montre la machine : types déclarés, entiers qui débordent, accès non vérifiés, passage par valeur, mémoire rendue à la main. OCaml prend l'autre chemin sur quatre d'entre eux, et le programme officiel dit pourquoi : OCaml « permet en particulier aux étudiants de recourir rapidement à un niveau d'abstraction supérieur et de manipuler facilement des structures de données récursives ».

Deux langages, donc, et à parité : « on veille à développer de façon parallèle les compétences de programmation dans ces deux langages ». Ce n'est pas de la redondance. Un même algorithme écrit deux fois, dans deux langages qui ne font pas les mêmes choix, sépare ce qui appartient à l'algorithme de ce qui n'appartient qu'à sa mise en œuvre. C'est exactement ce qu'un concours vous demandera de savoir distinguer.

COCaml
Typesdéclarés par le programmeurinférés par le compilateur
Vérificationstatique, mais faiblestatique et forte
Mémoire`malloc` / `free`automatique
Structure de basele tableaula liste et le type somme
Style dominantimpératiffonctionnel, l'impératif restant possible
ImportantL'annexe B est limitative, comme l'annexe A

Elle porte sur OCaml « version 4 ou supérieure », et distingue là encore ce qui est su sans rappel de ce qui l'est après rappel (section sec:ocaml-apres-rappel). Les modules au sens de module M = struct ... end, les foncteurs, les objets ne sont pas au programme : on utilise des modules (List.length), on n'en écrit pas.

2.2 Tout est expression

Définition 2.1Il n'y a pas d'instruction en OCaml

En C, if est une instruction : elle fait quelque chose. En OCaml, if est une expression : elle vaut quelque chose.


let m = if a > b then a else b     (* m reçoit la VALEUR de l'expression *)

Conséquence immédiate : le else n'est pas facultatif quand la valeur est utilisée — une expression doit valoir quelque chose dans tous les cas. Et les deux branches doivent avoir le même type, car l'expression entière n'en a qu'un.

Définition 2.2Lier une valeur à un nom

let x = 3                     (* définition globale *)
let y = x + 1 in y * 2        (* définition locale : y n'existe que dans « y * 2 » *)

let carre x = x * x           (* une fonction : pas de parenthèses, pas de return *)
let rec fact n =              (* rec : la fonction peut s'appeler elle-même *)
  if n = 0 then 1 else n * fact (n - 1)

let ... in ... est lui-même une expression : il vaut son second membre.

Attention`let` ne fait pas d'affectation

let x = 3 ne crée pas une variable modifiable : il lie le nom x à la valeur 3, définitivement. Écrire ensuite let x = 4 ne modifie pas la première liaison, il en crée une seconde qui masque la première. La distinction est visible dès qu'une fonction entre en jeu :


let a = 10
let f y = y + a       (* f capture la valeur de a AU MOMENT DE LA DÉFINITION *)
let a = 100
let z = f 1           (* z vaut 11, et non 101 *)

C'est la portée lexicale, que l'annexe nomme explicitement : « lorsqu'une définition utilise une variable globale, c'est la valeur de cette variable au moment de la définition qui est prise en compte ».

2.3 Le typage : inféré, fort, polymorphe

Définition 2.3Inférence

Vous n'écrivez pas les types ; le compilateur les trouve.


let double x = 2 * x
(* le compilateur répond : val double : int -> int = <fun> *)

Il a raisonné ainsi : * est la multiplication des entiers, donc x est un int, donc le résultat aussi. La notation int -&gt; int se lit « d'int vers int ».

Définition 2.4Polymorphisme

Quand rien ne contraint le type, il reste variable :


let identite x = x            (* val identite : 'a -> 'a *)
let premier (a, b) = a        (* val premier : 'a * 'b -> 'a *)

'a se lit « alpha » et désigne n'importe quel type. Une seule fonction sert alors pour tous les types — c'est le polymorphisme, dont l'annexe demande une « idée naïve ».

AttentionLe typage fort ne pardonne aucun mélange

1 + 2.5     (* ERREUR de compilation : + attend deux int *)
1.0 +. 2.5  (* correct : les flottants ont LEURS opérateurs, suffixés d'un point *)

Les opérateurs flottants sont +., -., *., /.. C'est déroutant deux jours, puis c'est un filet : en C, 7 / 2 valait sans un mot. En OCaml, une division entière ne peut pas se déguiser en division réelle.

iRemarqueCe qui ne change pas : les dépassements

« Entiers et flottants sont sujets aux dépassements de capacité », dit l'annexe. Le typage fort protège des mélanges de types, pas de l'arithmétique. Un int OCaml est sur 63 bits, donc plus large qu'un int32_t, mais il déborde tout de même.

2.4 Filtrage par motif

C'est le trait central du langage, et celui qui n'a aucun équivalent en C.

Définition 2.5`match ... with`

let rec longueur liste =
  match liste with
  | [] -> 0                      (* motif : la liste vide *)
  | _ :: reste -> 1 + longueur reste   (* motif : une tête, et un reste *)

Chaque motif décrit une forme possible de la valeur, et le premier qui correspond l'emporte. Le motif _ désigne « n'importe quoi, dont je n'ai pas besoin ».

Bonne pratique (Le compilateur vérifie l'exhaustivité, et il faut l'écouter)

Si vous oubliez un cas, OCaml vous avertit : this pattern-matching is not exhaustive. C'est un service que C ne rend pas. Un filtrage incomplet est presque toujours un bogue en attente.

L'annexe pose deux règles sur les motifs : ils « ne doivent pas comporter de variable utilisée antérieurement ni deux fois la même variable », et l'ordre compte « quand ils ont des instances communes ». Le motif le plus général se place donc en dernier.

2.5 Les types structurés

2.5.1 n-uplets et listes

Définition 2.6n-uplets

let p = (3, "trois", 3.0)      (* type : int * string * float *)
let (a, b, c) = p              (* on récupère les composantes SANS match *)
Définition 2.7Listes

Une liste OCaml est immuable et simplement chaînée. Deux constructeurs seulement : [] la liste vide, et :: qui ajoute une tête.


let l = 1 :: 2 :: 3 :: []      (* équivaut à [1; 2; 3] *)
List.length l                  (* 3 *)
l @ [4; 5]                     (* concaténation *)
AttentionLe coût de `@` n'est pas nul, et l'annexe l'exige

L'annexe demande de connaître « l'opérateur @ (y compris sa complexité) ». Concaténer l1 @ l2 recopie toute l1 : c'est . Ajouter un élément en fin de liste dans une boucle coûte donc au total. On ajoute en tête (), quitte à renverser à la fin.

2.5.2 Tableaux, et le retour de la mutabilité

Définition 2.8Tableaux

Là où la liste est immuable et chaînée, le tableau est mutable et à accès direct.


let t = [| 3; 1; 4 |]          (* type : int array *)
let u = Array.make 10 0        (* 10 cases, toutes à 0 *)
t.(0)                          (* lecture : 3 *)
t.(0) <- 7                     (* écriture : la flèche gauche MODIFIE *)
Array.length t                 (* 3 *)
Attention`Array.copy` est superficielle

L'annexe insiste : Array.copy copie « y compris le caractère superficiel de cette copie ». Copier un int array array duplique le tableau extérieur, mais les lignes restent partagées : modifier une ligne de la copie modifie l'original.

AttentionUn accès hors bornes lève une exception — et c'est une bonne nouvelle

let t = [| 1; 2; 3 |] in t.(7)
(* Exception: Invalid_argument "index out of bounds". *)

Comparez au chapitre précédent : le même accès en C écrivait en silence dans une mémoire étrangère. OCaml vérifie, à un petit coût en temps, et vous arrête à l'endroit exact de la faute.

2.5.3 Types sommes et types récursifs

Définition 2.9Déclarer ses propres types

type couleur = Trefle | Carreau | Coeur | Pique      (* type énuméré *)

type forme =                                          (* type somme : chaque cas
                                                         porte ses propres données *)
  | Cercle of float
  | Rectangle of float * float

let aire f =
  match f with
  | Cercle r -> 3.14159 *. r *. r
  | Rectangle (l, h) -> l *. h

Les constructeurs commencent par une majuscule, les identifiants par une minuscule.

Définition 2.10Types récursifs : l'arbre en quatre lignes

type 'a arbre =
  | Vide
  | Noeud of 'a arbre * 'a * 'a arbre

let rec taille a =
  match a with
  | Vide -> 0
  | Noeud (g, _, d) -> 1 + taille g + taille d

Quatre lignes pour le type, quatre pour la fonction. En C, il fallait une structure, un pointeur, un malloc par nœud et un free pour chacun. C'est ce que le programme appelle « manipuler facilement des structures de données récursives », et c'est la raison pour laquelle toute la partie « arbres » du programme se lit mieux en OCaml.

Définition 2.11Le type `option`

type 'a option = None | Some of 'a     (* prédéfini *)

(* Renvoie Some v si la clé figure dans l'association, None sinon. *)
let rec cherche cle assoc =
  match assoc with
  | [] -> None
  | (c, v) :: reste -> if c = cle then Some v else cherche cle reste

option rend l'absence de résultat visible dans le type. Là où C rendait NULL — que rien n'oblige à tester —, OCaml rend une valeur que le filtrage oblige à décomposer.

2.5.4 Ce que l'annexe exige encore

Quatre traits de l'annexe B.1 restent à nommer. Ils sont exigibles sans rappel, et ce sont ceux qu'on emploie sans y penser.

Définition 2.12Curryfication et ordre supérieur

Une fonction OCaml à plusieurs arguments est en réalité une fonction à un argument qui rend une fonction. C'est la curryfication, et elle se lit dans le type :


let ajoute x y = x + y        (* val ajoute : int -> int -> int *)
let incr = ajoute 1           (* val incr : int -> int  — application PARTIELLE *)

Une fonction qui prend ou rend une fonction est dite d'ordre supérieur. List.map en est une : son type ('a -&gt; 'b) -&gt; 'a list -&gt; 'b list annonce qu'elle attend une fonction en premier argument.

Définition 2.13Fonctions anonymes, définitions croisées

let double = fun x -> 2 * x           (* fonction anonyme *)
List.map (fun x y -> x + y) [1; 2]    (* deux arguments, sans nom *)

let rec pair n   = n = 0 || impair (n - 1)   (* let rec … and … : *)
and     impair n = n <> 0 && pair (n - 1)    (* les deux se voient l'une l'autre *)
Définition 2.14Chaînes de caractères

String.length "chien"      (* 5 *)
"chi" ^ "en"               (* concaténation *)
"chien".[0]                (* le caractère 'c' *)

Les chaînes sont immuables : on ne peut pas écrire dans s.[i]. Et char porte un ordre total, donc les comparaisons &lt; et &gt; y ont un sens.

AttentionUne chaîne compte des OCTETS, pas des caractères

String.length &quot;été&quot; vaut 5 et non : en utf-8, chaque « é » occupe deux octets. Et &quot;été&quot;.[1] rend le second octet du premier « é », qui n'est pas un caractère affichable. Le programme ne demande rien sur l'utf-8 ; il faut simplement savoir que String.length ne compte pas ce qu'on croit dès qu'on sort de l'ascii.

Définition 2.15Affichage, saisie, et `begin … end`

print_int 42; print_string "\n"; print_float 3.14
let n = read_int ()          (* aussi read_float, read_line *)

if c then begin              (* begin … end groupe plusieurs expressions *)
  print_string "oui";        (* là où la syntaxe n'accepterait qu'une seule *)
  incr compteur
end

begin ... end est un synonyme exact de la parenthèse ; on l'emploie quand le bloc tient sur plusieurs lignes, où il se lit mieux.

2.6 Programmer impérativement en OCaml

Le programme le demande : OCaml n'est pas cantonné au style fonctionnel.

Définition 2.16Références, séquence, boucles

let compteur = ref 0        (* une CASE modifiable, de type int ref *)
compteur := !compteur + 1   (* := écrit,  ! lit *)

let somme t =
  let s = ref 0 in
  for i = 0 to Array.length t - 1 do
    s := !s + t.(i)
  done;
  !s

let attendre c =                (* pas de « rec » : rien ne s'appelle soi-même *)
  while !c > 0 do c := !c - 1 done

Le point-virgule sépare deux expressions : il évalue la première, jette sa valeur, et rend la seconde. Le type unit, dont l'unique valeur s'écrit (), est celui des expressions qu'on évalue pour leur effet et non pour leur valeur.

Bonne pratique (Les références « doivent être utilisées à bon escient »)

C'est la formule de l'annexe, et elle mérite d'être prise au mot. Une accumulation dans une ref au sein d'une boucle est souvent la traduction littérale d'un programme C, là où un parcours récursif ou un Array.fold dirait la même chose plus clairement. On garde les références pour ce qu'elles font le mieux : compter, mémoriser un état qui change vraiment.

Définition 2.17Exceptions

let division a b =
  if b = 0 then raise Division_by_zero else a / b

let sur a b =
  try Some (division a b) with Division_by_zero -> None

let jamais () = failwith "cas impossible"   (* pour l'irrattrapable *)

Le programme met en garde, et c'est une remarque de fond : « on veille à ne pas laisser penser que les exceptions servent uniquement à gérer des erreurs ». Une exception est aussi une sortie anticipée légitime — trouver un élément et quitter la boucle sur-le-champ, par exemple.

2.7 Le même algorithme, deux fois

Exemple 2.18Recherche du maximum, en C et en OCaml

/* Renvoie le plus grand des n premiers termes de t. Précondition : n >= 1. */
int maximum(const int t[], int n) {
    assert(n >= 1);
    int m = t[0];
    for (int i = 1; i < n; i = i + 1) {
        if (t[i] > m) { m = t[i]; }
    }
    return m;
}

(* Renvoie le plus grand élément de t. Précondition : t est non vide. *)
let maximum t =
  assert (Array.length t >= 1);
  let m = ref t.(0) in
  for i = 1 to Array.length t - 1 do
    if t.(i) > !m then m := t.(i)
  done;
  !m

(* La même chose sur une liste, sans aucune case modifiable. *)
let rec maximum_liste = function
  | [] -> failwith "liste vide"
  | [x] -> x
  | x :: reste -> max x (maximum_liste reste)

Les deux premières versions sont le même algorithme : un accumulateur, un parcours. La troisième est un autre algorithme, récursif, que la liste appelle naturellement. Savoir laquelle écrire — et pourquoi — est l'objet de tout le reste de ce livre.

2.8 Éléments utilisables après rappel

2.9 Ce qu'il faut retenir

ImportantChoisir son langage, à chaque problème
C s'impose quand…OCaml s'impose quand…
la structure est un tableau que l'on parcourt et modifiela structure est récursive : arbre, formule, expression
la mémoire et son coût sont le sujetle raisonnement par cas est le sujet
l'on veut voir ce que fait la machinel'on veut que le compilateur vérifie les cas

Aucun des deux n'est « le bon ». Un même chapitre de ce livre les emploiera souvent tous les deux : le programme le demande, et surtout, un algorithme qui ne survit pas au changement de langage n'était pas un algorithme — c'était une astuce.

Continuer sur Adloun : animation, QCM, fiches, exercices