Cours alternatif d’OCaml

Logo OCaml
V — Coder, et bien coder

Écrire une spécification

Quand on commence à réaliser des fonctions un peu complexe, il peut être difficile d’écrire du code correct du premier coup. On sort alors généralement un papier et un crayon et on commence à faire des schémas ou à écrire des exemples pour mieux comprendre comment fonctionnera notre code.

De même, il peut parfois être complexe de comprendre du premier coup d’œil ce que fait une fonction, quand on lit du code.

Pour éviter ces problèmes, on va souvent passer par une étape de spécification, qui consiste à réfléchir et à écrire de manière explicite le fonctionnement de notre code. Ensuite, on passera à l’implémentation, c’est-à-dire le code en lui-même. Ainsi, on sait où on va quand on écrit notre code plutôt que de tâtonner, et on évite ainsi beaucoup d’erreurs de raisonnement.

Concrètement, une spécification est un commentaire qu’on place juste avant le code d’une fonction (ou qui peut être en dehors du code, notamment en examen si on vous demande juste une spécification). Ce commentaire contient :

Voici un exemple concret avec une fonction qui donnerait le maximum de deux éléments :

(* maximum : ℕ → ℕ → ℕ
 *           int -> int -> int
 *
 * Renvoie le maximum de deux entiers.
 *
 * maximum 2 3 = 3
 * maximum 5 5 = 5
 *)

Pour rendre les différentes parties plus explicites, on met souvent des titres devant :

(* SPÉCIFICATION : maximum
 * PROFIL :        ℕ → ℕ → ℕ
 * SIGNATURE :     int -> int -> int
 *
 * SÉMANTIQUE :    Renvoie le maximum de deux entiers.
 *
 * EXEMPLES :
 *
 *   maximum 2 3 = 3
 *   maximum 5 5 = 5
 *)

Ici, écrire à la fois le profil est la signature fait du sens, car on n’écrit pas les ensembles ℕ et int de la même façon en « mathématiques » et en OCaml. Mais si on avait le code suivant :

type seq =
  | Fin
  | Element of int * seq

let rec foix_deux (s : seq) : seq =
  match s with
  | Fin -> Fin
  | Element(x, suite) -> Element(x * 2, fois_deux suite)

On pourrait écrire ce commentaire au-dessus de foix_deux pour la spécifier, sans distinction entre profil et signature :

(* SPÉCIFICATION : premier
 * PROFIL :        seq → seq
 *
 * SÉMANTIQUE :    Multiplie tous les éléments d’une séquence par deux.
 *
 * EXEMPLES :
 *
 *   foix_deux (Element(1, Element(5, Fin))) = (Element(2, Element(10, Fin)))
 *)

Les équations récursives

Quand on écrit des fonctions récursives, une étape supplémentaire est ajoutée à la spécification : l’écriture d’équations récursives. Il s’agit simplement d’équations qui ressemblent plus ou moins à celles qu’on pourrait trouver en mathématiques, et qui disent comment se comporte notre fonction récursive dans les différents cas. Concrètement, c’est une autre façon d’écrire les différents cas du match ou du if/else qu’on mettra dans notre code. Mais contrairement au match, l’ordre des différents cas n’a pas d’importance, car on précisera à chaque fois dans quel cas on se trouve s’il y a ambiguïté.

Voici un exemple (sans le reste de la spécification, uniquement les équations récursives) avec une fonction qui calcule la somme d’une séquence d’entiers (avec le même type seq qu’au-dessus) :

(* …
 *
 * ÉQUATIONS RÉCURSIVES :
 *   somme(Fin) = 0
 *   somme(Element(x, suite)) = x + somme(suite)
 * …
 *)
let rec somme (s : seq) : int =
  match s with
  | Nil -> 0
  | Element(x, suite) -> x + (somme suite)

La « syntaxe » des équations récursives n’est pas la même que celle d’OCaml : on essaie de se rapprocher de l’écriture dont on a l’habitude en mathématiques.

Voici un autre exemple avec la somme des entiers entre 0 et n, pour montrer comment éviter les ambiguïtés entre deux cas :

(* …
 *
 * ÉQUATIONS RECURSIVES :
 *   somme(0) = 0
 *   somme(x) = x + somme(x - 1), pour x ≠ 0
 * …
 *)
let rec somme (x : int) : int =
  match x with
  | 0 -> 0
  | autre -> autre + (somme (autre - 1))