Cours 2

Htmligure
Version & licenses
Creative Commons License

Les types paramétrés

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Les types paramétrés.

Nous avons déjà vu l'existence de deux sortes de types :

  • les types élémentaires : int, float, etc.
  • les types construits à partir de types plus simples : int -> int, (float * float), etc.

Dans cette section, nous étudions cette deuxième sorte de types, qui sont appelés types paramétré.

Exemple introductif.

Imaginons une petite base de données, décrite par l'interface suivante :

  1. module Entry :
  2. sig
  3. type t
  4. val create : name:string -> nickname:string -> age:int -> t
  5. val get_name : t -> string
  6. val get_nickname : t -> string
  7. val get_age : t -> int
  8. end

  9. module Directory :
  10. sig
  11. type t
  12. val empty : t
  13. val add : Entry.t -> t -> t
  14. val sort_by_age : t -> t
  15. val sort_by_name : t -> t
  16. val find_age : name:string -> t -> int
  17. val find_nickname : name:string -> t -> string
  18. end

Nous avons un module Entry qui permet de représenter une entrée du répertoire, le type d'une entrée est Entry.t(nous ne connaissons pas sa représentation interne). Le type Directory.t représente les répertoires, c'est-à-dire des collections d'entrées. Là aussi, nous ne connaissons pas sa représentation interne.

On peut créer un petit répertoire ainsi :

  1. let my_dir =
  2. let open Directory in
  3. empty
  4. |> add (Entry.create ~name:"Alice" ~nickname:"Lily" ~age:21)
  5. |> add (Entry.create ~name:"Robert" ~nickname:"Bob" ~age:23)
  6. |> add (Entry.create ~name:"Christopher" ~nickname:"Chris" ~age:20)

Ce qui a pour effet d'afficher dans l'interpréteur :

  1. val my_dir : Directory.t =
  2. [ (name: Christopher, nickname: Chris, age: 20);
  3. (name: Robert, nickname: Bob, age: 23);
  4. (name: Alice, nickname: Lily, age: 21)
  5. ]

On peut ensuite par exemple réordonner les entrées :

  1. # let my_sorted_dir = Directory.sort_by_name my_dir;;
  2. val my_sorted_dir : Directory.t =
  3. [ (name: Alice, nickname: Lily, age: 21);
  4. (name: Christopher, nickname: Chris, age: 20);
  5. (name: Robert, nickname: Bob, age: 23)
  6. ]

ou bien chercher une entrée :

  1. # Directory.find_age ~name:"Alice" my_sorted_dir;;
  2. - : int = 21

Ce petit exemple possède déjà quelques gros défauts. Par exemple, que se passe-t-il si on cherche l'âge d'une personne qui n'est pas dans le répertoire ? La fonction Directory.find_age retourne un entier, mais si la personne n'est pas dans le répertoire elle n'aura rien à retourner. On voudrait donc avoir comme type Directory.find_age : name:string -> Directory.t -> int_or_nothingint_or_nothing serait un type adapté.

Cela résoudrait le problème de Directory.find_age, mais on serait obligé de procéder de même pour la fonction Directory.find_nickname, il faudrait qu'elle retourne une valeur de type string_or_nothing.

De façon plus générale, on pourrait imaginer avoir pour tout type t besoin d'un type t_or_nothing. Mais on ne souhaite pas avoir à écrire chaque type deux fois. En Java, la solution serait de faire une classe générique pour représenter une valeur ou l'absence d'une valeur (cette classe existe dans Java 8 , c'est la classe Optional).

En OCaml, la solution va être de créer un type, qu'on peut appeler provisoirement or_nothing, qui dépend d'un autre type t, ce que l'on notera t or_nothing. Ici or_nothing définit en réalité non pas un type, mais une famille de types distincts : int or_nothing, string or_nothing, (float or_nothing) or_nothing, (int -> int) or_nothing, etc. Un tel type s'appelle type paramétré : or_nothing dépend d'un paramètre, qui peut être un type quelconque.

Le véritable nom du type or_nothing en OCaml est option. On peut donc corriger l'interface du module Directory ainsi :

  1. module Directory :
  2. sig
  3. type t
  4. val empty : t
  5. val add : Entry.t -> t -> t
  6. val sort_by_age : t -> t
  7. val sort_by_name : t -> t
  8. val find_nickname : name:string -> t -> string option
  9. val find_age : name:string -> t -> int option
  10. end

Et on obtient alors :

  1. # Directory.find_age ~name:"Alice" my_dir;;
  2. - : int option = Some 21
  3. # Directory.find_age ~name:"David" my_dir;;
  4. - : int option = None

Un autre problème est qu'on peut trier le répertoire par nom et par âge, mais pas par surnom. C'est un choix qui semble arbitraire. On pourrait vouloir trier selon n'importe quelle fonction de comparaison sur les entrées. En OCaml comme dans de nombreux langages, les fonctions de comparaisons prennent deux arguments et retourne un entier, on pourrait donc mettre dans le module Directory une fonction du type :

  1. val sort : (Entry.t -> Entry.t -> int) -> t -> t

Ce n'est malheureusement pas très lisible. On pourrait donc introduire un nouveau type pour représenter les fonctions de comparaisons. Comme on peut faire des fonctions de comparaisons pour n'importe quel type, il semble naturel de faire un type paramétré par le type des valeurs que l'on souhaite comparer, ce qui nous amène à la solution :

  1. type 'value comparison = 'value -> 'value -> int

Et remplacer les fonctions de tri de Directory par

  1. val sort : by:(Entry.t comparison) -> t -> t

Nous continuerons à développer les comparaisons dans la suite de ce cours. Continuons donc sur l'exemple du répertoire de noms en se posant la question de comment représenter le répertoire lui-même, c'est-à-dire la collection des entrées ? Il nous faut une structure de données pour regrouper toutes les entrées, par exemple un tableau. En OCaml, le type adapté est celui de liste, il nous faut donc une liste d'entrées. On imagine bien que si sur ce problème on veut avoir une collection d'entrées, sur un autre problème on voudra une collection d'entiers, de chaînes de caractères, ou bien d'un type quelconque adapté aux données. Là encore, on ne veut pas avoir à définir un nouveau type à chaque fois qu'on trouve une nouvelle application pour les listes. Le type des listes est donc paramétré par le contenu de la liste. On trouve donc les types int list, bool list, Entry.t list, (float list) list, etc.

Syntaxe.

Comme dans les exemples que nous venons de voir, les types paramétrés s'écrivent sous la forme :

  1. parameter name_of_parametrized_type

On peut créer un nouveau type paramétré grâce au mot-clé type. Le paramètre est nécessairement une variable de type (donc commence par une apostrophe), puisqu'il s'agit de l'abstraction d'un type (tout comme les variables sont des abstractions de valeurs).

  1. type 'param type_name = type_expression

Il est possible de faire des types avec plusieurs paramêtres, en utilisant des parenthèses pour regrouper les paramètres et des virgules pour les séparer :

  1. type ('param1,'param2) type_name = type_expression

Regardons quelques exemples :

  1. type ('left,'right) pair = ('left * 'right)

  2. type ('domain,'image) funct = 'domain -> 'image

  3. type 'elt list

  4. type 'value option

Les deux premiers montrent que les paires (plus généralement les tuples) et les fonctions sont des types paramétrés, avec deux paramètres. Simplement ils n'obéissent pas à la syntaxe usuelle de mettre la liste des paramètres suivi du nom du type, mais utilisent un opérateur infixe ('*' ou '->').

Les deux exemples suivants sont tirés de la librairie standard. Les listes sont les conteneurs de bases, elles ont un rôle similaire aux tableaux des langages procéduraux (mais ne s'utilisent pas de la même façon). Nous apprendrons à les utiliser plus tard. Les options, comme nous l'avons vu, codent l'absence ou la présence d'une valeur.

Notons que les types paramétrés permettent de profiter pleinement du polymorphisme : en définissant une fonction polymorphe en 'elt sur les listes 'elt list, on définit cette fonction pour tous les types possibles de listes, en une seule fois.

La plupart des librairies en OCaml utilisent d'une façon ou d'une autre des types paramétrés. Ils existent plusieurs grandes raisons de faire des types paramétrés, et nous allons donc les catégoriser dans la suite du cours, selon leur usage.

Retour au sommaire.