• Aucun résultat trouvé

Un constructeur de valeurs peut prendre plusieurs valeurs comme paramètres et retourner une nouvelle valeur. Par exemple, le constructeur CarCar prend trois valeurs et produit une valeur de type CarCar . De manière similaire, les constructeurs de types peuvent prendre des types en paramètres pour créer de nouveaux types. Ça peut sembler un peu trop méta au premier abord, mais ce n’est pas si compliqué. Si vous êtes familier avec les templates C++, vous verrez des parallèles. Pour se faire une bonne image de l’utilisation des paramètres de types en action, regardons comment un des types que nous avons déjà rencontré est implémenté.

data Maybe a = Nothing | Just a

Le aa est un paramètre de type. Et puisqu’il y a un paramètre de type impliqué, on dit que MaybeMaybe est un constructeur de type. En fonction de ce que l’on veut que ce type de données contienne lorsqu’il ne vaut pas

Nothing

Nothing , ce constructeur de types peut éventuellement construire un type Maybe IntMaybe Int , Maybe CarMaybe Car , Maybe String

Maybe String , etc. Aucune valeur ne peut avoir pour type MaybeMaybe , car ce n’est pas un type per se, mais un constructeur de types. Pour pouvoir être un type réel qui peut avoir une valeur, il doit avoir tous ses paramètres remplis.

Donc, si l’on passe CharChar en paramètre de type à MaybeMaybe , on obtient un type Maybe CharMaybe Char . La valeur Just 'a'

Just 'a' a pour type Maybe CharMaybe Char par exemple.

Vous ne le savez peut-être pas, mais on a utilisé un type qui a un paramètre de type avant même d’utiliser Maybe

Maybe . Ce type est le type des listes. Bien qu’il y ait un peu de sucre syntaxique en jeu, le type liste prend un paramètre et produit un type concret. Des valeurs peuvent avoir pour type [Int] , [Char][Int] [Char] , [[String]][[String]] , mais aucune valeur ne peut avoir pour type [][] .

Jouons un peu avec le type MaybeMaybe .

ghci> Just "Haha"

Just "Haha"

Just 84

ghci> :t Just "Haha"

Just "Haha" :: Maybe [Char]

ghci> :t Just 84

Just 84 :: (Num t) => Maybe t

ghci> :t Nothing Nothing :: Maybe a

ghci> Just 10 :: Maybe Double Just 10.0

Les paramètres de types sont utiles parce qu’on peut créer différents types en fonction de la sorte de types qu’on souhaite que notre type de données contienne. Quand on fait :t Just "Haha":t Just "Haha" , le moteur d’inférence de types se rend compte que le type doit être Maybe [Char]Maybe [Char] , parce que le a dans le Just aa Just a est une chaîne de caractères, donc le aa de Maybe aMaybe a doit aussi être une chaîne de caractères.

Remarquez que le type de NothingNothing est Maybe aMaybe a . Son type est polymorphique. Si une fonction nécessite un Maybe IntMaybe Int en paramètre, on peut lui donner NothingNothing , parce que NothingNothing ne contient pas de valeur de toute façon, donc peu importe. Le type Maybe aMaybe a peut se comporter comme Maybe Int s’il le faut, tout comme 5Maybe Int 5 peut se comporter comme un IntInt ou un DoubleDouble . De façon similaire, le type de la liste vide est

[a]

[a] . Une liste vide peut être une liste de quoi que ce soit. C’est pourquoi on peut faire [1, 2, 3] ++ [][1, 2, 3] ++ [] et ["ha", "ha", "ha"] ++ []["ha", "ha", "ha"] ++ [] . Utiliser les paramètres de types est très bénéfique, mais seulement quand les utiliser a un sens. Généralement, on les utilise quand notre type de données fonctionne sans se soucier du type de ce qu’il contient en lui, comme pour notre type Maybe a . Si notre type se comporte comme uneMaybe a sorte de boîte, il est bien de les utiliser. On pourrait changer notre type de données CarCar de ceci :

data Car = Car { company :: String , model :: String , year :: Int } deriving (Show)

en cela :

data Car a b c = Car { company :: a , model :: b , year :: c } deriving (Show)

Mais y gagnerait-on vraiment ? La réponse est : probablement pas, parce qu’on finirait par définir des fonctions qui ne fonctionnent que sur le type Car String String Int

Car String String Int . Par exemple, vu notre première définition de CarCar , on pourrait écrire une fonction qui affiche les propriétés de la voiture avec un joli petit texte.

tellCar :: Car -> String

tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y

ghci> let stang = Car {company="Ford", model="Mustang", year=1967}

ghci> tellCar stang

"This Ford Mustang was made in 1967"

Quelle jolie petite fonction ! La déclaration de type est mignonne et fonctionne bien. Maintenant, si CarCar était Car a b cCar a b c ?

tellCar :: (Show a) => Car String String a -> String

tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y Nous devrions forcer cette fonction à prendre un type Car tel que (Show a) => Car String String aCar (Show a) => Car String String a . Vous pouvez constater que la signature de type est plus compliquée, et le seul avantage qu’on en tire serait qu’on pourrait utiliser n’importe quel type instance de la classe de types ShowShow pour cc .

ghci> tellCar (Car "Ford" "Mustang" 1967)

"This Ford Mustang was made in 1967"

ghci> tellCar (Car "Ford" "Mustang" "nineteen sixty seven")

"This Ford Mustang was made in \"nineteen sixty seven\""

ghci> :t Car "Ford" "Mustang" 1967

ghci> :t Car "Ford" "Mustang" "nineteen sixty seven"

Car "Ford" "Mustang" "nineteen sixty seven" :: Car [Char] [Char] [Char]

Dans la vie réelle cependant, on finirait par utiliser Car String String Int la plupart du temps, et il semblerait queCar String String Int paramétrer le type Car ne vaudrait pas le coup. On utilise généralement les paramètres de types lorsque le typeCar contenu dans les divers constructeurs de valeurs du type de données n’est pas vraiment important pour que le type fonctionne. Une liste de choses est une liste de choses, peu importe ce que les choses sont, ça marche. Si on souhaite sommer une liste de nombres, on peut spécifier au dernier moment que la fonction de sommage attend une liste de nombres. De même pour MaybeMaybe . MaybeMaybe représente une option qui est soit de n’avoir rien, soit d’avoir quelque chose. Peu importe ce que le type de cette chose est.

Un autre exemple de type paramétré que nous avons déjà rencontré est Map k v de Data.MapMap k v Data.Map . Le kk est le type des clés, le v le type des valeurs. C’est un bon exemple d’endroit où les types paramétrés sont très utiles. Avoir des mapsv paramétrées nous permet de créer des maps de n’importe quel type vers n’importe quel autre type, du moment que le type de la clé soit membre de la classe de types Ord . Si nous souhaitions définir un type de map, on pourrait ajouter laOrd contrainte de classe dans la déclaration data :

data (Ord k) => Map k v = ...

Cependant, il existe une très forte convention en Haskell qui est de ne jamais ajouter de contraintes de classe à une déclaration de

données. Pourquoi ? Eh bien, parce que le bénéfice est minimal, mais on se retrouve à écrire plus de contraintes de classes, même lorsqu’elles ne

sont pas nécessaires. Que l’on mette ou non, la contrainte Ord k dans la déclaration data de Map k vOrd k Map k v , on aura à écrire la contrainte dans les fonctions qui supposent un ordre sur les clés de toute façon. Mais, si l’on ne met pas la contrainte dans la déclaration data, alors on n’aura pas à mettre (Ord k) =>(Ord k) => dans les déclarations de types des fonctions qui n’ont pas besoin de cette contrainte pour fonctionner. Un exemple d’une telle fonction est toList , qui prend un mapping et le convertit en liste associative. Sa signature de type est toList :: Map k a -> [(k, a)]toList toList :: Map k a -> [(k, a)] . Si Map k vMap k v avait une contrainte de classe dans sa déclaration data, le type de ToListToList devrait être

toList :: (Ord k) => Map k a -> [(k, a)]

toList :: (Ord k) => Map k a -> [(k, a)] , alors que cette fonction ne fait aucune comparaison de clés selon leur ordre.

Conclusion : ne mettez pas de contraintes de classe dans les déclarations data même lorsqu’elles ont l’air sensées, parce que de toute manière, vous devrez les écrire dans les fonctions qui en dépendent.

Implémentons un type de vecteur 3D et ajoutons-y quelques opérations. Nous utiliserons un type paramétré afin qu’il supporte plusieurs types numériques, bien qu’en général on n’en utilise qu’un seul.

data Vector a = Vector a a a deriving (Show)

vplus :: (Num t) => Vector t -> Vector t -> Vector t

(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)

vectMult :: (Num t) => Vector t -> t -> Vector t (Vector i j k) `vectMult` m = Vector (i*m) (j*m) (k*m)

scalarMult :: (Num t) => Vector t -> Vector t -> t

(Vector i j k) `scalarMult` (Vector l m n) = i*l + j*m + k*n

vplus

vplus somme deux vecteurs. Deux vecteurs sont sommés en sommant leurs composantes deux à deux. scalarMultscalarMult est le produit scalaire de deux vecteurs et vectMultvectMult permet de multiplier un vecteur par un scalaire. Ces fonctions peuvent opérer sur des types comme Vector IntVector Int ,

Vector Integer

Vector Integer , Vector FloatVector Float , à condition que le aa de Vector aVector a soit de la classe NumNum . Également, si vous examinez les déclarations de type des fonctions, vous verrez qu’elles n’opèrent que sur des vecteurs de même type et que les scalaires doivent également être du type contenu dans les vecteurs. Remarquez qu’on n’a pas mis de contrainte Num dans la déclaration data, puisqu’on a eu à l’écrire dans chaqueNum fonction qui en dépendait de toute façon.

Une fois de plus, il est très important de distinguer le constructeur de types du constructeur de valeurs. Lorsqu’on déclare un type, le nom à gauche du == est le constructeur de types, et les constructeurs situés après (séparés par des || ) sont des constructeurs de valeurs. Donner à une fonction le type Vector t t t -> Vector t t t -> t serait faux, parce que l’on doit donner des types dans les déclarations de types, leVector t t t -> Vector t t t -> t

constructeur de types vecteurs ne prend qu’un paramètre, alors que le constructeur de valeurs en prend trois. Jouons avec nos vecteurs.

ghci> Vector 3 5 8 `vplus` Vector 9 2 8

ghci> Vector 3 5 8 `vplus` Vector 9 2 8 `vplus` Vector 0 2 3

Vector 12 9 19

ghci> Vector 3 9 7 `vectMult` 10

Vector 30 90 70

ghci> Vector 4 9 5 `scalarMult` Vector 9.0 2.0 4.0 74.0

ghci> Vector 2 9 3 `vectMult` (Vector 4 9 5 `scalarMult` Vector 9 2 4) Vector 148 666 222