Cours 4

Htmligure
Version & licenses
Creative Commons License

Types inductifs

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Les types inductifs

Pour les types sommes comme pour les types produits, nous avons dit que les informations associés aux champs ou aux constructeurs peuvent être de n'importe quel type. Il est même possible d'utiliser le type en train d'être défini, afin de définir un type inductif. Ce n'est pas spécialement original, par exemple pour faire une liste simplement chainée en Java, on écrirait :

  1. class LinkedList<Elt> {
  2. Elt head;
  3. LinkedList<Elt> tail;

  4. public LinkedList(Elt head, LinkedList<Elt> tail) {
  5. this.head = head;
  6. this.tail = tail;
  7. }

La classe LinkedList contient donc une référence à elle-même.

La même construction est possible en OCaml avec un type somme et un type produit mutuellement récursifs :

  1. type 'elt custom_list =
  2. | Nil
  3. | NonEmptyList of 'elt non_empty_list

  4. and 'elt non_empty_list =
  5. { head: 'elt;
  6. tail : 'elt custom_list
  7. }

  8. (* or with OCaml 4.03 and above: *)
  9. type 'elt custom_list =
  10. | Nil
  11. | NonEmptyList of { head: 'elt; tail : 'elt custom_list }

Contrairement à Java, la liste vide est explicitement définie comme étant une valeur propre au type des listes, plutôt que de réutiliser le pointeur nul. L'avantage est qu'OCaml nous force ensuite à prendre en considération le cas où une liste est Nil

La véritable définition des listes est cependant un peu plus simple : on utilise une paire plutôt qu'un type produit, ce qui réduit la verbosité :

  1. type 'elt list =
  2. | []
  3. | (::) of 'elt * 'elt list

Les arbres binaires sont un autre exemple classique :

  1. type 'node bin_tree =
  2. | Leaf
  3. | Node of ('node bin_tree * 'node * 'node bin_tree)

Un arbre binaire est soit une feuille, soit un nœud avec deux sous-arbres.

Comprendre les types inductives.

Les constructions inductives sont une technique formelle pour construire des ensembles infinis de valeurs. On définit une ou plusieurs valeurs élémentaires, et une ou plusieurs opérations de construction par combinaison de valeurs de l'ensemble, et on définit alors l'ensemble des valeurs qui peuvent être ainsi construite.

On peut construire beaucoup de choses ainsi. Par exemple, les entiers naturels :

  • Zero est un entier naturel,
  • Succ n est un entier naturel si n est un entier naturel.

L'ensemble des entiers naturels est alors le plus petit ensemble qui vérifie ces deux propriétés. Nous avons que $0 :=$ Zero est un entier par la première règle, $1 :=$ Succ Zero est un entier par la deuxième règle, $2 :=$ Succ (Succ Zero) est un entier par la deuxième règle aussi en prenant $n = 1$, etc. Donc tout ensemble vérifiant ses deux règles contient les entiers naturels (ou en tout cas une représentation des entiers naturels).

Mais il y a d'autres ensembles vérifiant ces deux règles, par exemple les nombres relatifs, et même des ensembles très exotiques. Mais une définition inductive définit le plus petit ensemble, autrement dit seulement les valeurs que les règles nous obligent de créer. Il se trouve qu'il existe un seul ensemble minimal de valeurs vérifiant ces deux règles, et c'est cet ensemble que la définition inductive définit.

La définition des listes est presque identique (ce n'est pas une coïncidence) :

  • la liste vide [] est une liste d'entiers,
  • si head est un entier et tail est une liste d'entiers, head::tail est une liste d'entiers.

et pour les arbres :

  • Leaf est un arbre binaire indicé par les entiers,
  • si i est un entier, que left et right sont des arbres binaires indicés par les entiers, Node (Left,i,right) est un arbre binaire indicé par un entier.

La définition de ces types en OCaml est une réécriture dans la syntaxe du langage de ces définitions inductives. Les définitions inductives permettent de définir quasiment toutes les données que nous pourrions souhaiter manipuler. En informatique les définitions inductives sont fréquentes, par exemple, les grammaires hors-contextes (telles que les grammaires en Backus-Naur Form BNF qui permettent de décrire la syntaxe d'un langage de programmation) sont des définitions inductives d'ensembles de termes, et c'est l'une des applications des types inductifs.

Pourquoi des types inductifs ?

Les types inductifs ont en OCaml deux principales applications.

Leur premier usage est pour la définition de structures de données. Nous avons par exemple déjà vu les listes et les arbres binaires, et nous savons depuis les cours d'algorithmique que les listes et les arbres sont les deux principales composantes de la plupart des structures de données. Nous pouvons facilement définir plusieurs types d'arbres :

  1. (* Binary trees *)
  2. type 'elt bin_tree =
  3. | BinLeaf
  4. | BinNode of 'elt bin_tree * 'elt * 'elt bin_tree

  5. (* Ternary trees *)
  6. type 'elt bin_tree =
  7. | TernLeaf
  8. | TernNode of 'elt * 'elt bin_tree * 'elt bin_tree * 'elt bin_tree

  9. (* (1,2)-arity trees *)
  10. type 'elt ot_tree =
  11. | OtLeaf
  12. | OtSingleNode of 'elt * 'elt ot_tree
  13. | OtDoubleNode of 'elt * 'elt ot_tree

  14. (* arbitrary trees *)
  15. type 'elt tree =
  16. | Leaf
  17. | Node of 'elt * 'elt tree list

  18. (* unary trees = list *)
  19. type 'elt list =
  20. | []
  21. | (::) of 'elt * 'elt list

Il est donc assez facile d'adapter la définition générique d'arbre pour obtenir diverses variantes selon les besoins. On peut aussi facilement ajouter des informations dans les nœuds, éventuellement en utilisant un type produit adapté, pour éviter de manipuler des tuples trop longs.

Le deuxième usage des types inductifs est de représenter des arbres syntaxiques. Nous avons mentionné que les grammaires hors-contextes sont des définitions inductives de l'ensemble des termes d'un langage. Il n'est donc pas étonnant que les arbres syntaxiques, qui représentent des dérivations de la grammaire, se représentent naturellement par des types inductifs. Considérons l'exemple des expressions arithmétiques, avec l'addition et la multiplication sur les entiers :

  1. type arithm =
  2. | Literal of int
  3. | Sum of arithm * arithm
  4. | Product of arithm * arithm

  5. let expr_zero = Sum (Product (Literal 3, Literal 2), Literal (-6))

  6. let (++) term1 term2 = Sum (term1, term2)
  7. let ( ** ) factor1 factor2 = Product (factor1, factor2)

  8. let expr_eight = (Literal 2) ** (Literal 2) ++ (Literal 3) ++ (Literal 1)
  9. val expr_eight : arithm =
  10. Sum (Sum (Product (Literal 2, Literal 2), Literal 3), Literal 1)

Une expression arithmétique est soit un entier, soit une somme de deux termes, soit un produit de deux facteurs. Le type arithpermet de représenter toutes les expressions arithmétiques pouvant être construite par sommes et produits de littéraux.

Un exemple plus complexe est l'arbre syntaxique des programmes OCaml. La librairie standard contient un module ParseTreequi expose la représentation qu'utilise le compilateur pour les programmes, et qui utilise des types sommes et produits mutuellement récursifs. Ce n'est qu'une version extrêmement développée de l'exemple des expression arithmétiques.

D'autres applications des types inductifs sont à mi-chemin entre structure de données et arbres syntaxiques. La librairie yojson utilise un type inductifs pour représenter des données au format json.

  1. type json = [
  2. | `Assoc of (string * json) list
  3. | `Bool of bool
  4. | `Float of float
  5. | `Int of int
  6. | `List of json list
  7. | `Null
  8. | `String of string
  9. ]

Si on ignore les crochets et les accents graves bizarres, que nous expliquerons bientôt, il s'agit d'un type inductif. De même on peut décrire les données au format XML par un type inductif. On peut aussi représenter des calculs à effectuer, par leur arbre syntaxique, qui pourront être évalués ultérieurement, ce qui permet de retarder le calcul. Beaucoup de librairies utilisent cette technique pour exposer une interface fonctionnelle et efficace. Par exemple Vg, la librairie de dessin vectoriel, ne dessine rien pendant la construction du dessin, mais enregistre en quelque sorte les opérations de construction à l'aide de types algébriques. C'est seulement lors du rendu, en svg ou en pdf, que la construction est interprétée et le dessin réalisé.

Retour au sommaire.