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

Le langage fonctionnel Caml

Support de cours basé sur Caml Light


précédentsommairesuivant

V. Types définis, types paramétrés

Un langage de programmation évolué doit offrir la possibilité de définir les types nécessaires à la gestion d'une application informatique particulière. Par exemple, la gestion d'une bibliothèque implique la manipulation Par exemple, les types livre, abonné ou emprunt pour une application de gestion de bibliothèque.

Caml permet de construire de nouveaux types par produit ou union de types existants : les premiers sont appelés types enregistrement et les seconds, types union . Caml permet de plus la définition de types paramétrés ou polymorphes . Par exemple : le type « pile de valeurs de type quelconque », qui pourra ensuite être instancié selon les besoins, en « pile d'entiers », « piles de caractères », etc.

Nous étudierons successivement les types enregistrements, les types unions et les types paramétrés et nous présenterons de nombreux exemples les utilisant.

V-A. Types enregistrement

Un enregistrement est un ensemble de valeurs nommées. Par exemple, une adresse composée d'une rue, d'un code postal et d'une ville.

Un type enregistrement est défini par la phrase :

 
Sélectionnez
type t = {C1: t1; …; Cn: tn};;

t , C 1 , …, Cn sont des identificateurs et t 1 , … tn sont des expressions de types. Cette phrase définit un type enregistrement de nom t , dont les instances sont les enregistrements { C 1 = v 1 ; …; Cn = v n } tels que pour tout i de 1 à n , vi est de type t i . On a donc :

 
Sélectionnez
ext(t) = {{C1 = v1; …; Cn = vn} | v1  ext(t1), …, vn  ext(tn)}

Les identificateurs C 1 , …, Cn sont les noms des champs du type t et v 1 , …, vn sont les valeurs de ces champs. Les noms de champ C 1 , …, Cn sont spécifiques au type t et ne peuvent donc pas être des noms de champs d'un autre type enregistrement. Dans un enregistrement l'ordre des champs n'a pas d'importance.

Par exemple :

 
Sélectionnez
#type enfant = {Prénom: string; Age: int};;
Type enfant defined.

Le type enfant est un type enregistrement qui a deux champs Prénom et Age. La valeur {Age = 12; Prénom = "Amandine"}, par exemple, est une valeur de type enfant.

V-A-1. Construction d'un enregistrement

La construction d'un enregistrement de champs C 1 , …, Cn est réalisée par le constructeur { C 1 = ; …; Cn = }.

Par exemple :

 
Sélectionnez
#{Prénom = "Jean-" ^ "Paul"; Age = 5 + 1};;
- : enfant = {Prénom = "Jean-Paul"; Age = 6}

La sémantique du constructeur d'enregistrement est la suivante :

CONSTRUCTION D'UN ENREGISTREMENT. Si Env est un environnement contenant la définition type t = { C 1 : t 1 ; …; C n : t n } et si pour tout i de 1 à n , ei est une expression telle que type ( e i , Env ) = ti alors { C 1 : e 1 ; …; Cn : e n } est une expression telle que :
type ({ C 1 : e 1 ; …; Cn : e n }, Env ) = t ,
val ({ C 1 = e 1 ; …; Cn = e n }, Env ) = { C 1 = val ( e 1 , Env ); …; Cn = val ( e n , Env )}.

V-A-2. Égalité structurelle

Deux enregistrements de même type sont structurellement égaux si les valeurs des champs de même nom sont structurellement égales.

V-A-3. Accès à la valeur d'un champ

L'accès à la valeur d'un champ est réalisée par l'opérateur « . ». Si v = { C 1 = v 1 ; … ; Cn = v n } alors pour tout i de 1 à n , v . Ci = vi . Par exemple :

 
Sélectionnez
#let jp = {Prénom = "Jean-Paul"; Age = 6};;
jp : enfant = {Prénom = "Jean-Paul"; Age = 6}
#jp.Age;;
- : int = 6

V-A-4. Filtrage

Si t est un type défini par t = { C 1 : t 1 ; … ; C n : t n } et si, pour tout i de 1 à n , vi est une valeur de type ti et Fi est un filtre de type t i , alors :

 
Sélectionnez
{C1 = F1; …; Cn = Fn}

est un filtre de type t tel que :

 
Sélectionnez
env_filtré({C1 = v1; … ; Cn = vn}, {C1 = F1; … ;Cn = Fn}) = env_filtré(v1, F1)  …  env_filtré(vn, Fn)

Par exemple :

 
Sélectionnez
#let age = function {Prénom = _; Age = a} -> a;;
age : enfant -> int = <fun>
#age jp;;
- : int = 6

Puisqu'un nom de champ est attaché à un et un seul type, il est possible, dans un filtre, d'omettre les champs non concernés. La définition de age dans l'exemple précédent peut alors s'exprimer par :

 
Sélectionnez
#let age = function {Age = a} -> a;;
age : enfant -> int = <fun>

V-B. Types union

Un type union rassemble sous un même nom des valeurs de différents types. Par exemple, le type « véhicule » rassemblant les vélos, les motos et les autos.

Un type union est défini par la phrase :

 
Sélectionnez
type t = C1 of t1 | &#8230; | Cn of tn;;

t, C1, …, Cn sont des identificateurs et t1, …, tn sont des expressions de type. Cette phrase définit un type union de nom t dont les instances sont les valeurs Ci v telles que Ci {C1, …, Cn} et v est de type ti. On a donc :

 
Sélectionnez
ext(t) = {C1 v | v &#61646; ext(t1)} &#61640; &#8230; &#61640; {Cn v | v &#61646; ext(tn)}

Les identificateurs C 1 , … , C n sont les noms des constructeurs des valeurs de type t . Ces constructeurs sont spécifiques au type t et ne peuvent donc pas être des noms de champs d'un autre type union.

Pour des raisons de symétrie une barre (« | ») peut être placée devant le premier constructeur.

Par exemple :

 
Sélectionnez
#type forme =
    | Rectangle of float * float
    | Carré of float
    | Cercle of float;;
Type forme defined.

Le type forme est un type union qui a trois constructeurs Rectangle, Carré et Cercle. Les valeurs Rectangle (15.5, 20.0), Carré 12.3 et Cercle 10.25 sont de type forme.

Un type union peut avoir une seule composante. Par exemple :

 
Sélectionnez
#type pile = Pile of int list;;
Type pile defined.

ou bien avoir certaines composantes constantes (c'est-à-dire réduites à leur constructeur). Par exemple :

 
Sélectionnez
#type couleur_primitive = Rouge | Jaune | Bleu;;
Type couleur_primitive defined.

Le type prédéfini bool est un type union défini de la façon suivante :

 
Sélectionnez
type bool = false | true;;

V-B-1. Construction d'une valeur de type union

Une valeur d'un type union est construite par application de l'un des constructeurs de ce type. Par exemple :

 
Sélectionnez
#Carré (27. +. 0.5);;
- : forme = Carre 27.5
#Pile [3; 3 - 1; 3 - 2];;
- : pile = Pile [3; 2; 1]
#[Rouge; Jaune; Bleu];;
- : couleur_primitive list = [Rouge; Jaune; Bleu]

La sémantique d'un constructeur de valeur de type union est la suivante :

CONSTRUCTION D'UNE VALEUR DE TYPE UNION. Si Env est un environnement contenant la définition type t = C1 t1 | … | Cn tn et si e est une expression telle que type(e, Env) = ti et ti {t1, …, tn} alors :
type(Ci e, Env) = t,
val(Ci e, Env) = Ci val(e, Env).

V-B-2. Égalité structurelle

Deux valeurs d'un même type union sont structurellement égales si elles sont produites par l'application du même constructeur à deux valeurs structurellement égales.

V-B-3. Filtrage

Si t un type union défini par t = C1 t1 | … | Cn tn et si F est un filtre de type ti {t1, …, tn} alors :

 
Sélectionnez
Ci F

est un filtre de type t tel que env_filtré ( C i F , C i v ) = env_filtré ( F , v ).

Voici quelques exemples de manipulation de valeurs de type forme ou pile définis ci-dessus :

 
Sélectionnez
#let surface = function
    | Rectangle (lon, lar) -> lon *. lar
    | Carré côté -> côté *. côté
    | Cercle rayon -> 3.14 *. rayon *. rayon;;
surface : forme -> float = <fun>
#surface (Cercle 10.);;
- : float = 314
#let empiler i (Pile p) = Pile (i::p);;
empiler : int -> pile -> pile = <fun>
#let dépiler = function
    | Pile (i::p) -> i
    | Pile [] -> failwith "pile_vide";;
dépiler : pile -> int = <fun>
#dépiler (empiler 3 (Pile [1; 2]));;
- : int = 3

La fonction failwith appliquée à une chaîne de caractères m génère une exception, expliquée par le message m . Le traitement des exceptions est l'objet du chapitre suivant (chapitre IV).

Il est naturel que le corps d'une fonction s'appliquant à une valeur de type union comporte un cas pour chaque type de valeur composant ce type. Cette fonction ne pouvant être définie que par filtrage, si un cas n'est pas prévu, Caml le signalera (voir ci-dessus chapitre II, §III.C). Par exemple :

 
Sélectionnez
#let périmètre = function
    | Rectangle (lon, lar) -> 2. *. (lon +. lar)
    | Carré côté -> 4. *. côté;;
> Toplevel input:
>................function
> Rectangle (lon, lar) -> 2. *. (lon +. lar)
>    | Carré côté -> 4. *. côté..
> Warning: pattern matching is not exhaustive

Image non disponible

Figure 5.1 : Pièce composée

 
Sélectionnez
périmètre : forme -> float = <fun>

Le cas Cercle n'a pas été prévu. La fonction périmètre n'est donc applicable qu'aux rectangles et aux carrés :

 
Sélectionnez
#périmètre (Carré 4.);;
- : float = 16

mais pas aux cercles :

 
Sélectionnez
#périmètre (Cercle 10.);;
Uncaught exception: Match_failure ("", 1151, 1235)

(l'exception Match_failure … a été déclenchée par le système qui n'a pu filtrer la valeur Cercle 10.).

V-C. Types récursifs

Il est possible de définir des types récursifs ou mutuellement récursifs. Un type t est récursif lorsque t intervient dans sa propre définition. n types sont mutuellement récursifs, si chacun d'eux apparaît directement ou indirectement dans la définition des n - 1 autres.

Considérons par exemple des pièces (mécaniques, électriques…) comme celle de la figure 5.1, identifiées par leur nom et pouvant être simples ou composées d'autres pièces. Ces pièces peuvent être représentée par un enregistrement du type pièce défini de la façon suivante :

 
Sélectionnez
#type pièce = {Nom: string; Composants: pièce list};;
Type pièce defined.

Le type pièce est récursif puisqu'il apparaît dans sa propre définition.

Par exemple, la pièce A de la figure 5.1 qui est composée d'une pièce simple B et d'une pièce C elle-même composée d'une pièce simple D, pourra être représentée par la valeur suivante, de type pièce :

 
Sélectionnez
{Nom = "A";
Composants = [{Nom = "B"; Composants = []};
        {Nom = "C"; Composants = [{Nom = "D"; Composants = []}]}]}

Le défaut de cette représentation est de ne pas mettre clairement en évidence le fait qu'une pièce soit simple ou composée : une pièce simple est caractérisée par une liste de composants vide. On peut le faire en stipulant explicitement qu'une pièce est soit simple, soit composée, qu'une pièce simple est décrite par son nom et qu'une pièce composée est décrite par un enregistrement à deux champs : son nom et la liste des pièces qui la compose. On représentera donc les pièces comme des valeurs du type pièce défini par :

 
Sélectionnez
type pièce = Simple of string | Composée of pièce_composée

et les descriptions de pièces composées comme des valeurs du type pièce_composée défini par :

 
Sélectionnez
type pièce_composée = {Nom: string; Composants: pièce list}

Les types pièce et pièce_composée sont mutuellement récursifs puisque le type pièce_composée apparaît dans la définition du type pièce et que le type pièce apparaît dans le type pièce_composée. À l'instar des fonctions mutuellement récursives (voir chapitre III, §III.E), ils devront être définis simultanément de la façon suivante :

 
Sélectionnez
#type pièce = Simple of string | Composée of pièce_composée
and pièce_composée = {Nom: string; Composants: pièce list};;
Type pièce defined.

La pièce A de la figure 5.1 pourra alors être représentée par la valeur suivante de type pièce :

 
Sélectionnez
Composée {Nom = "A";
        Composants = [Simple "B";
                Composée {Nom = "C";
                        Composants = [Simple "D"]}]}

V-D. Types paramétrés

Certaines des opérations réalisées sur des valeurs structurées telles que des listes, des piles ou bien encore des arbres, sont indépendantes du type des composantes (ou de certaines composantes) de ces valeurs. Par exemple, le calcul de la longueur d'une liste ne nécessite pas de connaître le type des éléments de cette liste. De même, l'empilement d'un élément sur une pile, ne nécessite pas de connaître le type des éléments de cette pile.

Afin de définir de telles opérations, que l'on peut qualifier de génériques, il est nécessaire que les types des composantes (ou de certaines des composantes) de ces valeurs structurées soit des paramètres et donc que ces valeurs structurées aient un type paramétré.

Reprenons l'exemple des piles, on définira tout d'abord le type paramétré « pile de t  » t est un paramètre de type qui désigne le type des éléments de la pile puis l'opération « empiler une valeur de type t sur une pile de type “pile de t ” ». À partir de ce type et de cette opération, on pourra par exemple engendrer le type « pile de caractères » et l'opération « empiler un caractère sur une pile de caractères » en instanciant t par le type « caractère ».

Rappelons (voir chapitre IV, §IV.B) qu'un paramètre de type est noté ' i i est un identificateur et qu'un type paramétré est un type dont l'expression contient au moins un paramètre de type.

Un type paramétré est défini par la phrase :

 
Sélectionnez
type ('a1, &#8230;, 'an) t = d ;;

où ' p 1 , …, ' pn sont des paramètres de type, t est le nom du type et d est sa définition qui peut être, soit une définition de type enregistrement, soit une définition de type union, contenant les paramètres ' p 1 , …, ' p n . Lorsque le type défini ne possède qu'un seul paramètre les parenthèses peuvent être omises. L'objet t ainsi défini est un constructeur de type, c'est-à-dire une fonction qui appliquée aux types t 1 , …, tn a pour valeur le type obtenu en remplaçant dans d , ' a 1 par t 1 , …, ' an par t n .

Un premier exemple de type paramétré est le type prédéfini list que nous avons étudié au chapitre III. Il est défini de la façon suivante :

 
Sélectionnez
type 'a list = [] | prefix :: of 'a * 'a list

Le paramètre de type 'a représente le type des éléments de la liste. Il faut bien faire attention à la signification de cette définition. Elle stipule que les éléments d'une liste doivent avoir le même type et non que chaque élément peut avoir un type quelconque. Voici, par exemple, ce que répondra Caml si l'on essaie de construire une liste dont les éléments n'ont pas tous le même type :

 
Sélectionnez
#1::"x"::[];;
Toplevel input:
>1::"x"::[];;
> ^^^^^^^
This expression has type string list,
but is used with type int list.

Lorsque Caml rencontre 1, le premier élément de la liste en construction, il lie le paramètre de type 'a au type int et en déduit que la liste en construction doit être une liste d'entiers. Il signale donc une erreur lorsqu'il rencontre le deuxième élément "x" qui est de type string.

Allons un peu plus loin. Considérons, par exemple, la fonction appartient qui teste si une valeur v appartient à une liste l. Elle peut être définie de la façon suivante :

 
Sélectionnez
#let rec appartient v l =
    match l with
        | [] -> false
        | t::q -> if t = v then true else appartient v q;;
appartient : 'a -> 'a list -> bool = <fun>

Son type est le type paramétré : 'a -> 'a list -> bool qui signifie que l'élément dont on teste l'appartenance à la liste doit avoir le même type 'a que les éléments de cette liste. Si cette condition n'est pas vérifiée, voici ce que répondra Caml :

 
Sélectionnez
#appartient "x" 1::2::[];;
Toplevel input:
>appartient "x" 1::2::[];;
>                     ^
This expression has type int,
but is used with type string list.

Lorsque Caml rencontre "x", il lie le paramètre de type 'a au type string et en déduit que l'on veut tester l'appartenance à une liste de chaînes de caractères. Il signale donc une erreur lorsqu'il rencontre la liste 1::2::[] qui est de type int list.

V-E. Sémantique d'une définition de type

Jusqu'ici nous avons considéré qu'un environnement était constitué par un ensemble de liaisons « nom-valeur ». Lorsque qu'un nouveau type est défini il faut que son nom, ainsi que les constructeurs des valeurs de ce type soient insérés dans l'environnement de la session afin de pouvoir être utilisés par le synthétiseur de types. Pour les mêmes raisons, les types prédéfinis et leurs constructeurs de valeur sont présents dans l'environnement initial.

Une définition de type type t = d étend l'environnement courant en :

 
Sélectionnez
Env_courant &#61637; Env_def(type t = d, Env_courant).

Env_def calcule l'environnement ajouté par cette définition de type, selon les règles suivantes :

ENVIRONNEMENT AJOUTE PAR UNE DEFINITION DE TYPE. Si Env est un environnement, alors :
Env_def (type t = { C 1 : t 1 ; …; C n : t n }, Env ) = Env  {type t , cons { C 1 = t 1 ; …; Cn = t n } -> t },
Env_def (type t = C 1 of t 1 | … | Cn of t n , Env ) = Env  {type t , cons C 1 : t 1 -> t , …, cons Cn : tn -> t }.

Les noms de types et les noms de constructeurs ont été préfixés respectivement par les mots-clés type et cons afin de ne pas les confondre avec les noms de valeur, car en Caml ces trois ensembles de noms sont disjoints (c'est-à-dire qu'une valeur, un type ou un constructeur peuvent avoir le même nom).

V-F. Exemples

V-F-1. Calculette

A titre d'exemple, nous allons programmer une calculette permettant d'évaluer des expressions arithmétiques construites à partir des nombres flottants et des cinq opérations classiques d'addition, de soustraction, de multiplication, de division et de calcul de l'opposé.

Il faut tout d'abord choisir une représentation des expressions soumises à la calculette. Nous les représenterons comme des valeurs du type récursif expr défini de la façon suivante :

 
Sélectionnez
#type expr =
    | Nb of float
    | Add of expr * expr
    | Soust of expr * expr
    | Mult of expr * expr
    | Div of expr * expr
    | Opp of expr;;
Type expr defined.

Par exemple, l'expression arithmétique ƒ{(5,2 „e (35,0 + 19,3)) sera représentée par la valeur de type expr suivante :

 
Sélectionnez
Opp (Mult (Nb 5.2, Add (Nb 35.0, Nb 19.3)))

Programmons maintenant notre calculette. L'évaluation d'une expression arithmétique représentée par une valeur de type expr sera réalisée par application à cette expression de la fonction eval définie de la façon suivante :

 
Sélectionnez
#let rec eval = function
    | Nb n -> n
    | Add (e1, e2) -> (eval e1) +. (eval e2)
    | Soust (e1, e2) -> (eval e1) -. (eval e2)
    | Mult (e1, e2) -> (eval e1) *. (eval e2)
    | Div (e1, e2) -> (eval e1) /. (eval e2)
    | Opp n -> -. (eval n);;
eval : expr -> float = <fun>

Remarquons encore une fois la souplesse apportée par la programmation par filtrage. Il suffit de prévoir un cas pour chaque constructeur du type expr.

Essayons :

 
Sélectionnez
#let mon_expression = Opp (Mult ((Nb 5.2), (Add (Nb 35.0, Nb 19.3))));;
mon_expression : expr = Opp (Mult (Nb 5.2, Add (Nb 35.0, Nb 19.3)))

Image non disponible

Figure 5.2: Un arbre binaire de recherche

 
Sélectionnez
#eval mon_expression;;
- : float = -282.36

V-F-2. Arbres binaires de recherche

Afin d'illustrer l'utilisation des types paramétrés, nous allons travailler sur une structure de données très classique : celle des arbres binaires de recherche.

Un arbre binaire est un ensemble fini de nœuds qui peut être soit un ensemble vide, soit un ensemble d'au moins un nœud tel que :

  • un des nœuds constitue la racine de l'arbre ;
  • l'ensemble des nœuds restants (excluant la racine) est partitionné en deux sous-ensembles dont chacun est un arbre binaire et qui sont appelés sous-arbre gauche et sous-arbre droit de la racine.

Chaque nœud contient une information appelée étiquette de ce nœud.

La figure 5.2 présente un exemple d'arbre binaire constitué de 9 nœuds numérotés de 1 à 9 et d'étiquettes respectives : « Saturne », « Mars », « Jupiter », « Neptune », « Mercure », « Pluton », « Terre », « Uranus » et « Vénus », qui sont les noms des neuf planètes du soleil. L'arbre binaire {1, 2, 3, 4, 5, 6, 7, 8, 9} a pour racine le nœud 1 dont le sous-arbre gauche est {2, 3, 4, 5, 6} et le sous-arbre droit est {7, 8, 9}. L'arbre binaire {2, 3, 4, 5, 6} a pour racine le nœud 2 dont le sous-arbre gauche est {3} et le sous-arbre droit {4, 5, 6}. L'arbre binaire {4, 5, 6} a pour racine le nœud 4 dont le sous-arbre gauche est {5} et le sous-arbre droit est {6}. L'arbre binaire {7, 8, 9} a pour racine le nœud 7 dont le sous-arbre gauche est vide et le sous-arbre droit est l'arbre binaire {8, 9}. L'arbre binaire {8, 9} a pour racine le nœud 8 dont le sous-arbre gauche est vide et le sous-arbre droit est l'arbre {9}. Les arbres binaires {3}, {5}, {6} et {9} ont pour racines respectives les nœuds 3, 5, 6 et 9 dont les sous-arbres gauche et droit sont vides.

Une catégorie intéressante d'arbres binaires est celle des arbres binaires de recherche, car ils offrent une représentation des ensembles permettant de tester l'appartenance d'un élément à un ensemble plus rapidement qu'en parcourant séquentiellement les éléments de cet ensemble.

Un arbre binaire de recherche est un arbre binaire dont les étiquettes sont les éléments d'un ensemble muni d'une relation d'ordre total et qui est tel que pour chaque nœud d'étiquette e , les étiquettes des nœuds de son sous-arbre gauche sont toutes inférieures à e et celles des nœuds de son sous-arbre droit sont toutes supérieures à e . L'arbre binaire de la figure 5.2, par exemple, est un arbre binaire de recherche.

Pour chercher si un élément x appartient à l'ensemble E des étiquettes d'un arbre binaire de recherche R , il suffit d'appliquer l'algorithme suivant : « si R est vide alors x n'appartient pas à E , sinon, si x est égal à l'étiquette de r alors x appartient à E , sinon, si x est inférieur à l'étiquette de r alors x appartient à E s'il appartient à l'ensemble des étiquettes du sous-arbre gauche de r , sinon, si x est supérieur à l'étiquette de r alors x appartient à E s'il appartient à l'ensemble des étiquettes du sous-arbre droit de R  » .

Testons, par exemple, si « Neptune » appartient à l'ensemble Noms_planètes des étiquettes de l'arbre binaire de recherche de la figure 5.2. On se place sur le nœud 1 racine de cet arbre : « Neptune » est inférieur à l'étiquette « Saturne » du nœud 1. On descend donc sur le nœud 2 racine du sous-arbre gauche du nœud 1 : « Neptune » est supérieur à l'étiquette « Mars » du nœud 2. On descend donc sur le nœud 4 racine du sous-arbre droit du nœud 2 : « Neptune » est égal à l'étiquette du nœud 2, il appartient donc à l'ensemble Noms_planètes .

Testons maintenant si « Io » appartient à l'ensemble Noms_planètes . On se place sur le nœud 1 : « Io » est inférieur à l'étiquette « Saturne » du nœud 1. On descend donc sur le nœud 2 : « Io » est inférieur à l'étiquette « Mars » du nœud 2. On descend donc sur le nœud 3 : « Io » est inférieur à l'étiquette « Jupiter » du nœud 3. Mais le sous-arbre gauche du nœud 3 est vide : « Io » n'appartient donc pas à l'ensemble Noms_planètes .

Montrons maintenant comment représenter de tels arbres en Caml et comment les manipuler au travers de trois opérations : insertion d'un élément, test d'appartenance d'un élément à un ensemble et tri par ordre croissant des éléments de l'ensemble.

On définit tout d'abord le type 'a arbre dont les instances sont des arbres binaires de recherche dont les nœuds ont pour étiquette des valeurs de type 'a :

 
Sélectionnez
#type 'a arbre =
    | Noeud of 'a arbre * 'a * 'a arbre
    | Vide;;
Type arbre defined.

L'insertion d'un noeud dans un arbre binaire nécessite la connaissance de la relation d'ordre dont est muni l'ensemble des étiquettes de cet arbre. Nous représenterons cette relation par une fonction qui appliquée à deux étiquettes e 1 et e 2 retourne Egal si e 1 est égal à e 2 , Inf, si e 1 est inférieur à e 2 et Sup si e 1 est supérieur à e 2 . Les valeurs Inf, Egal et Sup sont les instances du type position défini par :

 
Sélectionnez
type position = Inf | Egal | Sup;;
#Type position defined.

L'insertion d'un nœud d'étiquette e dans un arbre binaire de recherche B produit un nouvel arbre binaire de recherche obtenu de la façon suivante :

  • si B est vide, on produit un arbre binaire de recherche dont la racine a l'étiquette e et un sous-arbre gauche et un sous-arbre droit vide ;
  • sinon, soit r l'étiquette de la racine de B , G le sous-arbre gauche de la racine de B et D son sous-arbre droit :
  • si e est égal à r , alors l'arbre produit est B puisque l'ensemble de ses étiquettes contient déjà e,
  • sinon, si e est inférieur à r alors l'arbre produit a une racine dont l'étiquette est r  ; le sous-arbre gauche de cette racine est le résultat de l'insertion du nœud d'étiquette e dans G et son sous-arbre droit est D,
  • sinon, si e est supérieur à r alors l'arbre produit a une racine dont l'étiquette est r  ; le sous-arbre gauche de cette racine est G et son sous-arbre droit est le résultat de l'insertion du nœud d'étiquette e dans D .

D'où la définition de la fonction insérer_noeud qui retourne l'arbre binaire de recherche obtenu en insérant le nœud d'étiquette e dans l'arbre binaire de recherche arbre muni de la relation d'ordre comp :

 
Sélectionnez
#let rec insérer_noeud comp arbre e =
    match arbre with
        | Vide -> Noeud (Vide, e, Vide)
        | Noeud (g, r, d) ->
            (match (comp e r) with
                | Inf -> Noeud ((insérer_noeud comp g e), r, d)
                | Egal -> arbre
                | Sup -> Noeud (g, r, (insérer_noeud comp d e)));;
insérer_noeud : ('a -> 'a -> position) -> 'a arbre -> 'a -> 'a arbre = <fun>

En examinant le type de la fonction insérer on constate qu'il impose bien que le type 'a de l'étiquette du noeud à insérer soit le même que le type des étiquettes de l'arbre dans lequel cet élément est inséré et le même que le type des éléments auxquels est appliquée la fonction comp.

Testons maintenant cette fonction, en construisant un arbre binaire de recherche planètes dont les étiquettes sont les noms des neuf planètes du soleil. Cet arbre sera muni de la relation d'ordre alphabétique définie par la fonction comp_chaînes définie de la façon suivante :

 
Sélectionnez
let comp_chaînes n1 n2 =
    if n1 < n2 then
        Inf
    else
        if n1 = n2 then
            Egal
        else
            Sup;;
#comp_chaînes : 'a -> 'a -> position = <fun>

L'arbre de recherche planètes est construit en insérant itérativement dans un arbre binaire de recherche initialement vide les noeuds d'étiquettes : « Saturne », « Mars  » , « Neptune », « Jupiter », « Terre », « Uranus », « Mercure », « Pluton » et « Vénus ». Ce qui donne :

 
Sélectionnez
#let planètes =
    let rec insérer_liste_noeuds comp arbre le =
        match le with
            | [] -> arbre
            | e::le -> insérer_liste_noeuds comp
                (insérer_noeud comp arbre e)
                le
    in
        insérer_liste_noeuds comp_chaînes
            Vide
            ["Saturne"; "Mars"; "Neptune"; "Jupiter";
            "Terre"; "Uranus"; "Mercure"; "Pluton";
            "Vénus"];;
planètes : string arbre =
    Noeud
        (Noeud
            (Noeud (Vide, "Jupiter", Vide), "Mars",
            Noeud
                (Noeud (Vide, "Mercure", Vide), "Neptune",
                Noeud (Vide, "Pluton", Vide))),
            "Saturne",
            Noeud (Vide, "Terre", Noeud (Vide, "Uranus", Noeud (Vide, "Vénus", Vide))))

On vérifiera facilement que l'on obtient l'arbre binaire de recherche planètes est celui de la figure 5.2.

Pour rechercher si un élément e appartient à l'ensemble E des étiquettes d'un arbre binaire de recherche B , on applique la règle suivante, qui dérive directement de la règle de construction des arbres binaires de recherche :

  • si B est vide, alors e n'appartient pas à ;
  • sinon, soit r l'étiquette de la racine de :
  • si e est égal à r alors e appartient à E,
  • sinon, si e est inférieur à r alors rechercher e dans le sous-arbre gauche de la racine de B,
  • • sinon, si e est supérieur à r alors rechercher e dans le sous-arbre droit de la racine de B .

D'où la définition de la fonction appartient qui retourne true si l'élément e appartient à l'arbre binaire de recherche arbre ou false sinon :

 
Sélectionnez
#let rec appartient comp arbre e =
    match arbre with
        | Vide -> false
        | Noeud (g, r, d) ->
            (match (comp e r) with
                | Inf -> appartient comp g e
                | Egal -> true
                | Sup -> appartient comp d e);;
appartient : ('a -> 'b -> position) -> 'b arbre -> 'a -> bool = <fun>

On peut tester cette fonction en recherchant « Neptune » puis « Io » dans l'ensemble des étiquettes de l'arbre binaire de recherche planètes :

 
Sélectionnez
#appartient comp_chaînes planètes "Neptune";;
- : bool = true
#appartient comp_chaînes planètes "Io";;
- : bool = false

On vérifie bien que « Neptune » appartient à cet ensemble et que « Io » n'y appartient pas.

Pour finir, nous allons définir une fonction trier, qui permet de construire la liste triée par ordre croissant des étiquettes de l'arbre binaire de recherche arbre. Pour réaliser ce tri, il suffit de parcourir récursivement cet arbre dans l'ordre sous-arbre gauche - racine - sous-arbre droit. La fonction trier peut donc être définie de la façon suivante :

 
Sélectionnez
#let rec trier arbre =
    match arbre with
        | Vide -> []
        | Noeud (g, r, d) ->
            (trier g) @ [r] @ (trier d);;
trier : 'a arbre -> 'a list = <fun>

Appliquons la fonction trier à l'arbre binaire de recherche planètes :

 
Sélectionnez
#trier planètes;;
- : string list =
["Jupiter"; "Mars"; "Mercure"; "Neptune"; "Pluton"; "Saturne"; "Terre";
"Uranus"; "Vénus"]

Cette application a bien pour valeur la séquence des neuf noms de planètes triée par ordre alphabétique.


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 Jacques Le Maitre. 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.