• Aucun résultat trouvé

Types sommes: unions

Les types sommes sont une généralisation des types énumérés: au lieu de définir des constantes, on définit des constructeurs qui prennent des arguments pour définir une valeur du nouveau type.

Considérons par exemple un type d'expressions contenant des constantes (définies par leur valeur

entière), des variables (définies par leur nom), des additions et des multiplications (définies par le couple de leurs deux opérandes), et des exponentiations définies par le couple d'une expression et de son

exposant. On définira le type expression par:

type expression = | Const of int | Var of string

| Add of expression * expression | Mul of expression * expression | Exp of expression * int;;

On crée des valeurs de type somme en appliquant leurs constructeurs à des arguments du bon type. Par exemple le polynôme 1 + 2x3 est représenté par l'expression:

let p = Add (Const 1, Mul (Const 2, Exp (Var "x", 3)));;

Les types somme permettent de faire du filtrage (pattern matching), afin de distinguer les cas en fonction d'une valeur filtrée. Ainsi la dérivation par rapport à une variable x se définirait simplement par:

let rec dérive x e = match e with

| Const _ -> Const 0

| Var s -> if x = s then Const 1 else Const 0 | Add (e1, e2) -> Add (dérive x e1, dérive x e2) | Mul (Const i, e2) -> Mul (Const i, dérive x e2)

| Mul (e1, e2) -> Add (Mul (dérive x e1, e2), Mul (e1, dérive x e2)) | Exp (e, i) -> Mul (Const i, Mul (dérive x e, Exp (e, i - 1)));;

Nous ne donnerons ici que la signification intuitive du filtrage sur notre exemple particulier. La

construction match du corps de dérive signifie qu'on examine la valeur de l'argument e et selon que c'est:

Const _ : une constante quelconque (_), alors on retourne la valeur . ●

Var s : une variable que nous nommons s, alors on retourne 1 ou selon que c'est la variable par ●

rapport à laquelle on dérive.

Add (e1, e2) : une somme de deux expressions que nous nommons respectivement e1 et e2, alors on retourne la somme des dérivée des deux expressions e1 et e2.

les autres cas sont analogues au cas de la somme. ●

On constate sur cet exemple la puissance et l'élégance du mécanisme. Combiné à la récursivité, il permet d'obtenir une définition de dérive qui se rapproche des définitions mathématiques usuelles.On obtient la dérivée du polynôme p = 1 + 2x3 en évaluant:

#dérive "x" p;; - : expression = Add

(Const 0,

Mul (Const 2, Mul (Const 3, Mul (Const 1, Exp (Var "x", 2)))))

On constate que le résultat obtenu est grossièrement simplifiable. On écrit alors un simplificateur (nai"f) par filtrage sur les expressions:

let rec puissance i j = match j with

| 0 -> 1 | 1 -> i

| n -> i * puissance i (j - 1);;

let rec simplifie e = match e with

| Add (Const 0, e) -> simplifie e

| Add (Const i, Const j) -> Const (i + j)

| Add (e, Const i) -> simplifie (Add (Const i, e)) | Add (e1, e2) -> Add (simplifie e1, simplifie e2) | Mul (Const 0, e) -> Const 0

| Mul (Const 1, e) -> simplifie e

| Mul (Const i, Const j) -> Const (i * j)

| Mul (e, Const i) -> simplifie (Mul (Const i, e)) | Mul (e1, e2) -> Mul (simplifie e1, simplifie e2) | Exp (Const 0, j) -> Const 0

| Exp (Const 1, j) -> Const 1

| Exp (Const i, j) -> Const (puissance i j) | Exp (e, 0) -> Const 1

| Exp (e, 1) -> simplifie e

| Exp (e, i) -> Exp (simplifie e, i) | e -> e;;

Pour comprendre le filtrage de la fonction simplifie, il faut garder à l'esprit que l'ordre des clauses Types sommes: unions

est significatif puisqu'elles sont essayées dans l'ordre. Un exercice intéressant consiste aussi à prouver formellement que la fonction simplifie termine toujours.

On obtient maintenant la dérivée du polynôme p = 1 + 2x3 en évaluant:

#simplifie (dérive "x" p);;

- : expression = Mul (Const 2, Mul (Const 3, Exp (Var "x", 2)))

Next: Types produits: enregistrements Up: Quelques éléments de Caml Previous: Types sommes: types énumérés

1/11/1998

Next: Types abréviations Up: Quelques éléments de Caml Previous: Types sommes: unions

Types produits: enregistrements

Les enregistrements (records en anglais) permettent de regrouper des informations hétérogènes. Ainsi, on déclare un type date comme suit:

type mois =

| Janvier | Février | Mars | Avril | Mai | Juin | Juillet | Aout | Septembre | Octobre | Novembre | Décembre;;

type date = {j: int; m: mois; a: int};;

let berlin = {j = 10; m = Novembre; a = 1989} and bastille = {j = 14; m = Juillet; a = 1789};;

Un enregistrement contient des champs de type quelconque, donc éventuellement d'autres

enregistrements. Supposons qu'une personne soit représentée par son nom, et sa date de naissance; le type correspondant comprendra un champ contenant un enregistrement de type date:

type personne = {nom: string; naissance: date};;

let poincaré =

{nom = "Poincaré"; naissance = {j = 29; m = Avril; a = 1854}};;

Les champs d'un enregistrement sont éventuellement mutables, c'est-à-dire modifiables par affectation. Cette propriété est spécifique à chaque champ et se déclare lors de la définition du type, en ajoutant le mot clé mutable devant le nom du champ. Pour modifier le champ label du record r en y déposant la valeur v, on écrit r.label <- v.

#type point = {mutable x : int; mutable y : int};; Le type t est défini.

#let origine = {x = 0; y = 0};; origine : point = {x = 0; y = 0} #origine.x <- 1;; - : unit = () #origine;; - : point = {x = 1; y = 0}

En combinaison avec les types somme, les enregistrements modélisent des types de données complexes: Types produits: enregistrements

type complexe =

| Cartésien of cartésiennes | Polaire of polaires

and cartésiennes = {re: float; im: float} and polaires = {rho: float; theta: float};;

let pi = 4.0 *. atan 1.0;;

let x = Cartésien {re = 0.0; im = 1.0}

and y = Polaire {rho = 1.0; theta = pi /. 2.0};;

Une rotation de s'écrit alors:

let rotation_pi_sur_deux = function

| Cartésien x -> Cartésien {re = -. x.im; im = x.re}

| Polaire x -> Polaire {rho = x.rho; theta = x.theta +. pi /. 2.0};;

1/11/1998

Next: Types abstraits Up: Quelques éléments de Caml Previous: Types produits: enregistrements

Types abréviations

On donne un nom à une expression de type à l'aide d'une définition d'abréviation. C'est quelquefois utile pour la lisibilité du programme. Par exemple:

type compteur == int;;

définit un type compteur équivalent au type int.

1/11/1998 Types abréviations

Next: Égalité de types Up: Quelques éléments de Caml Previous: Types abréviations

Types abstraits

Si, dans l'interface d'un module (voir ci-dessous), on exporte un type sans rien en préciser (sans donner la liste de ses champs s'il s'agit d'un type enregistrement, ni la liste de ses constructeurs s'il s'agit d'un type somme), on dit qu'on a abstrait ce type, ou qu'on l'a exporté abstraitement. Pour exporter abstraitement le type t, on écrit simplement

type t;;

L'utilisateur du module qui définit ce type abstrait n'a aucun moyen de savoir comment le type t est implémenté s'il n'a pas accès au source de l'implémentation du module. Cela permet de changer cette implémentation (par exemple pour l'optimiser) sans que l'utilisateur du module n'ait à modifier ses propres programmes. C'est le cas du type des piles dans l'interface du module stack décrit ci-dessous.

1/11/1998 Types abstraits

Next: Structures de données polymorphes Up: Quelques éléments de Caml Previous: Types abstraits

Égalité de types

La concordance des types se fait par nom. Les définitions de type sont qualifiées de génératives, c'est-à-dire qu'elles introduisent toujours de nouveaux types (à la manière des let emboîtés qui

introduisent toujours de nouveaux identificateurs). Ainsi, deux types sont égaux s'ils font référence à la même définition de type.

Attention: ce phénomène implique que deux types de même nom coexistent parfois dans un programme. Dans le système interactif, cela arrive quand on redéfinit un type qui était erroné. Le compilateur ne confond pas les deux types, mais il énonce éventuellement des erreurs de type bizarres, car il n'a pas de moyen de nommer différemment les deux types. Étrangement, il indique alors qu'un type t (l'ancien) n'est pas compatible avec un type t (mais c'est le nouveau). Considérons les définitions

#type t = C of int;; Le type t est défini. #let int_of_t x =

match x with C i -> i;; int_of_t : t -> int = <fun>

Jusque là rien d'anormal. Mais définissons t à nouveau (pour lui ajouter un nouveau constructeur par exemple): l'argument de la fonction int_of_t est de l'ancien type t et on ne peut pas l'appliquer à une valeur du nouveau type t. (Voir aussi l'URL

http://pauillac.inria.fr/caml/FAQ/FAQ_EXPERT-fra.html.) #type t = C of int | D of float;;

Le type t est défini. #int_of_t (C 2);;

Entrée interactive: >int_of_t (C 2);; > ^^^

Cette expression est de type t, mais est utilisée avec le type t.

Ce phénomène se produit aussi avec le compilateur indépendant (en cas de gestion erronée des

dépendances de modules). Si l'on rencontre cet étrange message d'erreur, il faut tout recommencer; soit quitter le système interactif et reprendre une nouvelle session; soit recompiler entièrement tous les modules de son programme.

Next: Structures de données polymorphes Up: Quelques éléments de Caml Previous: Types abstraits Égalité de types

1/11/1998 Égalité de types

Next: Modules Up: Quelques éléments de Caml Previous: Égalité de types

Structures de données polymorphes

Toutes les données ne sont pas forcément d'un type de base. Certaines sont polymorphes c'est-à-dire qu'elles possèdent un type dont certaines composantes ne sont pas complètement déterminées mais comporte des variables de type. L'exemple le plus simple est la liste vide, qui est évidemment une liste d'entiers (int list) ou une liste de booléens (bool list) et plus généralement une liste de

``n'importe quel type'', ce que Caml symbolise par 'a (et la liste vide polymorphe est du type 'a list).

On définit des structures de données polymorphes en faisant précéder le nom de leur type par la liste de ses paramètres de type. Par exemple:

type 'a liste = | Nulle

| Cons of 'a * 'a liste;;

ou encore pour des tables polymorphes:

type ('a, 'b) table = {nb_entrées : int; contenu : ('a * 'b) vect};;

Les listes prédéfinies en Caml forment le type list et sont équivalentes à notre type liste. La liste vide est symbolisée par [] et le constructeur de liste est noté ::, et correspond à notre constructeur Cons. Pour plus de commodité, l'opérateur :: est infixe: x :: l représente la liste qui commence par x, suivi des éléments de la liste l. En outre, on dispose d'une syntaxe légère pour construire des listes littérales: on écrit les éléments séparés par des point-virgules, le tout entre crochets [ et ].

#let l = [1; 2; 3];; l : int list = [1; 2; 3] #let ll = 0 :: l;;

ll : int list = [0; 1; 2; 3]

Les listes sont munies de nombreuses opérations prédéfinies, dont les fonctionnelles de parcours ou d'itération, map, do_list ou it_list, ou encore la concaténation @. À titre d'exemple emblématique de fonction définie sur les listes, nous redéfinissons la fonctionnelle map qui applique une fonction sur tous les éléments d'une liste.

#let print_list l = do_list print_int l;; print_list : int list -> unit = <fun>

#print_list l;; 123- : unit = () #let rec map f l =

Structures de données polymorphes

match l with | [] -> []

| x :: rest -> f x :: map f rest;;

map : ('a -> 'b) -> 'a list -> 'b list = <fun> #let succ_l = map (function x -> x + 1) ll;; succ_l : int list = [1; 2; 3; 4]

#print_list succ_l;; 1234- : unit = ()

#let somme l = it_list (function x -> function y -> x + y) 0 l;; somme : int list -> int = <fun>

#somme ll;; - : int = 6 #somme succ_l;; - : int = 10

La manipulation des listes est grandement facilitée par la gestion automatique de la mémoire, puisque l'allocation et la désallocation sont prises en charge par le gestionnaire de mémoire et son programme de récupération automatique des structures devenues inutiles (le ramasse-miettes, ou glaneur de cellules, ou GC (garbage collector)).

Documents relatifs