Type algébrique de donnéesUn type algébrique est une forme de type de données composite[note 1], qui combine les fonctionnalités des types produits (n‐uplets ou enregistrements) et des types sommes (union disjointe). Combinée à la récursivité, elle permet d’exprimer les données structurées telles que les listes et les arbres. DéfinitionsType produitLe type produit de deux types A et B est l’analogue en théorie des types du produit cartésien ensembliste et est noté A × B. C’est le type des couples dont la première composante est de type A et la seconde de type B. Deux fonctions canoniques lui sont associées, appelées projections, donnant la valeur de chaque composante. Exemple : type produit en OCaml :
On peut définir en langage OCaml le type d’une entrée de dictionnaire : type dict_entry = string * int
let entry = ("clé", 37)
(* get_value : dict_entry -> int *)
let get_value (key, value) = value
Le produit se généralise naturellement à un nombre quelconque d’opérandes, pour donner des types de n‐uplets. Dans le cas particulier du produit vide, le type des 0‐uplets est nommé type unité et noté 1 : c’est l’élément neutre du produit et il contient une unique valeur, souvent notée (). Des considérations pratiques amènent souvent à nommer les composantes[note 2]. Dans ce contexte, le type est souvent appelé structure et ses valeurs des enregistrements ; les composantes sont appelées membres, et la projection selon le membre Exemple : structure en OCaml :
Toujours en OCaml, l’exemple précédent s’adapte ainsi : type dict_entry = {
key : string ;
value : int ;
}
let entry = { key = "clé" ; value = 37 }
(* get_value : dict_entry -> int *)
let get_value entry = entry.value
Exemple : structure en langage C :
Cette fonctionnalité se traduit en langage C par le mot‐clé typedef struct {
char* key ;
int value ;
} dict_entry ;
dict_entry entry = { .key = "clé", .value = 37 } ;
int get_value (dict_entry entry) {
return entry.value ;
}
Type sommeLe type somme de deux types A et B est l’analogue en théorie des types de l’union disjointe ensembliste et est noté A + B. Il représente un type contenant toutes les valeurs de chacun des deux types A et B, de telle sorte qu’une valeur issue de A ne puisse pas être confondue avec une valeur issue de B (même si A = B). En théorie des ensembles, on représenterait la somme par l’ensemble {1}×A ∪ {2}×B ; la première composante (1 ou 2) d’un tel objet est une étiquette qui indique si cet objet se trouve dans le bras de gauche (A) ou dans le bras de droite (B) de la somme. Les analogues en théorie des types des expressions (1, a) et (2, b) sont souvent notés ι1 a et ι2 b (ι est la lettre grecque iota). Ces notations ι1 et ι2 peuvent être vues comme des fonctions injectives, respectivement de A dans A + B et de B dans A + B, qui permettent de construire les valeurs de la somme, d’où leur nom de constructeurs. Dans ι1 a, la valeur a est appelée l’argument du constructeur ι1. Traiter des valeurs d’un type somme requiert un raisonnement par cas, nommé dans ce contexte filtrage par motif. Chaque bras — qu’on reconnaît par son constructeur et dont on peut récupérer la valeur puisque ce constructeur est injectif — fait l’objet d’un cas séparé. Exemple : type somme en OCaml :
On peut définir on OCaml l’union disjointe des nombres entiers et des nombres flottants et définir par filtrage une fonction sur cette union : type sum = Int of int | Float of float
(* print : sum -> unit *)
let print = function
| Int i -> Printf.printf "%i" i
| Float f -> Printf.printf "%f" f
Ici, les constructeurs sont nommés Exemple : type « union » en langage C :
Cette fonctionnalité s’approxime en langage C avec le mot clé typedef struct {
enum { INT, FLOAT } tag ;
union {
int i ;
float f ;
} ;
} sum_t ;
void print (sum_t x) {
if (x.tag == INT)
printf("%i", x.i) ;
else if (x.tag == FLOAT)
printf("%f", x.f) ;
}
Exemple : type somme en Rust :
Le langage Rust supporte nativement les types somme avec le mot clef enum Sum {
Int(i32),
Float(f64),
}
fn print(x: Sum) {
match x {
Sum::Int(i) => println!("Int: {i}"),
Sum::Float(f) => println!("Float: {f}"),
}
}
// Utilisation
let x = Sum::Int(10);
print(x);
La somme se généralise naturellement à un nombre quelconque d’opérandes. Dans le cas particulier de la somme vide, le type est nommé type vide et noté 0 : c’est l’élément neutre de la somme (et élément absorbant du produit) et il ne contient aucune valeur. Type énuméréUn type énuméré représente un ensemble fini, dont les éléments sont les différents constructeurs. Définir une fonction dessus revient à définir l’image de chaque élément, individuellement. Exemple : type énuméré en OCaml :
On peut par exemple coder l’ensemble des quatre couleurs d’un jeu de cartes classique : type couleur = Coeur | Carreau | Trefle | Pique
(* nom_de_la_couleur : couleur -> string *)
let nom_de_la_couleur = function
| Coeur -> "♥ cœur"
| Carreau -> "♦ carreau"
| Trefle -> "♣ trèfle"
| Pique -> "♠ pique"
Exemple : type énuméré en langage C :
Cette fonctionnalité se traduit en langage C par le mot‐clé typedef enum { COEUR, CARREAU, TREFLE, PIQUE } couleur ;
char* nom_de_la_couleur (couleur c) {
switch (c) {
case COEUR : return "♥ cœur" ;
case CARREAU : return "♦ carreau" ;
case TREFLE : return "♣ trèfle" ;
case PIQUE : return "♠ pique" ;
}
}
Type algébriqueUn type algébrique est une somme de produits, et généralise donc ces deux notions. Ainsi, des cas spéciaux de types algébriques sont les types produits (un seul constructeur), les types sommes (un seul argument pour chaque constructeur) et les types énumérations (plusieurs constructeurs sans argument). Exemple : type option et type résultat :
Les types options (en) sont des applications courantes de types algébriques. Ils permettent d’ajouter à un type donné une valeur spéciale, considérée comme « indéfinie » ou comme valeur d’erreur (l’équivalent de La valeur spéciale est représentée par un constructeur type int_option = None | Some of int
(* division : int -> int -> int_option *)
let division x y =
if y = 0 then
None
else
Some (x / y)
On peut perfectionner le mécanisme en agrémentant le cas d’erreur d’un message de description (donnée de type type int_result = Result of int | Error of string
(* division : int -> int -> int_result *)
let division x y =
if y = 0 then
Error "division by zero"
else
Result (x / y)
PolymorphismeDans les langages qui les supportent, les types algébriques peuvent être (paramétriquement) polymorphes, ce qui permet la programmation générique. Ainsi, la définition d’un type algébrique peut être paramétrée par des variables de types. On peut alors définir des fonctions génériques agissant sur de tels types polymorphes. Exemple : type option polymorphe :
On peut rendre polymorphe la définition du type option vue précédemment. Ça s’écrit ainsi en langage OCaml (où type 'a option = None | Some of 'a
(** Utilisation d’instances du type polymorphe : **)
(* int_division : int -> int -> int option *)
let int_division x y =
if y = 0 then
None
else
Some (x / y)
(* float_division : float -> float -> float option *)
let float_division x y =
if y = 0.0 then
None
else
Some (x /. y)
(** Définition d’une fonction générique : **)
(* get_value : 'a -> 'a option -> 'a *)
let get_value default_value optional_value =
match optional_value with
| None -> default_value
| Some value -> value
Type algébrique généraliséRécursivitéListesUn des exemples les plus importants de type algébrique est le type liste, défini de façon récursive par deux constructeurs :
Par exemple, Cons 1 (Cons 2 (Cons 3 (Cons 4 Nil))), aussi noté Toutes les opérations sur les listes se définissent alors par récurrence, en utilisant le filtrage par motif. Par exemple, pour calculer la longueur d’une liste :
Exemple : type liste en OCaml :
Cette définition se traduit ainsi en langage OCaml : type 'a list =
| Nil
| Cons of 'a * 'a list
let list1234 = Cons 1 (Cons 2 (Cons 3 (Cons 4 Nil)))
let rec length = function
| Nil -> 0
| Cons x s -> 1 + length s
ArbresLes types algébriques permettent également de définir des structures d’arbres. Un arbre binaire peut se construire au moyen de deux constructeurs :
Par exemple, Node 1 (Node 2 (Leaf 4) (Node 5 (Leaf 6) (Leaf 7) ) ) (Leaf 3) est l’arbre suivant : 1 / \ 2 3 / \ 4 5 / \ 6 7 Comme pour les listes, les opérations sur les arbres se définissent par récurrence. Par exemple, pour calculer la hauteur d’un arbre :
Exemple : type arbre binaire en OCaml :
Cette définition se traduit ainsi en langage OCaml : type tree =
| Leaf of int
| Node of tree * int * tree
let my_tree = Node 1 (Node 2 (Leaf 4) (Node 5 (Leaf 6) (Leaf 7))) (Leaf 3)
let rec height = function
| Leaf e -> 1
| Node e l r -> 1 + max (height l) (height r)
AbstractionUn type algébrique peut être abstrait : il suffit pour ça de ne pas exposer sa structure interne (ses constructeurs et leurs divers champs). Ainsi, il ne peut être manipulé que par les fonctions prévues à cet effet, et son implémentation peut être changée. C’est une technique fréquente car les types algébriques permettent de réaliser des structures de données complexes. Voir aussiNotes & référencesNotes
Références
|