IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

L'approche qualité avec OCaml

Image non disponible


précédentsommairesuivant

III. Partie II

5. Expressions immédiates
6. Fonctions et application
7. Déclarations de variables
8. Déclarations et récursion
9. Déclarations de fonctions
10. Récursion et accumulation
11. Déclarations locales
12. Fonctions et assertions
13. Les types énumérés
14. Le filtrage
15. Les listes
16. Les fonctions sur les listes
17. Le polymorphisme paramétrique
18. Les couples
19. Les huit reines
20. Le tri fusion
21. Les modules
22. Le traitement des erreurs
23. Les enregistrements
24. Les performances

III-A. La programmation fonctionnelle

 

III-A-1. 5. Expressions immédiates

La programmation en OCaml consiste essentiellement à évaluer des expressions. Lorsqu'une expression valide suivie de ;; est entrée :

  1. OCaml synthétise son type ;
  2. Si la synthèse de type échoue, un message d'erreur de type est affiché ;
  3. Sinon OCaml affiche son type et sa valeur.

Exemple :

 
Sélectionnez
# 1 + 3;;
- : int = 4

Ce qui signifie : l'expression était entière et valait 4.
Pour tirer le meilleur parti de OCaml il est de la plus grande importance de saisir la stratégie utilisée pour la synthèse de type.
Le type des opérandes doit toujours être conforme au type attendu par les opérateurs.
Ainsi l'expression suivante est mal typée et ne peut pas être évaluée :

 
Sélectionnez
# 1.0 + 3;;
Characters 0-3:
  1.0 + 3;;
  ^^^
This expression has type float but is here used with type int

Parce que + est l'addition entière et attend deux arguments entiers.
Dans ce cas il faut utiliser +. qui est l'addition réelle :

 
Sélectionnez
# 1. +. 3.;;
- : float = 4.

Remarque : contrairement à d'autres langages OCaml infère le type des opérandes à l'aide du type des opérateurs.

OCaml distingue les minuscules et les majuscules.
Les commentaires sont ouverts par (* et sont fermés par *) et ils peuvent être imbriqués.

Les types atomiques sont les nombres entiers (int), les booléens (bool), les chaînes de caractères (string), les caractères (char) et les nombres réels à virgule flottante (float). Voici la liste de ces types accompagnés de leurs opérateurs les plus courants. Ce tableau est suivi de quelques exemples d'utilisation :

Type:   int
Instances:   1 4 -3 0
Préfixés:   min max abs - lnot float_of_int char_of_int string_of_int
Infixés:   + - * / mod land lor lsl lsr = <> < <= >= >
Type:   bool
Instances:   true false
Préfixés:   not
Infixés:   && || or = <>
Type:   string
Instances:   "Hello World!"
Préfixés:   String.length print_string int_of_string
Infixés:   ^ .[i] = <> < <= >= >
Type:   char
Instances:   'a'
Préfixés:   int_of_char
Infixés:   = <> < <= >= >
Type:   float
Instances:   3.14 .5 0.
Préfixés:   min max sqrt sin cos tan log exp asin acos atan int_of_float
Infixés:   +. -. *. /. ** = <> < <= >= >
Exemples:   Résultat:
# 1+1;;   - : int = 2
# 2*3 = 13 mod 7;;   - : bool = true
# abs (-3);;   - : int = 3
# min 34 (-67);;   - : int = -67
# not (1<2);;   - : bool = false
# not (true && false);;   - : bool = true
# "good" ^ "bye!";;   - : string = "goodbye!"
# "beef".[3];;   - : char = 'f'
# 2.0**3.0;;   - : float = 8.
# sqrt 4.;;   - : float = 2.
# exp 1.;;   - : float = 2.7182818284590451
# cos 0.;;   - : float = 1.
(* La priorité des opérateurs est respectée *)
# 2+3*4;;
- : int = 14
(* Les parenthèses permettent d'utiliser un opérateur infixe en position préfixe *)
# (+) 2 2;;
- : int = 4
(* L'opérateur * en position préfixe doit être entouré d'espaces *)
(* sinon il est interprété comme l'ouverture d'un commentaire *)
# ( * ) 2 2;;
- : int = 4



Le typage remonte l'arbre d'expression, il s'applique d'abord aux constantes, puis à l'échelle des expressions élémentaires et enfin il se propage à travers toutes les expressions englobantes.

Règles de typage (expressions simples)

La règle de typage d'une expression abs n, -n, lnot n, float_of_int n, char_of_int n, ou string_of_int n distingue deux cas possibles :

  • ou bien le type de n est int alors l'expression est de type (respectivement) int, int, int, float, char, string ;
  • ou bien l'expression est mal typée.


La règle de typage d'une expression a + b, a - b, a * b, a mod b, a land b, a lor b, a lsl b, a lsr b, distingue deux cas possibles :

  • ou bien le type de a est int et le type de b est int alors l'expression est de type int ;
  • ou bien l'expression est mal typée.


La règle de typage d'une expression a = b, a <> b, a < b et a <= b, a >= b, ou a > b distingue deux cas possibles :

  • ou bien a et b ont un même type E alors l'expression est de type bool ;
  • ou bien l'expression est mal typée.


La règle de typage d'une expression min a b ou max a b distingue deux cas possibles :

  • ou bien a et b ont un même type E alors l'expression est de type ;
  • ou bien l'expression est mal typée.


La règle de typage d'une expression a && b, a || b ou a or b distingue deux cas possibles :

  • ou bien le type de a est bool et le type de b est bool alors l'expression est de type bool ;
  • ou bien l'expression est mal typée.


La règle de typage d'une expression sqrt x, sin x, cos x, tan x, log x, exp x, asin x, acos x ou atan x distingue deux cas possibles :

  • ou bien le type de x est float alors l'expression est de type float ;
  • ou bien l'expression est mal typée.
 

III-A-2. 6. Fonctions et application

En OCaml les notions d'expression et de valeur ont un sens assez large puisqu'elles englobent aussi les fonctions qui sont des valeurs à part entière.
Une fonction d'une seule variable se déclare à l'aide du mot-clé fun et d'une flèche, par exemple la fonction de n qui renvoie n + 1 se déclare comme ceci :

 
Sélectionnez
# fun n -> n + 1;;
- : int -> int = <fun>

L'interpréteur a bien évalué cette expression comme une valeur et lui a attribué le type intint, c'est-à-dire une fonction d'un entier qui renvoie un entier.
Cette attribution de type se fait grâce au soutien des règles de typage, au niveau le plus élevé, celui de la fonction, le système de typage a réuni suffisamment d'informations de typage pour en déduire le type complet d'une fonction.
La flèche est un constructeur infixe pour les types fonctionnels, elle prend à gauche le type d'un argument et à droite le type d'une application à un argument. Un type fonctionnel est le correspondant OCaml d'une signature dans d'autres langages. Dans ces autres langages, une signature n'est pas un type parce que les fonctions ne sont pas des valeurs à part entière.

L'application de fonction est une opération tellement élémentaire qu'en OCaml on l'écrit par simple juxtaposition, par exemple le successeur de l'entier 2 est l'entier 3 :

 
Sélectionnez
# (fun n -> n + 1) 2;;
- : int = 3

Une fonction à plusieurs arguments se construit en imbriquant plusieurs fonctions à un seul argument, par exemple cette fonction réalise l'addition de deux entiers a et b :

 
Sélectionnez
fun a -> (fun b -> a + b);;

Son type est int(intint), car la flèche est associative à droite, si on lui passe en entier comme argument alors le résultat est de type intint. Comme la flèche est associative à droite et que l'application de fonction est associative à gauche, pour additionner 1 et 2 on pourra écrire :

 
Sélectionnez
(fun a -> fun b -> a + b) 1 2;;

La valeur 1 est alors associée à la variable a puis la valeur 2 est associée à la variable b dans l'expression 1 + b ce qui donne 1 + 2 = 3.
On peut écrire en plus condensé :

 
Sélectionnez
(fun a b -> a + b) 1 2;;

Cette abréviation n'altère en rien le mécanisme d'évaluation qui reste exactement le même, en particulier, comme le suggérait le type int(intint), on peut construire une fonction d'incrémentation en appliquant 1 à cette fonction :

 
Sélectionnez
(fun a b -> a + b) 1;;

Règle de typage (construction d'une fonction)
La règle de typage d'une expression fun vexpr distingue trois cas possibles :

  • ou bien toutes les occurrences de v dans expr ont un même type T alors l'expression est de type TEE est le type de expr ;
  • ou bien il n'y a aucune occurrence de v dans expr alors l'expression est de type 'aEE est le type de expr, ce cas ne nous intéresse pas pour l'instant, nous définirons le type 'a au moment opportun ;
  • ou bien l'expression est mal typée.


Règle de typage (application d'une fonction)
La règle de typage d'une application f x distingue deux cas possibles :

  • ou bien l'expression f est du type XY et l'expression x est du type X alors l'application f x est de type Y ;
  • ou bien l'application f x est mal typée.
 

III-A-3. 7. Déclarations de variables

Les déclarations se font avec le mot-clé let qui associe une valeur à une variable, la constante pi pourra être définie ainsi :

 
Sélectionnez
let pi = 3.1415926535898;;

L'addition entière pourra être définie ainsi :

 
Sélectionnez
let add = fun a b -> a + b;;

Attention: les variables ainsi définies sont liées, elles ne sont pas modifiables, leur valeur est fixée, elles ne peuvent pas en changer en utilisant une assignation.

Les mots-clés if ... then ... else ... permettent un choix parmi deux expressions possibles selon la valeur d'une condition :

 
Sélectionnez
let abs = fun z -> if z >= 0 then z else -z;;
let minimum = fun a b -> if a < b then a else b;;
let maximum = fun a b -> if a > b then a else b;;

Le if ... then ... else ... n'est pas une instruction, mais une expression qui peut prendre deux valeurs possibles.

Règle de typage (expression conditionnelle)
La règle de typage d'une expression if cond then expr1 else expr2 distingue deux cas possibles :

  • ou bien le type de cond est bool et les expressions expr1 et expr2 ont un même type E alors l'expression est de type E ;
  • ou bien l'expression est mal typée.
 

III-A-4. 8. Déclarations et récursion

Le mot-clé rec permet la récursion c'est-à-dire l'utilisation d'une variable pendant sa définition avec let :

 
Sélectionnez
let rec factorial =
  fun n -> if n=0 then 1 else n * factorial (n - 1);;

let rec power =
  fun x n -> if n=0 then 1. else x *. power x (n-1);;

let rec binomial =
  fun n p -> if (p=0) or (p=n) then 1 else binomial (n-1) p + binomial (n-1) (p-1);; 

let rec fibonacci =
  fun n -> if n < 2 then 1 else fibonacci (n - 1) + fibonacci (n - 2);;

Remarque : pour donner une idée intuitive de cette dernière fonction fibonacci disons que si une grenouille pouvait sauter au maximum deux marches d'escalier alors fibonacci n serait le nombre de façons différentes pour cette grenouille de monter n marches à l'aide d'une combinaison de sauts d'une ou deux marches.

 

III-A-5. 9. Déclarations de fonctions

Les déclarations suivantes sont, par abus de langage, appelées déclarations de fonctions :

 
Sélectionnez
let rec factorial n = if n=0 then 1 else n * factorial (n - 1);;

let rec power x n = if n=0 then 1. else x *. power x (n-1);;

let rec binomial n p = if (p=0) or (p=n)  then 1 else binomial (n-1) p + binomial (n-1) (p-1);; 

let rec fibonacci n = if n < 2 then 1 else fibonacci (n - 1) + fibonacci (n - 2);;

Leur sémantique est exactement la même que leurs précédentes versions respectives. factorial, power, binomial et fibonacci restent des variables liées à des valeurs fonctionnelles.

 

III-A-6. 10. Récursion et accumulation

Il existe une façon alternative de définir des fonctions récursives: au lieu d'effectuer le calcul au plus tard on l'effectue au plus tôt, pour cela on rajoute un paramètre dit accumulateur qui enregistre le résultat partiel du calcul en cours :

 
Sélectionnez
let rec factorial acc n =
  if n=0 then acc else factorial (acc*n) (n-1);;

L'expression factorial 1 correspond alors à la fonction factorielle recherchée.

Remarque : on a bien renversé le sens du calcul, cette version itérative calcule n×(n-1)×(n-2)×…×2 alors que la version sans accumulation calculait 2×…×(n-2)×(n-1)×n, bien sûr cela n'a pas d'intérêt ici, car la multiplication est commutative, mais cet effet peut se révéler très utile lors d'applications plus concrètes comme nous aurons l'occasion d'en donner des exemples.

Nombre de fonctions récursives (mais pas toutes) peuvent être réécrites à l'aide d'un ou plusieurs accumulateurs :

 
Sélectionnez
let rec power acc x n =
  if n=0 then acc else power (acc *. x) x (n-1);;

L'expression power 1.0 correspond à la fonction puissance ordinaire.

Pour la fonction fibonacci on aura besoin de deux accumulateurs :

 
Sélectionnez
let rec fibonacci acc0 acc1 n =
  if n < 2 then acc1 else fibonacci acc1 (acc0 + acc1) (n - 1);;

L'expression fibonacci 1 1 correspond à la fonction fibonacci ordinaire.

Cette définition de fibonacci est beaucoup plus efficace que la version purement récursive. Pour faire une comparaison imaginons qu'avec la version à accumulateurs la grenouille se contente de monter n marches, alors avec la version récursive la grenouille expérimenterait toutes les façons possibles de monter ces mêmes n marches.

Dans toutes ces implémentations avec accumulateurs la branche then renvoie le résultat final tandis que la branche else n'est qu'une réapplication de la fonction avec de nouveaux arguments. Comme nous le verrons plus loin, cette propriété dite de récursion terminale a aussi son importance dans les performances des calculs intensifs.

 

III-A-7. 11. Déclarations locales

Avouons-le, les versions à accumulateur(s) sont moins confortables à l'usage, à cause de leur(s) paramètre(s) supplémentaire(s). Il serait plus commode de camoufler les paramètres-accumulateurs pour n'utiliser que les paramètres significatifs. Pour ce faire nous allons utiliser les mots-clés let v = expr1 in expr2. Cette construction autorise une définition locale, elle agit comme un let ordinaire sauf que la variable v n'est définie que dans expr2 où elle vaudra expr1. La variable v n'est pas définie dans le reste du programme. La construction let rec v = expr1 in expr2 autorise la récursion sur une définition locale de la variable v, c'est-à-dire que v pourra être présente dans expr1.

Nous avons besoin d'un let pour créer le raccourci ainsi que d'un let rec qui définira la fonction récursive locale à accumulateur :

 
Sélectionnez
let factorial n =
  let rec loop acc n =
    if n=0 then acc else loop (acc*n) (n-1)
  in loop 1 n;;

let power x n =
  let rec loop acc n =
    if n=0 then acc else loop (acc *. x) (n-1)
  in loop 1. n;;

let fibonacci n =
  let rec loop acc0 acc1 n =
    if n < 2 then acc1 else loop acc1 (acc0 + acc1) (n - 1)
  in loop 1 1 n;;

Il existe une autre raison, plus profonde que celle de la commodité, pour laquelle un changement dans l'expression d'une fonction ne doit pas modifier la façon d'appliquer cette fonction. Cette raison tient à l'approche de componentisation systématique sur laquelle nous nous sommes engagés dès notre présentation de la notion de complexité dans les programmes. Les fonctions sont un de nos meilleurs atouts, si nous voulons préserver leur composabilité il nous faut préserver leur type, car il est le connecteur qui permet de les emboîter les unes avec les autres.

Règle de typage (déclaration locale)
La règle de typage d'une expression let v = expr1 in expr2 distingue trois cas possibles :

  • ou bien toutes les occurrences de v dans expr2 ont le même type que expr1 alors l'expression est de type EE est le type de expr2 ;
  • ou bien il n'y a aucune occurrence de v dans expr2 alors l'expression est de type EE est le type de expr2 ;
  • ou bien l'expression est mal typée.
 

III-A-8. 12. Fonctions et assertions

Parfois le domaine de définition d'une fonction est strictement inclus dans son type, on utilisera alors le mot-clé assert pour garantir qu'une fonction n'est toujours appliquée que sur son domaine de définition :

 
Sélectionnez
let factorial n =
  assert(n >= 0);
  let rec loop acc n =
    if n=0 then acc else loop (acc*n) (n-1)
  in loop 1 n;;

let power x n =
  assert(n >= 0);
  let rec loop acc n =
    if n=0 then acc else loop (acc *. x) (n-1)
  in loop 1. n;;

let fibonacci n =
  assert(n >= 0);
  let rec loop acc0 acc1 n =
    if n < 2 then acc1 else loop acc1 (acc0 + acc1) (n - 1)
  in loop 1 1 n;;

let rec binomial n p =
  assert(0 <= p && p <= n);
  if (p=0) or (p=n)  then 1 else binomial (n-1) p + binomial (n-1) (p-1);;

Lorsqu'une fonction sera appliquée hors de son domaine de définition OCaml terminera l'évaluation par une erreur Assert_failure telle que celle-ci :

 
Sélectionnez
# factorial (-1);;
Exception: Assert_failure ("", 5, 1).

Une assertion ne fait que rendre explicite une indication d'usage que vous auriez autrement mis en commentaire. Expliciter ce qui autrefois était implicite c'est un thème récurrent du principe quality first.

Le respect du principe quality first prescrit d'écrire systématiquement les assertions nécessaires, avant même d'écrire le corps de la fonction. Vous vous remercierez de l'avoir fait pendant le débogage. Anticiper les phases suivantes du développement c'est un autre thème récurrent du principe quality first. N'effacez jamais une assertion, au pire mettez-la en commentaire.

Les assertions font également partie de notre panoplie d'outils pour préserver la composabilité, elles doivent être considérées comme des types, la contrainte qu'elles ajoutent a du sens, elle capture une restriction de domaine que notre système de type n'est pas assez expressif pour imposer à la compilation.

Règle de typage (assertion)
La règle de typage d'une expression assert (cond) distingue deux cas possibles :

  • ou bien le type de cond est bool alors l'expression est de type unit (nous reparlerons de ce type unit au moment opportun) ;
  • ou bien l'expression est mal typée.
 

III-A-9. 13. Les types énumérés

Un type énuméré est un ensemble fini qui correspond au domaine de définition d'une fonction. De cette façon on n'aura pas à ajouter d'assertion, car le type de la fonction correspondra à son domaine de définition.

 
Sélectionnez
type day =
  | Monday
  | Tuesday
  | Wednesday
  | Thursday
  | Friday
  | Saturday
  | Sunday
  ;;

Chaque jour ainsi énuméré devient un constructeur pour le type day.

Remarque : le nom de chacun des membres (on dit variants) d'un type énuméré doit commencer par une majuscule, c'est une obligation, au contraire les autres identificateurs habituels (les types, fonctions, variables et étiquettes) doivent toujours commencer par une lettre minuscule.

Règle de typage (constructeur)
La règle de typage d'une expression Constructeur ne contient qu'un seul cas possible :

  • le type d'un Constructeur est le type dans lequel il a été énuméré.
 

III-A-10. 14. Le filtrage

Le filtrage (pattern matching) permet une analyse de cas en distinguant chacun des motifs (patterns) possibles. Une variable dans un motif prend la valeur à laquelle elle est associée dans ce motif. Les motifs sont ordonnés, ils sont confrontés tour à tour à la valeur filtrée, le premier motif filtrant cette valeur sélectionne la valeur renvoyée par l'expression filtrante. Les variables dans les motifs filtrent toutes les valeurs.

 
Sélectionnez
let rec fibonacci n =
  assert(n >= 0);
  match n with
  | 0 -> 1
  | 1 -> 1
  | m -> fibonacci(m-1) + fibonacci(m-2) ;;

La sémantique de l'expression filtrante match dans cette fonction est la suivante :

  • si n = 0 alors l'expression vaut 1 ;
  • si n = 1 alors l'expression vaut 1 ;
  • sinon l'expression vaut let m = n in fibonacci(m-1) + fibonacci(m-2).

Comme les motifs sont ordonnés, un motif plus général pourra cacher un motif plus spécifique, dans ce cas OCaml délivrera un avertissement à prendre au sérieux comme dans l'exemple suivant où les motifs ont été clairement mal ordonnés :

 
Sélectionnez
# let rec factorial n =
    match n with
    | m -> m * factorial (m - 1)
    | 0 -> 1;;
Characters 80-81:
Warning U: this match case is unused.
    | 0 -> 1;;
      ^
val factorial : int -> int = <fun>

De même qu'il existe des fonctions anonymes (introduites par le mot-clé fun) il existe également des variables anonymes.
La variable anonyme est notée "_", la fonction fibonacci peut donc être réécrite :

 
Sélectionnez
let rec fibonacci n =
  assert(n >= 0);
  match n with
  | 0 -> 1
  | 1 -> 1
  | _ -> fibonacci(n-1) + fibonacci(n-2) ;;

Le filtrage multiple, c'est-à-dire sur plusieurs valeurs, est possible à l'aide de la virgule :

 
Sélectionnez
let rec binomial n p =
  assert(0 <= p && p <= n);
  match n,p with 
  | _,0 -> 1
  | _,_ -> if p = n then 1
           else binomial (n-1) p + binomial (n-1) (p-1) ;;

Les schémas autorisés dans un motif sont :

  • une nouvelle variable ;
  • un constructeur ;
  • une constante immédiate ;
  • une nouvelle variable anonyme.

Les expressions et les applications de fonction ne sont pas autorisées dans les motifs, car un motif n'est pas une valeur, mais un schéma de valeurs.
OCaml vérifie l'exhaustivité du filtrage, si un cas n'est pas filtré, un message d'alerte vous invite à intégrer ce cas ou à reconsidérer votre fonction :

 
Sélectionnez
# let string_of_day d =
    match d with
    | 1 -> "Monday"
    | 2 -> "Tuesday"
    | 3 -> "Wednesday"
    | 4 -> "Thursday"
    | 5 -> "Friday"
    | 6 -> "Saturday"
    | 7 -> "Sunday";;
Characters 23-97:
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
0

Vous devez prendre cet avertissement au sérieux, dans le cas ci-dessus le filtrage a peu de sens, car il y a une infinité d'entiers pour seulement 7 jours de la semaine. L'avertissement concernant la fonction string_of_day est le symptôme d'une erreur sur le domaine de définition, dans notre cas un type énuméré ferait davantage l'affaire :

 
Sélectionnez
let string_of_day d =
  match d with
  | Monday    -> "Monday"
  | Tuesday   -> "Tuesday"
  | Wednesday -> "Wednesday"
  | Thursday  -> "Thursday"
  | Friday    -> "Friday"
  | Saturday  -> "Saturday"
  | Sunday    -> "Sunday";;

let tomorrow d =
  match d with
  | Monday    -> Tuesday
  | Tuesday   -> Wednesday
  | Wednesday -> Thursday
  | Thursday  -> Friday
  | Friday    -> Saturday
  | Saturday  -> Sunday
  | Sunday    -> Monday;;

Remarque : OCaml rejette la définition multiple d'une même variable dans un même motif.

 
Sélectionnez
# let equal a b =
    match a,b with
    | x,x -> true
    | _,_ -> false;;
Characters 43-44:
      | x,x -> true
          ^
This variable is bound several times in this matching

Ici, ce qui semble une contrainte d'égalité est en fait une tentative de définir la même variable avec deux valeurs différentes.

Règle de typage (filtrage)
La règle de typage d'une expression match expr with pat1expr1 | ... | patnexprn comportant n clauses distingue deux cas possibles :

  • ou bien l'expression expr et les motifs pat1 à patn ont un même type T et les expressions expr1 à exprn ont un même type E alors l'expression est de type E ;
  • ou bien l'expression est mal typée.
 

III-A-11. 15. Les listes

La liste chaînée est le type conteneur-extensible le plus simple, tellement simple qu'en OCaml ce type est prédéfini.
Il existe deux façons distinctes d'exprimer une liste chaînée :

  • ou bien par énumération, en encadrant les éléments par des crochets, séparés par des points-virgules
 
Sélectionnez
# [2;3;4];;
- : int list = [2; 3; 4]
  • ou bien par construction à l'aide du constructeur infixe ::
 
Sélectionnez
# 2::3::4::[];;
- : int list = [2; 3; 4]

Dans le cas du constructeur :: l'argument à gauche est appelé la tête (head) et celui à droite la queue (tail) de la liste.

Les fonctions qui manipulent des listes utilisent abondamment le filtrage :

  • une liste énumérée pour le (ou les) cas de base ;
  • une liste construite pour les cas composites.

Par exemple, la fonction sum_list renvoie la somme des entiers d'une liste, la somme des éléments d'une liste vide étant nulle, et la somme des éléments d'une liste non vide étant égale à sa tête n plus la somme des éléments de sa queue t :

 
Sélectionnez
let rec sum_list l =
  match l with
  | []   -> 0
  | n::t -> n + sum_list t;;

Il s'agit là d'un raisonnement par cas qui sera souvent utilisé sur les listes :

  • ou bien on est dans le cas trivial de la liste vide ;
  • ou bien on déconstruit la liste et on applique le traitement adéquat sur sa tête et sa queue, souvent on aura une récursion sur la queue.

Plus rarement il faudra traiter le cas de la liste à un seul élément ou alors il faudra considérer les deux premiers éléments en tête de la liste. Nous aurons bien assez tôt l'occasion de voir maints exemples de fonctions sur les listes.

Règle de typage (liste)
La règle de typage d'une expression a::l ne contient qu'un seul cas possible :

  • le type de l'expression a est T ;
  • le type de l'expression l est T list ;
  • le type de l'expression a::l est T list.
 

III-A-12. 16. Les fonctions sur les listes

On se propose de donner deux exemples plus avancés avec les listes.
En premier on déterminera toutes les vues sur un tétromino.
En second on donnera la liste des coordonnées des sommets du cube unité en dimensions quelconques.

Un polyomino est une congruence orthogonale de n carrés. Le jeu Tetris tire son nom du fait qu'il se joue avec les congruences orthogonales de quatre carrés, ce sont les cinq tétrominos surnommées I.L.O.S.T. :

Image non disponible

On représentera un polyomino par une edge list, une liste des arêtes (edge) orientées dans le sens des aiguilles d'une montre où le type edge est défini par :

 
Sélectionnez
type edge =
  | Up
  | Down
  | Left
  | Right
  ;;

Voici les cinq tétrominos I.L.O.S.T. :

 
Sélectionnez
let tetro_I = [Up;Up;Up;Up;Right;Down;Down;Down;Down;Left];;
let tetro_L = [Up;Up;Up;Right;Down;Down;Right;Down;Left;Left];;
let tetro_O = [Up;Up;Right;Right;Down;Down;Left;Left];;
let tetro_S = [Up;Right;Up;Right;Right;Down;Left;Down;Left;Left];;
let tetro_T = [Up;Left;Up;Right;Right;Right;Down;Left;Down;Left];;

On voudra pouvoir les tourner (d'un quart de tour dans le sens horaire) :

 
Sélectionnez
let rec rotate p =
  match p with
  | [] -> []
  | Up::t    -> Right::rotate t 
  | Down::t  -> Left::rotate t
  | Left::t  -> Up::rotate t
  | Right::t -> Down::rotate t
  ;;

On voudra également pouvoir les miroiter (échange gauche-droite) :

 
Sélectionnez
let mirror p =
  let rec loop l a =
    match l with
    | [] -> a
    | Up::t    -> loop t (Down::a) 
    | Down::t  -> loop t (Up::a)
    | Left::t  -> loop t (Left::a)
    | Right::t -> loop t (Right::a)
  in loop p [];;

Remarque : pour pouvoir retourner la liste, on a utilisé une fonction avec un accumulateur.

Nous allons maintenant représenter le vecteur unité par la liste [[0];[1]] :

 
Sélectionnez
# [[0];[1]];;
- : int list list = [[0]; [1]]

Et le carré unité par la liste [[0;0]; [0;1]; [1;0]; [1;1]]. Pour généraliser en dimension n on s'aidera de la fonction mult_unit qui multiplie un cube c par l'intervalle unité lui ajoutant ainsi une dimension. Il suffit alors d'utiliser cette multiplication pour calculer la puissance nième de l'intervalle unité, c'est ce que fait la fonction power_cube :

 
Sélectionnez
let rec power_cube n =
  assert(n >= 1);
  let rec mult_unit c =
    match c with
    | []   -> []
    | h::t -> (0::h)::(1::h)::mult_unit t 
  in
    if n=1 then [[0];[1]]
    else mult_unit (power_cube (n-1));;
 

III-A-13. 17. Le polymorphisme paramétrique

Nous avons déjà vu qu'une fonction n'a qu'un seul paramètre (sur son domaine) et ne renvoie qu'un résultat (sur son codomaine).

Une conséquence logique c'est qu'une fonction n'a que deux schémas de type possibles :

  • ou bien le codomaine est identique au domaine, alors le type de la fonction est de la forme α → α ;
  • ou bien le codomaine est différent du domaine, alors le type de la fonction est de la forme α → β.

Ou encore, dit autrement, l'opérateur infixe → est le constructeur du type fonctionnel α → β.

En OCaml les schémas de type α et β sont notés respectivement 'a et 'b :

 
Sélectionnez
# let f x = x in f;;
- : 'a -> 'a = <fun>

# let rec f x = f x in f;;
- : 'a -> 'b = <fun>

Un type paramétré par un schéma de type est dit polymorphe.
Un tel type n'est pas une curiosité, nous en avons déjà rencontré un :

 
Sélectionnez
# [];;
- : 'a list = []

Le type list est un type paramétré, une liste est homogène, tous ses éléments sont forcément du même type 'a. Ce paramètre de type 'a peut être fixé librement d'où le nom de polymorphisme paramétrique donné à cette capacité.
Ce polymorphisme paramétrique s'applique aussi bien aux types qu'aux fonctions :

  • un type polymorphe permet de séparer le type conteneur du type contenu ;
  • une fonction polymorphe permet de séparer le type de la fonction du type de ses paramètres.

Les deux vont de pair, la définition d'un type polymorphe s'accompagnant des fonctions polymorphes qui agissent sur lui.
Par exemple des fonctions sur une liste qui ne présagent pas du contenu de cette liste si bien qu'elles sont utiles sur tous les types de liste possibles.
C'est en premier lieu le cas du constructeur de liste, qui n'est pas une fonction puisqu'on ne peut pas l'appliquer, mais dont on va faire apparaître le type de la façon suivante :

 
Sélectionnez
# fun h t -> h::t;;
- : 'a -> 'a list -> 'a list = <fun>

La distinction entre fonctions et constructeurs se justifie par le filtrage, une fonction est faite pour être appliquée, un constructeur est fait pour être instancié et filtré.

L'opérateur infixe @ de concaténation de liste est également polymorphe :

 
Sélectionnez
# (@);;
- : 'a list -> 'a list -> 'a list = <fun>

Il est temps maintenant de profiter de la pleine fonctionnalité. Nous allons encore écrire des fonctions jouets, mais dorénavant elles seront plus amusantes que jamais. Alors, écrivons sept fonctions polymorphes sur les listes.

La première s'appelle filter, elle est du type :

 
Sélectionnez
('a -> bool) -> 'a list -> 'a list

Son 1er paramètre est donc un prédicat (une fonction qui teste un élément), son 2d paramètre est une liste et la fonction renvoie également une liste. Et le type nous dit encore plus que ceci. En effet regardons les paramètres de type, ils nous disent que le prédicat peut tester tous les éléments de la liste donnée en paramètre et que si on appliquait un prédicat à filter, c'est-à-dire si on ne lui fournissait que le premier de ses deux arguments (on appelle ceci une application partielle), alors filter serait un endomorphisme (une fonction de type α → α), elle aurait donc le même type que la fonction identité.

À partir de là, sur la seule foi de son type, il est possible d'imaginer ce que fait la fonction filter: elle renvoie la liste donnée en paramètre à laquelle ont été ôtés tous les éléments qui ne satisfont pas le prédicat ('a -> bool). Nous allons maintenant l'implémenter, voilà comment nous procédons, en raisonnant par cas :

  • ou bien la liste est vide alors, la liste filtrée est vide ;
  • ou bien la liste a une tête et une queue alors on filtre la queue et on lui ajoute la tête (si elle satisfait le prédicat) ou pas (si elle ne satisfait pas le prédicat).

Ce qui nous donne :

 
Sélectionnez
let rec filter p l =
  match l with
  | [] -> []
  | h::t -> if p h then h::filter p t
            else filter p t;;

Passons à notre deuxième fonction polymorphe, elle s'appelle exists et elle est du type :

 
Sélectionnez
('a -> bool) -> 'a list -> bool

Raisonnons sur son type, c'est encore une fonction qui attend un prédicat et une liste, encore une fois le prédicat teste les éléments de cette liste (puisque lui et la liste ont le même paramètre de type 'a), mais cette fois-ci la fonction exists est elle-même un prédicat (sur la liste donnée en argument), car si on lui applique un prédicat (par application partielle) alors son type devient αbool. La fonction exists fait l'union logique de l'application du prédicat sur tous les éléments de la liste. Avec ce type elle pourrait faire la conjonction au lieu de l'union (c'est d'ailleurs le rôle de la fonction for_all que nous verrons plus tard). Le type d'une fonction ne la caractérise donc pas totalement, mais il en dit déjà assez long et il serait désavantageux d'ignorer cette information.

Raisonnons par cas :

  • ou bien la liste est vide, elle ne contient alors aucun élément qui puisse satisfaire le prédicat, nous renvoyons false ;
  • ou bien la liste a une tête et une queue alors nous renvoyons la conjonction logique du prédicat sur la tête et de la récursion sur la queue.

Ce qui nous donne :

 
Sélectionnez
let rec exists p l =
  match l with
  | [] -> false
  | h::t -> p h or exists p t;;

Passons à notre troisième fonction polymorphe, elle s'appelle mapi et elle est du type :

 
Sélectionnez
(int -> 'a -> 'b) -> int -> 'a list -> 'b list

Imaginons que les int soient absents de ce type alors le type serait celui-ci :

 
Sélectionnez
('a -> 'b) -> 'a list -> 'b list

Et c'est tout de suite plus clair : nous comprenons que la fonction mapi applique la fonction ('a → 'b) sur tous les éléments de la liste ('a list) et renvoie une liste ('b list). Alors que viennent faire les int ? C'est simple, il s'agit d'un argument supplémentaire qui croit avec la position de l'élément 'a dans la liste ('a list).

Raisonnons à nouveau par cas :

  • ou bien la liste est vide alors la fonction f ne s'applique jamais et la liste renvoyée est vide ;
  • ou bien la liste a une tête et une queue alors nous appliquons (f n h) et nous ajoutons le résultat à la transformation de la queue.

Ce qui nous donne :

 
Sélectionnez
let rec mapi f n l =
  match l with
  | [] -> []
  | h::t -> f n h::mapi f (n+1) t;;

Notre quatrième fonction polymorphe utilisera notre fonction exists, elle s'appelle exists_commutative et elle est du type :

 
Sélectionnez
('a -> 'a -> bool) -> 'a list -> bool

Ce type ressemble beaucoup à celui de exists sauf que cette fois-ci le prédicat teste deux éléments de la liste donnée en paramètre. Cette fonction nous dira donc s'il existe deux éléments dans la liste qui vérifient le prédicat donné en paramètre.

Raisonnons par cas :

  • ou bien la liste est vide alors elle ne contient pas deux éléments, nous renvoyons false ;
  • ou bien la liste a une tête et une queue alors, nous renvoyons la conjonction logique du prédicat (duquel on a fixé le premier argument avec la tête) appliqué à toute la queue et de la récursion sur la queue.
 
Sélectionnez
let rec exists_commutative p l =
  match l with
  | []   -> false
  | h::t -> (exists (p h) t) or exists_commutative p t;;

Notez que le prédicat doit être commutatif, car pour chaque couple d'éléments il n'est appliqué qu'une seule fois, si le prédicat était non commutatif il faudrait également l'appliquer en échangeant l'ordre des deux éléments.

Notre cinquième fonction polymorphe est la fonction flatten qui renvoie la concaténation de plusieurs listes :

 
Sélectionnez
let rec flatten l =
  match l with
  | [] -> []
  | h::t -> h @ flatten t;;

Notre sixième fonction polymorphe est distribute qui utilise mapi pour renvoyer la liste des listes l dans lesquelles on a inséré l'élément a à toutes les positions possibles :

 
Sélectionnez
let rec distribute a l =
  match l with
  | []   -> [[a]]
  | h::t -> (a::l) :: (mapi (fun _ x -> h::x) 0 (distribute a t));;

Enfin notre septième et dernière fonction est permute qui utilise flatten, mapi et distribute pour renvoyer la liste de toutes les permutations d'une liste :

 
Sélectionnez
let rec permute l =
  match l with
  | []   -> [[]]
  | h::t -> flatten (mapi (fun _ -> distribute h) 0 (permute t));;

Remarque :

Les opérations sur les listes sont toujours non destructives, la structure des listes passées en arguments est toujours préservée, les listes retournées en résultat sont fraîchement allouées dans le tas (la mémoire globale) par le constructeur (::), on dit que la liste est une structure de données immutable. D'une façon générale, la programmation fonctionnelle privilégie plutôt les données immutables alors que la programmation impérative privilégie plutôt les données mutables. Les données immutables bénéficient de la propriété dite de substituabilité qui facilite un raisonnement équationnel assez similaire à celui utilisé en mathématiques. Cette facilité de raisonner sur le texte du programme et non sur son exécution améliore grandement la lisibilité du code. De plus la rigueur du raisonnement par récurrence fait que le texte du programme se rapproche naturellement du raisonnement qui établit la preuve de sa correction.

Remarques :

  • toute fonction de type 'a'a est isomorphe à la fonction identité ;
  • toute fonction de type 'a'b'a est un opérateur associatif à gauche ;
  • toute fonction de type 'a'b'b est un opérateur associatif à droite.


Règle de typage (liaison d'une variable de type)
Cette règle explicite l'instanciation d'un type polymorphe par un autre type monomorphe ou polymorphe :

  • la variable 'a peut être liée à n'importe quel type T ne contenant pas d'occurrence libre de la variable 'a ;
  • la variable 'a peut être liée à deux types T1 et T2 si et seulement si le type T1 est identique au type T2.
 

III-A-14. 18. Les couples

Les couples (pairs) correspondent à un produit cartésien de valeurs, ils combinent deux valeurs de types quelconques sous la forme d'une seule paire. Les deux éléments d'un couple sont séparés par une virgule. Les couples sont utilisés notamment :

  • comme des containers ;
  • pour retourner deux valeurs ;
  • pour le filtrage multiple (nous avons déjà parlé du filtrage sur plusieurs valeurs).

La virgule est un constructeur, mais nous allons exposer son type comme nous l'avons fait pour le constructeur de listes :

 
Sélectionnez
# fun a b -> (a,b);;
- : 'a -> 'b -> 'a * 'b = <fun>

Deux remarques :

  • le résultat contient deux paramètres de type 'a et 'b, un couple peut donc être hétérogène ;
  • les parenthèses ne sont pas indispensables, mais elles sont souvent utiles surtout si on ne connaît pas bien la priorité des opérateurs OCaml.

On retiendra que l'opérateur infixe * est le constructeur du type produit α * β .

Le let, le let rec et les déclarations de fonctions peuvent filtrer les couples.

En tant que conteneurs, les couples peuvent représenter les nombres complexes :

 
Sélectionnez
let add_z (a1,b1) (a2,b2) = (a1+.a2,b1+.b2);;
let mul_z (a1,b1) (a2,b2) = (a1*.a2-.b1*.b2,a1*.b2+.a2*.b1);;

Un couple peut également être utilisé pour retourner deux valeurs, c'est par exemple le cas de la fonction predefinie modf qui renvoie un couple de float :

 
Sélectionnez
# modf;;
- : float -> float * float = <fun>

Les deux composantes du couple renvoyé peuvent être récupérées par un let-filtrant, par exemple cette expression extrait la partie décimale de pi :

 
Sélectionnez
# let pi = 3.14159 in
  let (d,i) = modf pi in d;;
- : float = 0.14158999999999988

Parfois un couple cumule les deux utilités, une fonction renvoie deux valeurs, mais ces valeurs peuvent être interprétées comme une seule valeur composite, par exemple ces valeurs peuvent être les deux bornes d'un intervalle à une dimension.

Règle de typage (construction d'un couple)
La règle de typage d'une expression a,b ne contient qu'un seul cas possible :

  • le type d'un couple a,b est A * BA est le type de l'expression a et B est le type de l'expression b



Pour finir, tout ce qui a été dit sur les couples est généralisable aux triplets ainsi qu'aux n-uplets (tuples) quelconques.

 
Sélectionnez
# (1,2,3);;
- : int * int * int = (1, 2, 3)
 

III-A-15. 19. Les huit reines

Le problème des 8 reines est un problème classique, l'exercice consiste à générer toutes les façons de positionner 8 reines sur un échiquier sans qu'aucune ne puisse en prendre une autre. Nous allons résoudre ce problème plus facilement qu'il n'y paraît.

Supposons qu'une reine soit sur la case (xa,ya) et une autre sur la case (xb,yb), alors le prédicat check_queens nous dit si ces deux reines sont en position de prise :

 
Sélectionnez
let check_queens (xa,ya) (xb,yb) =
  (xa = xb) or (ya = yb) or
  (xa - xb) = (ya - yb)  or
  (ya - yb) = (xb - xa);;

Cela résout-il notre problème, apparemment non. Mais réfléchissez un instant aux fonctions polymorphes que nous venons de développer, elles ont le pouvoir d'étendre la sémantique d'une fonction aussi loin qu'il est nécessaire pour la résolution de notre problème.

En effet, considérez que :

  • il n'y a forcément qu'une reine par colonne ;
  • il n'y a forcément qu'une reine par ligne ;
  • nous appelons reine n la reine placée sur la nième ligne, alors permute [1;2;3;4;5;6;7;8] place les reines 1 à 8 sur toutes les permutations de colonnes possibles ;
  • nous pouvons alors utiliser mapi pour créer les couples de coordonnées (colonne,ligne) ;
  • pour détecter une liste de reines qui ne serait pas une solution, nous pouvons appliquer le test exists_commutative check_queens car le prédicat check_queens est commutatif ;
  • nous pouvons utiliser filter pour ne garder que les listes de reines qui sont des solutions du problème.

Ce qui nous donne :

 
Sélectionnez
let safe_boards queens =
  filter
    (fun l -> exists_commutative check_queens l = false) 
    (mapi
      (fun _ l -> mapi (fun a b -> a,b) 1 l)
      1
      (permute queens)
    );;

safe_boards [1;2;3;4;5;6;7;8];;

Et comme la permutation nous garantit que chaque reine est sur un couple (colonne,ligne) unique nous pouvons simplifier le prédicat check_queens comme ceci :

 
Sélectionnez
let check_queens (xa,ya) (xb,yb) =
  (xa - xb) = (ya - yb) or
  (ya - yb) = (xb - xa) ;;

Remarque : les opérateurs logiques or et && sont semi-stricts, si la valeur de leur premier argument suffit à établir le résultat alors ils n'évaluent pas leur second argument. De ce fait la fonction check_queens ci-dessus est aussi efficace que la fonction check_queens ci-dessous.

 
Sélectionnez
let check_queens (xa,ya) (xb,yb) =
  if (xa - xb) = (ya - yb) then true
  else (ya - yb) = (xb - xa) ;;
 

III-A-16. 20. Le tri fusion

Encore un exemple, pour illustrer la récursion et la démarche diviser pour régner, et encore une fonction polymorphe sur les listes. Celle-ci trie une liste par ordre croissant. Elle tire son polymorphisme de celui de la fonction (>) :

 
Sélectionnez
# (>);;
- : 'a -> 'a -> bool = <fun>

En OCaml toutes les fonctions de comparaison sont des prédicats polymorphes, en effet, si 'a est int ou float alors la comparaison a son sens mathématique habituel, avec les types char et string les données sont comparées suivant l'ordre alphabétique, sinon les données sont comparées selon une méthode structurelle (voir la documentation en ligne du module Pervasives pour plus de détails).

Le tri-fusion polymorphe nécessite trois fonctions auxiliaires.
La fonction merge_list fusionne deux listes a et b déja triées et renvoie leur union, c'est-à-dire une liste triée qui contient à la fois les éléments de a et ceux de b :

 
Sélectionnez
let rec merge_list a b =
  match a,b with 
  | [],_ -> b
  | _,[] -> a
  | ha::ta,hb::tb ->
      if ha < hb then ha::(merge_list ta b)
      else hb::(merge_list a tb);;

Le premier élément d'une liste est l'élément de rang zéro.
La fonction odd_list renvoie la liste des éléments de rang impair de la liste l :

 
Sélectionnez
let rec odd_list l =
  match l with
  | []  -> []
  | [a] -> []
  | a::b::l -> b::odd_list l;;

La fonction even_list renvoie la liste des éléments de rang pair de la liste l :

 
Sélectionnez
let rec even_list l =
  match l with
  | []   -> []
  | a::l -> a::odd_list l;;

Enfin, la fonction merge_sort est la fonction du tri-fusion d'une liste l.
Elle procède en trois étapes :

  • elle scinde la liste l en deux listes ;
  • elle trie ces deux listes ;
  • elle fusionne les deux listes triées en une seule.

Cette décomposition récursive du processus de tri s'arrête nécessairement puisque l'on sait trier une liste vide et une liste qui contient un seul élément. Sinon la liste contient au moins deux éléments et alors on applique le procédé décrit ci-dessus. Voici le code de cette fonction :

 
Sélectionnez
let rec merge_sort l =
  match l with
  | [] -> []
  | [a] -> [a]
  | _ ->
      let p = even_list l in
      let q = odd_list  l in
      merge_list (merge_sort p) (merge_sort q);;

Remarque : on aurait pu effectuer la scission d'une liste en une seule étape, à l'aide de couples, comme ceci

 
Sélectionnez
let rec split_list l =
  match l with
  | [] -> [],[]
  | [a] -> [a],[]
  | a::b::l -> let p,q = split_list l in (a::p,b::q);;

let rec merge_sort l =
  match l with
  | [] -> []
  | [a] -> [a]
  | _ ->
      let p,q = split_list l
      in merge_list (merge_sort p) (merge_sort q);;

Remarque : il existe également un prédicat compare a b, qui effectue une comparaison plus complète que (<), qui est lui aussi polymorphe, et qui renvoie un entier dont le signe dépend de a < b et a > b

 
Sélectionnez
# compare;;
- : 'a -> 'a -> int = <fun>

Ce prédicat pourrait être implémenté ainsi pour les entiers :

 
Sélectionnez
# let compare_int a b = a - b;;
val compare_int : int -> int -> int = <fun>

Bien sûr cette version est monomorphe, elle n'accepte que les entiers, mais elle donne une bonne idée du comportement du compare polymorphe.

 

III-A-17. 21. Les modules

Bien entendu OCaml permet de gérer les gros projets et les projets en équipe, pour cela il fournit un mécanisme de modularité qui offre un espace de nommage (et même beaucoup plus, mais cela n'est pas le sujet de ce paragraphe). Ce mécanisme de modularité est d'autant plus indispensable que certaines fonctions sont "trop" polymorphes. Par exemple la fonction compare (celle du module Pervasives) est définie d'une façon trop générique, pour des types composites elle ne renverra pas forcément le résultat escompté, il faudra alors définir une fonction compare plus spécialisée qui renvoie le résultat attendu pour ce nouveau type. Il devient alors logique, pour ne pas écraser le compare habituel, de réunir la définition du nouveau type composite et la définition de sa fonction compare dans un même module.

Le mot-clé module permet de limiter la portée du let. Ainsi, en introduisant un module List on pourra limiter la portée des fonctions filter, exists, flatten sur les listes, ceci permettrait par exemple de réutiliser ces noms dans un hypothétique module Tree. Choisir des noms identiques pour des fonctionnalités identiques est une bonne pratique qui participe à augmenter la lisibilité des programmes.

La bonne nouvelle c'est que ce module List existe déjà, c'est le fichier Objective Caml/libs/list.ml, et il contient les fonctions List.filter, List.exists, et List.flatten telles que nous les avons défines, avec des implémentations récursives terminales quand cela est possible. Pour une spécification plus complète du module List veuillez consulter sa documentation en ligne.

Parfois la récursion terminale n'est pas directement possible, c'est le cas de la fonction List.append qui est la version préfixe de l'opérateur de concaténation de listes (@) que nous avons déjà utilisé. Il est possible d'écrire une version récursive terminale de la concaténation, en ajoutant un accumulateur, mais comme nous l'avons vu ceci peut avoir pour effet de renvoyer un résultat qui est le miroir du résultat voulu, dans le cas de la concaténation de liste ceci nous donne la fonction List.rev_append :

 
Sélectionnez
# List.rev_append;;
- : 'a list -> 'a list -> 'a list = <fun>

Cette fonction a le même type que List.append, elle est récursive terminale et elle concatène bien ses deux arguments, mais son résultat est renversé, nous verrons néanmoins que cette fonction n'est pas inutile, car nous n'aurons pas forcément besoin de renverser son résultat à l'aide de List.rev.

Vous l'avez deviné, List.rev l est la fonction qui renverse la liste l, elle est récursive terminale :

 
Sélectionnez
let rev l = rev_append l [];;

Voilà bien un premier exemple qui montre que List.rev_append n'est pas l'aberration qu'on pourrait croire.

Le module List contient également les fonctions List.map, List.for_all et List.sort.

La fonction List.map ressemble à notre fonction mapi à la différence près que List.map ne tient pas compte de la position des éléments dans la liste :

 
Sélectionnez
# List.map;;
- : ('a -> 'b) -> 'a list -> 'b list = <fun>

La fonction List.map applique la fonction ('a → 'b) sur tous les éléments de la liste ('a list) et renvoie une liste ('b list).
Cette fonction n'est pas récursive terminale, sa version récursive terminale (mais renversée) est List.rev_map.

La fonction List.for_all ressemble fort à la fonction List.exists et d'ailleurs on pourrait l'implémenter ainsi :

 
Sélectionnez
let for_all p l = List.exists (fun x -> p x = false) l = false;;

Elle dit simplement si les éléments d'une liste l vérifient tous le prédicat p.

Enfin la fonction List.sort ressemble fort à notre fonction merge_sort à la différence qu'en plus elle attend pour premier argument une fonction similaire à compare et c'est cette fonction qui lui sert de critère pour le tri :

 
Sélectionnez
# List.sort;;
- : ('a -> 'a -> int) -> 'a list -> 'a list = <fun>

Il reste le cas de nos fonctions mapi, exists_commutative et permute, et comme elles ont déjà prouvé leur utilité nous les conserverons dans un nouveau module que l'on nommera ListXP :

 
Sélectionnez
module ListXP = struct
  let rec mapi f n l = ...
  let rec exists_commutative p l = ...
  let rec permute l = ...
end;;

La portée de leurs déclarations est alors délimitée par les mots-clés struct et end.
Nous en profitons pour améliorer leurs implémentations en fournissant une version récursive terminale rev_mapi et en utilisant les fonctions du module List :

 
Sélectionnez
module ListXP = struct

  let rec mapi f n l =
    match l with
    | []   -> []
    | h::t -> f n h::mapi f (n+1) t;;

  let rev_mapi f n l =
    let rec loop l n acc = 
      match l with
      | []   -> acc
      | h::t -> loop t (n+1) (f n h::acc)
    in loop l n [];;

  let rec exists_commutative p l =
    match l with
    | []   -> false
    | h::t -> (List.exists (p h) t) or exists_commutative p t;;

  let rec distribute a l =
    match l with
    | []   -> [[a]]
    | h::t -> (a::l) :: (List.map (fun x -> h::x) (distribute a t));;

  let rec permute l =
    match l with
    | []   -> [[]]
    | h::t -> List.flatten (List.map (distribute h) (permute t));;

end;;

Notons qu'un module est une expression et a donc un type (qu'on appelle aussi signature), voici la signature de notre module ListXP telle que l'interpréteur OCaml l'a inférée :

 
Sélectionnez
module ListXP :
  sig
    val mapi : (int -> 'a -> 'b) -> int -> 'a list -> 'b list
    val rev_mapi : (int -> 'a -> 'b) -> int -> 'a list -> 'b list
    val exists_commutative : ('a -> 'a -> bool) -> 'a list -> bool
    val distribute : 'a -> 'a list -> 'a list list
    val permute : 'a list -> 'a list list
  end

Remarque : le nom d'un module doit obligatoirement commencer par une majuscule.

 

III-A-18. 22. Le traitement des erreurs

Parfois une fonction ne peut pas renvoyer de résultat parce que ses arguments sont incompatibles avec le calcul à effectuer.
Il existe trois façons simples de régler ces cas, par ordre de préférence :

  • la fonction assert, déjà vue ;
  • la fonction invalid_arg appliquée à une chaîne de message d'erreur ;
  • la fonction failwith appliquée à une chaîne de message d'erreur.

Voici un cas où l'on privilégiera la fonction assert parce qu'il n'y pas de filtrage :

 
Sélectionnez
# abs min_int;;
- : int = -1073741824
# let safe_abs n =
    assert(n > min_int);
    abs n;;
val safe_abs : int -> int = <fun>

Voici un cas où l'on privilégiera au contraire la fonction invalid_arg parce que le cas à éliminer est déjà pris en compte par un filtrage exhaustif :

 
Sélectionnez
# let maximum l =
    let rec loop a l =
      match l with
      | []   -> a
      | b::t -> if a > b then loop a t else loop b t
    in match l with
    | []   -> invalid_arg "maximum"
    | a::t -> loop a t;;
val maximum : 'a list -> 'a = <fun>

On réservera l'usage de la fonction failwith aux cas où les arguments sont bien dans le domaine de définition.

Remarque : les fonctions failwith et invalid_arg sont de type string'a, un type à l'évidence totalement dépourvu de sémantique, avec pour seul avantage qu'il trouve sa place n'importe où.
La bonne nouvelle c'est qu'un déclenchement d'erreur pourra se substituer à n'importe quelle expression.
La mauvaise nouvelle c'est que n'importe quelle expression suffisamment obscure pourra cacher un déclenchement d'erreur.

 

III-A-19. 23. Les enregistrements

Les enregistrements (records) correspondent eux aussi à un produit cartésien de valeurs et ont la même utilité que les couples ou n-uplets. La principale différence c'est qu'avec un n-uplet un élément est identifié par sa position alors qu'avec un enregistrement un élément est identifié par son étiquette (label).

Une autre différence c'est le typage, OCaml doit pouvoir typer la totalité d'un couple alors que pour un enregistrement il lui suffit de typer chacune de ses composantes. Nous étudierons plus tard en quoi le typage limite parfois les couples et avantage les enregistrements.

Typiquement la déconstruction d'un couple se fait plutôt par filtrage alors que la déconstruction d'un enregistrement se fait plutôt par ses accesseurs. Le filtrage est plus naturel pour le calcul symbolique, les accesseurs sont plus naturels pour les calculs numériques.

En voici un exemple, nous déclarons d'abord les types vector et point, en trois dimensions :

 
Sélectionnez
type vector = {x: float; y: float; z: float};;
type point  = vector;;

Le type point n'est qu'un alias (un synonyme) pour le type vector, parce qu'un point peut être désigné par le vecteur qui le sépare de l'origine. Nous pouvons maintenant implémenter les opérations vectorielles classiques, l'addition, la soustraction, le produit scalaire et le carré scalaire :

 
Sélectionnez
let add a b =
  {x = a.x +. b.x; y = a.y +. b.y; z = a.z +. b.z;};;    

let sub a b =
  {x = a.x -. b.x; y = a.y -. b.y; z = a.z -. b.z;};;    

let scalar_dot a b =
  a.x *. b.x +.
  a.y *. b.y +.
  a.z *. b.z;;

let scalar_sqr v =
  v.x *. v.x +.
  v.y *. v.y +.
  v.z *. v.z;;

Nous ajoutons également un constructeur qui fabrique un vecteur à partir de deux points :

 
Sélectionnez
let make (a: point) (b: point) =
  {x = b.x -. a.x; y = b.y -. a.y; z = b.z -. a.z;};;

On remarquera la notation (expr: type) qui demande explicitement au compilateur d'essayer d'inférer le type mentionné. Ceci n'a rien à voir avec le transtypage dans les langages pourvus d'un typage moins fort que celui d'OCaml, si le compilateur ne parvient pas à inférer le type demandé alors une erreur de typage sera reportée. Ici on demande au compilateur d'inférer le type point plutôt que le type vector. Cette façon d'orienter le typage s'appelle une annotation de type.

Nous voilà prêts pour affronter la problématique de base du lancé de rayons. Un rayon est une demi-droite paramétrée s + tv , elle a pour source le point s et pour direction le vecteur v.
Une sphère a pour centre un point c et pour rayon un réel r. L'intersection d'un rayon avec une sphère est soit vide, soit un intervalle (t1,t2) de paramètres du rayon s + tv.

 
Sélectionnez
# let ray_sphere (s: point) v (c: point) r =
    (* Carré du rayon de la sphère: *)
    let r2 = r *. r   in
    (* Vecteur de la source au centre de la sphère: *)
    let sc = make s c in
    (* Carré de la distance de la source au centre de la sphère: *)
    let sc2 = scalar_sqr sc   in
    let v2  = scalar_sqr v    in
    let p   = scalar_dot sc v in
    let p2  = p *. p    in
    (* Carré de la distance du rayon au centre de la sphère: *)
    let d2 = sc2 -. p2 /. v2 in
    let delta = r2 -. d2 in
    if delta <= 0. then
      failwith "empty"
    else
      let sq = sqrt (delta /. v2) in
      let t1 = p /. v2 -. sq in
      let t2 = p /. v2 +. sq in
      if t2 < 0. then
        failwith "empty"
      else
        (max 0. t1, t2);;
val ray_sphere : point -> vector -> point -> float -> float * float = <fun>

Règle de typage (synonymie de type)
Cette règle précise le sens de la condition "même type" en présence d'aliasing de type :

  • un type est le même type qu'un type qui est son alias.


Règle de typage (typage explicite)
La règle de typage d'une expression explicitement typée (expr: E) distingue deux cas possibles :

  • ou bien le type de expr est le même type que E alors l'expression est de type E ;
  • ou bien l'expression est mal typée.
 

III-A-20. 24. Les performances

Bien que les langages fonctionnels soient plus proches de la spécification que les langages impératifs ils n'en sont pas moins exempts des considérations de performances. Il y trois types de ressources à économiser :

  • la pile est une ressource mémoire locale et limitée ;
  • le tas désigne la mémoire globale ;
  • les processeurs désignent les ressources d'exécution.

Du fait de leur système de typage bien plus avancé, les langages fonctionnels peuvent mieux capitaliser sur la modularité pour réutiliser les meilleurs algorithmes et les meilleures structures de données les plus adaptés aux spécificités du domaine. Du coup le programmeur fonctionnel est généralement moins sous pression quant aux gains de performances liés à l'économie des ressources d'exécution.

Il faudra tout de même prendre garde à partager les calculs autant que possible, par exemple

 
Sélectionnez
let square f x = f x *. f x;;

devra s'écrire :

 
Sélectionnez
let square f x = let y = f x in y *. y;;

Sinon le calcul, potentiellement long, de f x se fera deux fois au lieu d'une seule.

La contrepartie d'une meilleure maîtrise des ressources d'exécution c'est que le programmeur fonctionnel doit faire davantage attention à économiser les ressources mémoire, les langages fonctionnels sont des langages de haut niveau, ils ont une tendance naturelle à consommer plus de pile et de tas que les langages de bas niveau.

L'astuce principale pour économiser la pile consiste à privilégier la récursion terminale, par exemple si l'on veut concaténer deux listes et que seuls comptent les éléments (pas leur ordonnancement) alors on utilisera List.rev_append plutôt que List.append parce que List.rev_append est récursive terminale et pas List.append. Il en va de même pour List.rev_map comparée à List.map.

Dans d'autres cas, il faudra utiliser l'accumulation si cela est nécessaire, notamment si l'on envisage de traiter des chaînes de longueur potentiellement élevée. Souvent il sera plus aisé de faire l'implémentation en deux étapes, d'abord écrire une version récursive, la tester, puis ensuite écrire une nouvelle version avec accumulation.

Une astuce de nature similaire s'applique au tas, elle consiste à remplacer les listes normales par des listes paresseuses, ces listes sont détruites au fur et à mesure qu'elles sont construites de sorte que seule leur tête est présente dans le tas au lieu de toute leur longueur. Nous n'aborderons pas les listes paresseuses dans ce chapitre.

Une autre astuce appréciable pour économiser le tas consiste à éviter la duplication des listes et des couples quand elle n'est pas strictement indispensable.

Pour les listes cela peut se faire en cumulant deux fonctionnalités en une seule, par exemple si on effectue des grandes quantités de ce traitement :

 
Sélectionnez
List.rev_map f (List.filter p l);;

Alors on pourra avantageusement le remplacer par :

 
Sélectionnez
let rev_map_filter p f l =
  let rec loop l acc =
    match l with
    | [] -> acc
    | h::t ->
        if p h then loop t (f h::acc)
        else loop t acc
  in loop l [];;

Ce qui permet d'économiser la production de listes par filter qui seraient aussitôt consommées par rev_map.

Une situation similaire concerne les couples, dans les cas où soit on veut pouvoir à la fois les détruire et les conserver.
Supposons que nous voulions ordonner tous les couples d'une liste, que le second élément du couple soit toujours supérieur au premier, voici une façon de faire :

 
Sélectionnez
let ordered_pair (a,b) = if a < b then (a,b) else (b,a);;
let list_ordered_pairs l = List.map ordered_pair l;;

Le cas dérangeant c'est le cas a < b, car alors nous détruisons le couple pour en construire un autre en tous points identique, en conséquence le tas est davantage sollicité qu'il ne le devrait. Afin d'éviter ces cas il existe le mot-clé as qui permet d'apparier une variable (ici p) à la totalité d'un motif (ici a,b) :

 
Sélectionnez
let ordered_pair (a,b as p) = if a < b then p else (b,a);;

Avec cette version, seuls les "nouveaux" couples sont alloués, les anciens sont réutilisés.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Damien Guichard. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.