Le polymorphisme
Un problème
Maintenant, quel est le type de la fonction head qui prend le premier élément d'une liste ? Si on veut l'utiliser sur une liste d'entiers, il faut que son type soit
[Integer] -> Integer
. Si on veut pouvoir l'utiliser sur une liste de caractères, son type doit être
[Char] -> Char
, et la même chose avec tous les types.
On pourrait dire que c'est une fonction qui prend une liste d'éléments de n'importe quel type, et renvoie un élément de n'importe quel type, mais dans ce cas on perd le fait que la valeur retournée est du même type que les éléments de la liste, et on risque toujours d'avoir des erreurs de types à l'exécution du programme.
La solution
Regardons quel est le type de head :
Code : Console | Prelude> :t head
head :: [a] -> a |
Le
a n'est pas un nom de type, puisqu'il ne commence pas par une lettre majuscule. En réalité c'est une variable de type, et on peut la remplacer par n'importe quel type, à partir du moment où on remplace à chaque endroit où elle apparait la variable par ce type.
Par exemple, on pourrait utiliser head comme une fonction de type
[Integer] -> Integer
, ou
[Char] -> Char
, mais pas comme une fonction de type
[Integer] -> Char
, puisqu'on a remplacé
a par deux types différents.
On peut aussi introduire plusieurs variables de types dans une même signature. Par exemple, quel est le type de la fonction suivante ?
Code : Haskell | construireTriplet x y z = (x,y,z)
|
Le polymorphisme ne concerne pas que les fonctions : par exemple, Nothing a pour type
Maybe a
.
Pour vous entrainer, vos pouvez essayer de trouver le type de quelques fonctions sur les listes, comme
:
,
++
,
reverse
ou
concat
simplement en pensant à ce qu'elles font, et le vérifier avec la commande
:t. Si vous ne vous souvenez plus de ce que font ces fonctions, vous pouvez relire la partie sur les listes du deuxième chapitre.
Quand vous écrivez le code d'une fonction et que vous savez ce qu'elle fait mais pas comment la coder, il est souvent pratique de commencer par penser au type de la fonction et de l'écrire, car cela peut donner des indications sur comment devrait fonctionner la fonction.
Classes de types
Quel le type de + ?
Le polymorphisme règle un certain nombre de problèmes, mais on a toujours des problèmes pour donner le type de certaines fonctions. Par exemple, l'opérateur + doit permettre d'additionner tous les types de nombres : on doit donc pouvoir l'utiliser avec le type
Integer -> Integer -> Integer
, mais aussi avec le type
Double -> Double -> Double
. On pourrait donc penser que le type de + est
a -> a -> a
. Cependant, cela pose toujours un problème : pour certains types, l'addition n'a pas de sens. Par exemple, que voudraient dire la multiplication ou la division sur les listes ?
Pour régler ce problème, on utilise les classes de types. Regardez dans ghci le type de + avec la commande
:t (+)
(on a besoin de la notation infixe quand on veut parler de l'opérateur tout seul en tant que fonction) : c'est
(Num a) => a -> a -> a
.
Comme prévu, il y a bien une variable de type, puisque la fonction doit être polymorphe. Cependant, cette signature est composée de deux parties, séparées par une double flèche
=>. La partie à droite est un type construit normalement, qui peut contenir des variables de type. La partie à gauche est plus intéressante : c'est un ensemble de
contraintes sur ces variables de type, séparées par des virgules. Une contrainte de la forme
Num a
signifie que le type a doit faire partie de la classe de types
Num, qui correspond aux nombres. On peut donc comprendre cette contrainte comme "a doit être un type numérique". On voit aussi qu'on ne peut additionner que des nombres
du même type. Par exemple, il est impossible d'ajouter un Double et un Integer.
On peut avoir plusieurs contraintes dans une même signature. Par exemple, le type de
f x y = (x+1,y+1)
est
f :: (Num a, Num b) => a -> b -> (a,b)
.
Limitations
Quand on écrit un nombre entier dans le code source du programme, il est vu par le compilateur comme une valeur de type
(Num a) => a
, c'est-à-dire n'importe quel type de nombre. De même, quand on entre un nombre décimal, il est vu comme une valeur de type
(Fractional a) => a
.
Cependant, pour des raisons de performances et sous certaines conditions, il peut arriver que le compilateur décide d'utiliser un type moins polymorphe que prévu. Par exemple, avec le fichier suivant :
Code : Haskell | entier = 13
decimal = 2.5
|
Code : Console | Prelude> :t entier
Integer
Prelude> :t decimal
Double |
On voit que dans ce cas-là, le type est plus restreint que prévu. Cela pose des problèmes, par exemple une erreur de type quand on tente de multiplier les deux nombres. Il est possible de forcer le compilateur à donner un type polymorphe en indiquant soi-même le type :
Code : Haskell | entier :: (Num a) => a
entier = 13
decimal :: (Fractional a) => a
decimal = 2.5
|
Vous pouvez aussi obtenir des erreurs du type :
Code : Console | ../haskell/Test.hs:8:7:
Ambiguous type variable `a' in the constraint:
`Eq a' arising from a use of `==' at ../haskell/Test.hs:8:7-10
Possible cause: the monomorphism restriction applied to the following:
egal :: a -> a -> Bool (bound at ../haskell/Test.hs:8:0)
Probable fix: give these definition(s) an explicit type signature
or use -XNoMonomorphismRestriction |
Dans ce cas, il suffit de donner un type à la variable qui pose problème pour résoudre le problème.
Classes de types les plus courantes
Maintenant, il est temps de voir les classes de types définies dans le Prelude. Je ne décrirais pour chaque classe que quelques fonctions utiles, ou seulement ce qu'elle représente. Pour plus d'information sur une classe donnée, reportez-vous à
la documentation du Prelude.
Ne vous sentez pas obligés de tout connaitre par coeur : vous pourrez revenir à cette partie du tuto ou lire la documentation plus tard. Les principales classes à retenir sont Num, Fractional, Eq et Ord.
Commençons par les classes numériques. Elles forment une hiérarchie de classes assez compliqué.
La classe
Num fournit les opérations mathématiques de base : l'addition, la soustraction et la multiplication. Elle fournit aussi une fonction
fromInteger :: (Num a) => Integer -> a
, qui permet de transformer tout nombre entier en n'importe quel autre type de nombre.
La classe
Real représente les types qui sont un sous-ensemble de nombres rationnels. Elle permet d'utiliser la fonction
toRational :: (Real a) => a -> Rational
, qui permet de transformer un nombre en nombre ratinonel. Il doivent aussi pouvoir être ordonnés.
La classe
Integral correspond aux nombres entiers. Par exemple, les types Int (entiers à nombre de chiffre limité) et Integer (entiers aussi grands qu'on veut) sont tous les deux des instances de cette classe. Les opérations intéressantes sont div et mod, qui permettent de trouver respectivement le quotient et le reste de la division euclidienne d'un nombre par un autre, et les opérations gcd et lcm qui permettent de trouver respectivement le
PGCD et le
PPCM de deux nombres. Il y a aussi une opération
toInteger :: (Integral a) => a -> Integer
qui permet de transformer n'importe quel nombre entier en Integer.
La classe
Fractional permet d'utiliser la division.
Floating rajoute toutes les opérations trigonométriques, l'exponentielle et les logarithmes.
La classe
RealFrac intègre les opérations d'arrondi vers le haut et vers le bas.
Pour les autres classes, c'est plus simple :
La classe
Eq est la classe des objets dont on peut déterminer s'ils sont égaux ou pas. Elle permet d'utiliser les fonctions == et /=.
La classe
Ord correspond aux types dont on peut comparer les éléments. Elle fournit les fonctions de comparaison habituelles.
Enfin, la classe
Enum correspond aux types dont on peut énumérer les éléments, et permet par exemple
d'utiliser la notation de séquences. Par exemple, les entiers et les caractères font partie de cette classe, donc on peut écrire
[1..10]
et
['a'..'z']
.
Enfin, deux classes . La classe
Show fournit une fonction
show :: (Show a) => a -> String
. Elle permet de convertir une valeur en chaine de caractères, par exemple pour l'afficher. Les valeurs sont représentées sous une forme qui peut normalement être utilisées dans du code Haskell. Par exemple :
Code : Console | Prelude> show 42
"42"
Prelude> show [1,2,3]
"[1,2,3]" |
La fonction read, de la classe de types
Read fait l'inverse : elle transforme une chaine de caractère en la valeur qu'elle représente. Cependant, le type n'est pas déterminé dynamiquement en fonction de ce qui est lu, mais à la compilation. Si on veut tester cette fonction dans ghci, il ne sait pas quel type de données on attend, il faut donc le préciser (cela n'est pas nécessaire en général, puisque le type de la valeur est déterminé suivant le contexte dans lequel on l'utilise). Pour cela, on utilise la notation ::
Code : Console | Prelude> read "42" :: Int
42
Prelude> read "[1,2,3]" :: [Int]
[1,2,3]
Prelude> read "[1,2,3]" :: Int
*** Exception: Prelude.read: no parse |
Si read n'arrive pas à lire correctement la valeur, il renvoie une erreur.