• Aucun résultat trouvé

Fonctionnelles et polymorphisme

4.3 Typage et polymorphisme

Synth`ese du type le plus g´en´eral

Comme nous l’avons d´ej`a dit, le compilateur de Caml donne un type `a chaque phrase entr´ee par l’utilisateur ; cette inf´erence de types ne n´ecessite aucune participation de

l’utilisateur : elle se produit automatiquement sans n´ecessit´e d’indiquer les types dans les programmes. Connaissant les types des valeurs de base et des op´erations primitives, le contrˆoleur de types produit un type pour une phrase en suivant des r`egles de typage pour les constructions du langage comme la d´efinition et l’application des fonctions. De plus, le type inf´er´e contient le plus petit ensemble de contraintes n´ecessaires au bon d´eroulement de l’ex´ecution du programme (ici, « bon d´eroulement » signifie qu’il n’y aura pas d’erreurs de type `a l’ex´ecution). On dit que le contrˆoleur de type trouve le type le plus g´en´eral de chaque expression (notion introduite par Robin Milner en 1978). Par exemple, la fonction successeur re¸coit le type int -> int parce que son argument doitˆetre un entier, puisqu’on lui ajoute 1. En revanche la fonction identit´ea le type ’a -> ’a parce qu’il n’y a pas de contrainte sur son argument. Le polymorphisme s’introduit donc naturellement `a partir de l’absence de contraintes sur le type d’un argument ou d’une valeur. Par exemple, rappelons la d´efinition de la fonctionnelle double_le_r´esultat_de:

# let double_le_r´esultat_de (f : int -> int) = function x -> double (f x);;

double_le_r´esultat_de : (int -> int) -> int -> int = <fun>

L’argument f devait ˆetre une fonction des entiers vers les entiers, `a cause de la contrainte de type (f : int -> int), explicitement ´ecrite dans le programme. Mais si nous retirons cette contrainte de type, nous obtenons une fonctionnelle plus g´en´erale :

# let double_le_r´esultat_de f = function x -> double (f x);; double_le_r´esultat_de : (’a -> int) -> ’a -> int = <fun>

La fonctionnelle devient polymorphe, car le contrˆoleur de type a d´ecouvert que f devait seulement renvoyer un entier en r´esultat, mais qu’il n’est nullement obligatoire qu’elle prenne un entier en argument. Voici un exemple o`u f re¸coit une chaˆıne de caract`eres :

# let double_de_la_longueur = double_le_r´esultat_de string_length;; double_de_la_longueur : string -> int = <fun>

# double_de_la_longueur "Caml";; - : int = 8

Le polymorphisme d´ecoule donc de l’absence de contraintes sur une valeur. Cela explique pourquoi un param`etre de type peut ˆetre remplac´e sans risque d’erreurs par n’importe quel type, y compris un type lui-mˆeme polymorphe. Par exemple, on applique la fonction identit´e`a elle-mˆeme en l’employant avec le type (’a -> ’a) -> (’a -> ’a):

# let id x = (identit´e identit´e) x;; id : ’a -> ’a = <fun>

Puisque la fonction identit´e renvoie toujours son argument, (identit´e identit´e) s’´evalue en identit´e, et la fonction id est donc tout simplement ´egale `a la fonction identit´e.

L’alg`ebre des types de Caml

Nous allons maintenant pr´eciser davantage l’ensemble des types qu’utilise le syst`eme Caml, ce qu’on nomme techniquement son alg`ebre des types. Tout type Caml entre dans l’une des cat´egories suivantes :

Typage et polymorphisme 63 • Types de base (comme int ou string).

• Types composites (comme int -> int ou int vect). • Param`etres de type (comme ’a).

Les types composites sont construits avec des constructeurs de types, tels que la fl`eche ->. ´Etant donn´es deux types t1 et t2, le constructeur de type fl`eche construit le

type t1 -> t2, qui est le type des fonctions ayant un argument du type t1 et rendant

un r´esultat du type t2, autrement dit les fonctions de t1 dans t2. Remarquons que le

constructeur fl`eche est un op´erateur binaire (deux arguments) et infixe (situ´e entre ses arguments, comme l’est le symbole de l’addition +). En revanche, le constructeur de types vect est unaire, puisqu’`a partir d’un unique type t1, il construit le type

t1 vect. Ce constructeur est postfixe, c’est-`a-dire plac´e apr`es son argument. Tous les

constructeurs de types unaires sont postfix´es en Caml. Par extension, les types n’ayant pas d’arguments (int par exemple) sont appel´es constructeurs de types constants.

Les paires

Il existe un autre constructeur de type binaire et infixe dont nous n’avons pas encore parl´e : le constructeur pr´ed´efini « * ». ´Etant donn´es deux types t1et t2, la notation t1*t2

est donc un type. C’est le produit cart´esien des types t1 et t2. Il d´enote le type des

couples d’un ´el´ement du type t1 avec un ´el´ement du type t2. En math´ematiques, le

produit cart´esien de deux ensembles A et B est l’ensemble des couples (x, y) tels que x est ´el´ement de A et y ´el´ement de B. Le produit cart´esien de A et B est not´e A × B. Cette analogie avec la notation de la multiplication est aussi employ´ee en Caml, d’o`u le symbole * dans les types.

Les valeurs de types produit se notent comme en math´ematiques : on ´ecrit les deux ´el´ements du couple entre parenth`eses et s´epar´es par une virgule. Une petite diff´erence d’appellation cependant : en informatique on parle plus volontiers de paires que de couples. De plus, en Caml, les parenth`eses autour des paires ne sont pas toujours strictement n´ecessaires.

# (1, 2);;

- : int * int = 1, 2

Les paires sont aussi utilis´ees en tant qu’arguments ou r´esultats de fonctions.

# let addition (x, y) = x + y;; addition : int * int -> int = <fun> # addition (1, 2);;

- : int = 3

`

A l’aide de paires, on ´ecrit des fonctions qui rendent plusieurs r´esultats. Par exemple, la fonction suivante calcule simultan´ement le quotient et le reste d’une division enti`ere :

# let quotient_reste (x, y) = ((x / y), (x mod y));; quotient_reste : int * int -> int * int = <fun> # quotient_reste (5, 3);;

- : int * int = 1, 2

Les notations pour les paires se g´en´eralisent aux triplets, aux quadruplets, et en fait aux n-uplets pour n’importe quel nombre d’´el´ements n. Par exemple, (1, 2, 3) est un triplet d’entiers et poss`ede le type int * int * int.

4.4

Curryfication

`

A proprement parler, une fonction prenant une paire comme argument ne poss`ede quand mˆeme qu’un seul argument et non pas deux. La fonction addition ci-dessus, qui prend un seul argument qui se trouve ˆetre une paire, est diff´erente de la fonction addsuivante, qui prend deux arguments.

# let add x y = x + y;;

add : int -> int -> int = <fun>

Du point de vue pratique, la diff´erence est minime, il est vrai. D’un point de vue tech- nique, une fonction qui re¸coit ses arguments un par un (comme add) est dite curryfi´ee. En revanche, une fonction qui re¸coit tous ses arguments `a la fois sous la forme d’une paire ou plus g´en´eralement d’un n-uplet de valeurs est dite non curryfi´ee. Le n´eologisme

«curryfier » n’est pas une allusion `a la cuisine indienne, mais un hommage au logicien Haskell Curry.

Application partielle

La diff´erence essentielle entre add et addition tient dans la mani`ere de les appli- quer : il est l´egal d’appliquer la fonction add `a un seul argument, obtenant ainsi une fonction comme r´esultat, tandis que la fonction addition doit forc´ement recevoir ses deux entiers en mˆeme temps. Cette capacit´e des fonctions curryfi´ees de ne recevoir qu’un certain nombre de leurs arguments permet l’application partielle. Par exemple, en appliquant (partiellement) add `a l’entier 1, on obtient la fonction successeur.

# let successeur = add 1;; successeur : int -> int = <fun> # successeur 3;;

- : int = 4

Curryfication et type fl`eche

Une fonction curryfi´ee est donc un cas particulier de fonctionnelle, puisqu’elle per- met de cr´eer d’autres fonctions, en fixant certains de ses arguments. Cette propri´et´e est en fait inscrite dans le type d’une fonction curryfi´ee. Par exemple, le type de add est int -> int -> int. Or, le constructeur de type -> associe `a droite, ce qui signifie que le type de add n’est autre que int -> (int -> int). Cette ´ecriture explicitement parenth´es´ee indique clairement que add est une fonctionnelle : ´etant donn´e un entier, addretourne une autre fonction dont le type est justement (int -> int). Cela paraˆıt difficile `a comprendre au premier abord, mais c’est simplement une autre mani`ere de voir des phrases aussi simple que « ajouter 2 au r´esultat pr´ec´edent », qui signifie en fait : utiliser l’addition avec l’un des arguments fix´e `a 2 et appliquer cette fonction au r´esultat pr´ec´edent. En Caml, cela correspondrait `a ´evaluer :

(add 2) («r´esultat pr´ec´edent»);;

Une autre approche f´econde est de consid´erer add comme une fonction g´en´erique, qui permet d’obtenir la famille de toutes les fonctions qui ajoutent une constante `a leur argument (et qui sont donc de type int -> int). Par exemple, la fonction add_3, qui ajoute 3 `a son argument, est d´efinie par :

Une fonctionnelle de tri polymorphe 65

# let add_3 = add 3;; add_3 : int -> int = <fun>

L’application partielle d’une fonction curryfi´ee pour fixer certains de ces arguments se justifie lorsque la fonction est tr`es g´en´erale. Dans ce cas, cette op´eration de sp´ecialisation permet de retrouver des fonctions int´eressantes en elles-mˆemes. Nous en verrons un exemple avec le tri, o`u fixer l’argument fonctionnel correspondant `a la comparaison permet de d´efinir le tri en ordre croissant ou le tri en ordre d´ecroissant.

De cette ´etude des fonctions curryfi´ees, retenons que le constructeur de type → est associatif `a droite, ce qui signifie tout simplement que :

t1 → t2 → t3 est ´equivalent `a t1 → (t2 → t3)