Le polymorphisme paramétrique, c'est le fait qu'une valeur donnée d'une fonction puisse, sur plusieurs appels différents de la fonction, avoir des types différents.
L'idée se voit très bien avec l'inférence de type ; mettons que je code la fonction identité (ou '
id'), qui ne fait par définition
rien à son argument, puisqu'elle le renvoie
à l'identique (on dirait pas mais cette fonction est centrale en maths : si, si) :
CitationSoit identité x = x
En OCaml, cela s'écrit ainsi :
Code : Autre
Comment le compilateur va-t-il deviner le type de la variable x ? Justement, il ne peut pas trop : les fonctions auxiliaires qui pourraient l'aider (comme
+ et
* dans l'autre programme, qui permettent de comprendre qu'on manipule des nombres) ont disparu !
On est donc dans un cas où le compilateur ne "peut pas deviner" le type de la fonction. C'est dans ces cas-là qu'on rencontre le
polymorphisme : le compilateur déclare "x est de type quelconque inconnu, que j'appellerai
'a", et il fonctionne avec le type
'a comme s'il s'agissait d'un type qu'il connaît.
Voici ce que répond le compilateur quand on lui demande le type de notre fonction id :
Citationval id : 'a -> 'a
Il a donc donné un nom au type inconnu qu'on lui avait présenté.
L'avantage du polymorphisme va plus loin : en plus d'être inconnu, ce type est quelconque : n'importe quelle valeur du langage pourrait être donnée à la fonction
id.
Ainsi, si je donne un entier à
id (par exemple '
id 3' renvoie 3),
id va rester de type
'a -> 'a (ce n'est pas parce qu'on lui a donné un entier une fois qu'elle devient de type
int -> int). On pourra toujours, dans la suite du programme, lui donner une chaîne de caractères, un tableau d'entiers, etc.
On a donc ajouté de la richesse à notre système de types : en plus d'avoir des types particuliers, on a aussi des types "généralistes" qui permettent d'être employés sur tous les types.
Vous allez me dire "ça existe aussi en C, on a
(void *)". Il y a une différence notoire entre les deux : si vous construisez par exemple une liste chaînée dont le champ de données est de type
void* (pour pouvoir y mettre n'importe quoi), quand vous mettez une valeur dedans, elle devient de type
void* et
perd son information de typage. Il est alors possible de la manipuler comme toutes les autres données de type
void*, et à ce niveau-là de faire des erreurs.
Par exemple, si vous avez une liste chaînée qui contient des
strings, et l'autre qui contient des tableaux d'entiers, les données seront forcées de devenir des
void* en étant insérées dans la liste, et il sera possible quand vous les extrairez de ces listes (par exemple "donne-moi le premier élément de la liste des tableaux de notes") de les faire passer de
void* à un type différent de celui qu'elles avaient à l'origine (par exemple, de mélanger des fonctions s'appliquant à la liste des tableaux d'entiers et à la liste des
strings).
Le
polymorphisme paramétrique est plus puissant que cela : le type de données générique est "
'a list" (liste d'objets de n'importe quel type), mais si vous construisez une liste d'entiers, elle sera de type "
int list". En effet,
'a est une lettre qui désigne le type (inconnu) de la liste ; si, pour une liste particulière, le type que représente
'a est connu, cette liste n'est plus polymorphe, et
'a est remplacé par le véritable nom du type.
'a joue un rôle de "paramètre de types", d'où le nom "polymorphisme paramétrique".
Ainsi, une liste de
strings sera de type "
string list", une liste de tableaux d'entiers "
int array list" (on doit lire "
(int array) list"), et vous ne pourrez pas les mélanger entre elles : le typage vous en empêche. Par contre, les fonctions qui prennent en entrée le type "
'a list" (par exemple : "quelle est la taille de la liste donnée", car la taille de la liste ne dépend pas du type de ses éléments) pourront s'appliquer aux deux types de listes différents, puisque "
int array list" et "
string list" sont tous les deux des
cas particuliers de "
'a list" (où
'a représente respectivement le type
int array et le type
string).
Le polymorphisme paramétrique n'est pas forcément évident à comprendre, alors voici deux petits exemples pour la route :
Code : Autre
Le type de cette fonction est "
'a -> 'b -> 'a". Cela signifie que le premier argument est de type
'a, le deuxième de type
'b, et que la valeur de retour est de type
'a (vu qu'on renvoie le premier argument). Que vient faire le
'b ici ? Il indique que le deuxième argument est de type quelconque, et
non nécessairement le même que le premier : pour différencier deux "types inconnus", on leur met un nom de paramètre différent.
Code : Autre1
| let choisir test x y = if test then x else y |
Le type de cette fonction est "
bool -> 'a -> 'a -> 'a" : elle prend en argument un booléen (représentant une valeur de vérité,
true ou
false), un argument de type
'a, et un autre argument de type
'a, et renvoie soit le deuxième, soit le troisième argument, selon la valeur du booléen.
Pourquoi les deux arguments sont-ils du même type
'a ? Parce que la valeur "choisir" ne peut avoir qu'un seul type de retour. Si le troisième argument était de type
'b, le type de retour de la fonction devrait être "soit
'a, soit
'b", ce qui n'est pas possible. Par inférence, le compilateur a donc conclu que les deux arguments étaient nécessairement du même type.