Quelles sont les structures de contrôle de base en Caml ?

Contacter l'auteur Pierre.Weis@inria.fr

Fichier créé en juin 1996.

Caml offre les structures de contrôle suivantes:

Si aucune de ces structures de contrôle ne répond à votre problème, il vous faut définir vos propres structures de contrôle en définissant vos propres fonctions (récursives ou non).

Table des matières:

if ... then ... else

Les tests se font de façon classique à l'aide d'une expression if condition then expression1 else expression2 qui renvoie expression1 ou expression2, selon la valeur booléenne de la condition.

Les opérateurs arithmétiques booléens sont =, <, >, <=, >=, <>, et les opérateurs booléens sont &&, ||, not. Par exemple:

let valeur_absolue x = if x >= 0 then x else - x;;
valeur_absolue : int -> int = <fun>

Typage: la condition doit être de type bool, et les deux branches doivent avoir le même type, qui est le type de toute la construction.

if ... then

Un if sans partie else correspondante est complété automatiquement avec une clause else (). Ce genre de construction est utile dans les séquences.

Typage: la condition doit être de type bool, et les deux branches doivent avoir le même type (unit).

Le filtrage

Le filtrage ou analyse de formes ou correspondance de patrons, sert à faire des tests complexes où intervient la forme de l'argument soumis à la construction.

Syntaxiquement, un filtrage se présente comme une liste de clauses. Chaque clause est une construction de la forme | filtre -> expression. Le mécanisme de filtrage consiste à analyser la forme d'une valeur donnée pour la comparer avec chacun des filtres (ou formes ou patrons) de la liste des clauses. L'analyse réussit dès que la valeur peut être considérée comme un exemple du filtre proposé par la clause. Une valeur est un exemple d'un filtre si elle en est un cas particulier (ou encore que le filtre est une forme générale de la valeur ou encore que le filtre est plus général que la valeur).

Un filtre est composé de constantes (constantes de base ou constructeurs), de variables et d'applications de constructeurs fonctionnels à des filtres arguments. On peut ainsi considérer les clauses | 0 -> 1 (filtre entier constant 0) ou | n -> 1 (filtre réduit à une variable n). La seule valeur filtrée par le filtre 0 est évidemment 0, alors que n filtre toute valeur quelque soit sa forme. Finalement un filtre de la forme C fC est un constructeur et f un filtre, filtre toutes les valeurs qui sont construites avec le constructeur C avec pour argument une valeur v qui est filtrée par f.

Les filtres des clauses sont essayés successivement dans l'ordre de présentation donné dans le programme, jusqu'à ce que l'un d'entre eux réussisse. Si aucun ne réussit une erreur se produit à l'exécution (pour plus de détails).

La valeur retournée par un filtrage est toujours celle de l'expression correspondant à la clause qui a réussi.

La construction syntaxique de base pour déclencher un filtrage est match expression with clauses: elle évalue tout d'abord expression, obtenant une valeur dont la forme est ensuite analysée et comparée avec chacune des clauses successives présentées dans la partie clauses du filtrage. Par exemple, succ 0 valant 1, le filtrage suivant renvoie l'expression correspondant à la clause | 1 -> "unité":

#match succ 0 with
| 0 -> "nul"
| 1 -> "unité"
| _ -> "plusieurs";;
- : string = "unité"

Remarquez la dernière clause | _ -> "plusieurs" qui comporte un souligné en partie filtre: le filtre _ signifie ``n'importe quoi d'autre'' et filtre donc toute valeur. Il sert souvent comme dernier filtre pour capturer tous les cas non envisagés plus haut dans le filtrage.

Le filtrage d'une valeur par une variable produit un autre effet: pendant le calcul de l'expression à droite de la clause correspondante, la variable vaut la valeur filtrée (on dit que le filtrage a opéré une liaison de la variable à la valeur). Par exemple, succ 1 valant 2, le filtrage suivant renvoie l'expression correspondant à la clause | n -> string_of_int n, et renvoie donc string_of_int n avec n valant 2 (on dit aussi que n est lié à 2):

#match succ 1 with
| 0 -> "nul"
| 1 -> "unité"
| n -> string_of_int n;;
- : string = "2"

On remarque que la construction if ... then ... else ... est un cas particulier de filtrage sur les booléens: if cond then e1 else e2 correspondant à

match cond with
| true -> e1
| _ -> e2

Typage: dans un filtrage toutes les clauses doivent avoir le même type: les filtres doivent tous avoir le même type et les expressions doivent aussi avoir un type commun. Dans le cas de la construction match e with filtrage, le type commun aux filtres doit être celui de e, et le type commun aux expressions des clauses du filtrage est celui de toute la construction.

Filtrages partiels ou redondants

Les filtrages doivent être dans la mesure du possible exhaustifs (tous les cas doivent être envisagés) et non redondants (il n'y a jamais deux cas qui peuvent s'appliquer). Le compilateur Caml émet généralement une alerte quand on lui présente un tel filtrage non exhaustif ou comportant des cas inutiles.

Au cas où le filtrage échoue à l'exécution parce que la valeur n'est un exemple d'aucun des filtres proposés, une exception est déclenchée qui indique généralement l'endroit du programme où l'erreur s'est produite.

Filtrages imbriqués

Pour des raisons syntaxiques, un filtrage interne à un autre filtrage doit toujours être parenthésé (à l'aide de parenthèses ou des mots clés begin et end).

Considérez en effet le filtrage de l'expression e2, interne au filtrage de l'expression e1:

match e1 with
| f1 ->
   match e2 with
   | f2 -> ...
   | f3 -> ...
| f4 -> e2

Tel qu'écrit ici, on comprend très bien que f2 et f3 sont des filtres du filtrage de e2, ce qui est parfaitement exact. En revanche, un lecteur humain interprètera sans doute l'indentation comme signifiant que le filtre f4 appartient au filtrage englobant, celui de e1. Pourtant ce filtre suit naturellement f3 que nous attribuons bien sûr au filtrage de e2. C'est pourquoi, et malgré l'indentation trompeuse, f4 est syntaxiquement rattaché au filtrage interne (celui de e2). L'intention du programmeur était sans doute:

match e1 with
| f1 ->
   begin match e2 with
   | f2 -> ...
   | f3 -> ... end
| f4 -> e2

Pour éviter ce genre d'erreurs, ayez le réflexe de toujours parenthéser les filtrages internes à d'autres filtrages.

Filtrages et conditionnelles

Lors d'un filtrage, vous pouvez évaluer une condition booléenne quelconque (qui peut même porter sur les variables du filtre): après le patron vous introduisez la condition par le mot-clé when, puis écrivez l'expression à tester. La clause correspondante ne s'appliquera que dans le cas où la condition est vérifiée. Sinon, le filtrage continuera comme d'habitude avec le cas suivant. Par exemple:

(* power i j computes i ^ j, provided j is positive. *)
let rec power i j =
 match j with
 | 0 when i != 0 -> 1
 | 1 -> i
 | j when j mod 2 = 1 -> let p = power i (j / 2) in i * p * p
 | j when i = 0 -> invalid_arg "power"
 | _ -> let p = power i (j / 2) in p * p;;
power : int -> int = <fun>

Un autre exemple typique consiste à tester l'égalité de deux variables d'un patron. Ce prédicat détermine si une liste comporte deux éléments successifs égaux:

let rec two_successive_equal = function
| [] -> false
| [_] -> false
| x :: y :: l when x = y -> true
| x :: l -> two_successive_equal l;;
two_equal : 'a list -> bool = <fun>

Pour en savoir plus, voir aussi ici.

Le séquencement

La séquence e1; e2 sert à signifier l'évaluation successive des deux expressions e1 puis e2. La valeur de la construction est celle de e2. Cette construction sert évidemment à faire des effets, par exemple des impressions:

#print_string "Hello ";
 print_string "World!";;
Hello World!- : unit = ()

Typage: le type d'une séquence est le type de la dernière expression.

Séquence et filtrage

La séquence en Caml n'a pas besoin d'être parenthésée dans les clauses de filtrage: ... -> e1; e2 est équivalent à ... -> begin e1; e2 end.

Séquence et if ... then ... else ...

Une séquence apparaissant dans l'une des alternatives d'une alternative, doit être parenthésée. Pour cela on utilise généralement begin et end. On écrit

if cond
 then begin e1; e2 end
 else begin e3; e4 end

Corrélativement, une alternative dans une séquence ne nécessite pas de parenthésage. Ainsi

if cond1 then e1;
if cond2 then e2 else begin e3; e4 end;;

signifie:

if cond1 then e1 else ();
if cond2 then e2 else begin e3; e4 end;;

En revanche, si l'on ne parenthèse pas la séquence correspondant à l'une des branches d'une l'alternative, cette séquence appartient à l'expression englobante. Par exemple, dans l'expression:

if cond1 then e2 else e3; e4;;

l'expression e4 est exécutée quelquesoit la valeur de la condition cond1. En effet cette expression est équivalente à:

(if cond1 then e2 else e3);
e4;;

Les boucles

Les boucles sont de deux types, boucle for et boucle while. Elles s'évaluent pour leurs effets et rendent toujours la valeur () ou ``rien''.

Boucle while

On écrit while condition do expression done, pour signifier l'évaluation répétitive de expression tant que condition s'évalue à vrai. En particulier, si condition est toujours fausse, expression n'est jamais évaluée.

Typage: la condition doit avoir le type bool, mais une boucle a toujours le type unit.

Boucle for

On écrit for ident = initial to final do expression done, pour signifier l'évaluation répétitive de expression, pour des valeurs de ident valant successivement initial, initial + 1, ..., jusqu'à final inclus. En particulier, si initial est strictement supérieur à final, expression n'est jamais évaluée.

Remarquez que ident est introduit par la boucle for, il n'a pas besoin d'avoir été préalablement défini ou déclaré. Remarquez aussi qu'il est impossible de modifier la valeur de ident pendant le déroulement de la boucle (d'ailleurs ident n'est pas une référence mutable).

#for i = 0 to 10 do print_int i; print_char ` ` done;;
0 1 2 3 4 5 6 7 8 9 10 - : unit = ()

Typage: l'indice de boucle est du type int de même que les expressions initial et final. La boucle a le type unit.

Sortie prématurée des boucles

Pour sortir d'une boucle de façon prématurée, il faut utiliser le mécanisme d'exception. On déclenche l'exception prédéfinie qui est prévue pour ce cas: l'exception Exit (voir plus loin).

Le déclenchement d'exceptions

La primitive raise sert à déclencher des exceptions (signaler des erreurs) en cas d'impossibilité de poursuivre les calculs. Ces exceptions devront être rattrapées par une construction ``try ... with'' englobante pour traiter l'erreur (soit en interrompant le programme après avoir imprimé un message, soit en poursuivant le traitement d'une autre manière). Si personne ne rattrape l'erreur, l'échec se propage jusqu'à l'arrêt complet de l'évaluation. Par exemple:

#print_string "Bonjour"; 
#raise (Failure "division par zéro");
#print_string " à tous ";;
BonjourException non rattrapée: Failure "division par zéro"

Typage: si exception a pour type exn, alors raise exception a pour type 'a.

Le rattrapage d'erreurs

La construction try expression with filtrage sert à la gestion des erreurs: on exécute expression en surveillant les erreurs qui peuvent se produire pendant cette exécution. On envisage les différents cas d'erreur dans la partie ``filtrage'' du try ... with, et dans le cas où une telle erreur se produit on la filtre par ce filtrage, et l'on renvoie l'expression correspondante de la clause sélectionnée. Si aucune clause ne convient l'exception est propagée (redéclenchée).

Typage: le typage de try expression with filtrage est analogue à celui de match expression with filtrage: les filtres du filtrage doivent tous avoir le type des valeurs exceptionnelles, exn, et le type commun aux expressions du filtrage doit être celui de l'expression protégée, expression. Ce type est celui de toute la construction.

La sortie prématurée des boucles

À l'aide du mécanisme d'exception, on implémente facilement la sortie prématurée des boucles for et while (l'équivalent de l'instruction break de C, ou exit du langage Ada). Pour cela, on déclenche l'exception prédéfinie Exit dans le corps de la boucle, et l'on pose un surveillant d'exception à l'extérieur de la boucle.

Par exemple, nous définissons une fonction qui teste si un vecteur donné contient un élément nul: si on rencontre un 0 dans le vecteur, on sort instantanément de la boucle de parcours et l'on rend la valeur vrai, sinon la boucle se déroule jusqu'au bout, et l'on retourne faux.

let contient_un_zéro v =
 try
  for i = 0 to vect_length v - 1 do
   if v.(i) = 0 then raise Exit;
  done;
  false
 with Exit -> true;;

La définition d'exceptions

L'utilisateur définit ses propres exceptions avec la construction exception nom;; pour une exception constante, ou exception nom of typ;; pour une exception avec argument. Ces exceptions sont génératives, c'est-à-dire que deux définitions successives d'une exception de même nom, créent deux exceptions différentes qui ne sont pas confondues par les programmes.

Typage: une exception constante a le type exn, une exception définie par exception nom of typ;; a le type typ -> exn.

Les exceptions prédéfinies

Un certain nombre d'exception sont prédéfinies pour signaler des problèmes ou erreurs courantes:


Page de présentation de Caml Dernière modification: vendredi 26 mars 2004
Copyright © 1995 - 2004, INRIA tous droits réservés.

Contacter l'auteur Pierre.Weis@inria.fr