• Aucun résultat trouvé

Précédemment, on a mentionné qu’en écrivant des types, les types [Char][Char] et StringString étaient équivalents et interchangeables. Ceci est implémenté à l’aide des synonymes de types. Les synonymes de types ne font rien per se, il s’agit simplement de donner des noms différents au même type afin que la lecture du code ou de la documentation ait plus de sens pour le lecteur. Voici comment la bibliothèque standard définit

String

String comme un synonyme de [Char][Char] . type String = [Char]

On a introduit le mot-clé type. Le mot-clé peut être déroutant pour certains, puisqu’on ne crée en fait rien de nouveau (on faisait cela avec le mot-clé data), mais on crée seulement un synonyme pour un type déjà existant.

Si l’on crée une fonction qui convertit une chaîne de caractères en majuscules et qu’on l’appelle toUpperString ,toUpperString on peut lui donner comme déclaration de type toUpperString :: [Char] -> [Char] outoUpperString :: [Char] -> [Char]

toUpperString :: String -> String

toUpperString :: String -> String . Ces deux déclarations sont en effet identiques, mais la dernière est plus agréable à lire.

Quand on utilisait le module Data.MapData.Map , on a commencé par représenter un carnet téléphonique comme une liste associative avant de le représenter comme une map. Comme nous l’avions alors vu, une liste associative est une liste de paires clé-valeur. Regardons le carnet que nous avions alors.

phoneBook :: [(String,String)]

phoneBook = [("betty","555-2938") ,("bonnie","452-2928") ,("patsy","493-2928") ,("lucille","205-2928") ,("wendy","939-8282") ,("penny","853-2492") ]

On voit que le type de phoneBook est [(String, String)]phoneBook [(String, String)] . Cela nous indique que c’est une liste associative qui mappe des chaînes de caractères vers des chaînes de caractères, mais pas grand chose de plus. Créons un synonyme de types pour communiquer plus d’informations dans la déclaration de type.

type PhoneBook = [(String,String)]

À présent, la déclaration de notre carnet peut être phoneBook :: PhoneBook . Créons aussi un synonyme pour StringphoneBook :: PhoneBook String . type PhoneNumber = String

type Name = String

type PhoneBook = [(Name,PhoneNumber)]

Donner des synonymes de types au type String est pratique courante chez les programmeurs en Haskell lorsqu’ils souhaitent indiquer plusString d’information à propos des chaînes de caractères qu’ils utilisent dans leur programme et ce qu’elles représentent.

À présent, lorsqu’on implémente une fonction qui prend un nom et un nombre, et cherche si cette combinaison de nom et de numéro est dans notre carnet téléphonique, on peut lui donner une description de type très jolie et descriptive.

inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool

Si nous n’avions pas utilisé de synonymes de types, notre fonction aurait pour type String -> String -> [(String,String)] -> Bool .String -> String -> [(String,String)] -> Bool Ici, la déclaration tirant parti des synonymes de types est plus facile à lire. Cependant, n’en faites pas trop. On introduit les synonymes de types pour soit décrire ce que des types existants représentent dans nos fonctions (et ainsi les déclarations de types de nos fonctions deviennent de meilleures documentations), soit lorsque quelque chose a un type assez long et répété souvent (comme [(String, String)] ) et représente[(String, String)] quelque chose de spécifique dans le contexte de nos fonctions.

Les synonymes de types peuvent aussi être paramétrés. Si on veut un type qui représente le type des listes associatives de façon assez générale pour être utilisé quelque que soit le type des clés et des valeurs, on peut faire :

type AssocList k v = [(k,v)]

Maintenant, une fonction qui récupère la valeur associée à une clé dans une liste associative peut avoir pour type (Eq k) => k -> AssocList k v -> Maybe v

(Eq k) => k -> AssocList k v -> Maybe v . AssocListAssocList est un constructeur de types qui prend deux types et produit un type concret, comme AssocList Int String par exemple.AssocList Int String

Fonzie dit : Hey ! Quand je parle de types concrets, je veux dire genre des types appliqués complètement comme Map Int StringMap Int String , ou si on se frotte à une de ces fonctions polymorphes, [a][a] ou (Ord a) => Maybe a(Ord a) => Maybe a et tout ça. Et tu vois, parfois moi et mes potes on dit que

Maybe

Maybe c’est un type, mais bon, c’est pas ce qu’on veut dire, parce que même les idiots savent que MaybeMaybe est un constructeur de types, t’as vu. Quand j’applique Maybe à un type, comme dans Maybe StringMaybe Maybe String , alors j’ai un type concret. Tu sais, les valeurs, elles ne peuvent avoir que des types concrets ! En gros, vis vite, aime fort, et ne prête ton peigne à personne !

Tout comme l’on peut appliquer partiellement des fonctions pour obtenir de nouvelles fonctions, on peut appliquer partiellement des constructeurs de types pour obtenir de nouveaux constructeurs de types. Tout comme on appelle une fonctions avec trop peu de paramètres, on peut spécifier trop peu de paramètres de types à un constructeur de types et obtenir un constructeur de types partiellement appliqué. Si l’on voulait un type qui représente une map (de Data.Map ) qui va des entiers vers quelque chose, on pourrait faire soit :Data.Map

type IntMap v = Map Int v

Ou bien :

type IntMap = Map Int

D’une manière ou de l’autre, le constructeur de types IntMapIntMap prend un seul paramètre qui est le type de ce vers quoi les entiers doivent pointer.

Ah oui. Si vous comptez essayer d’implémenter ceci, vous allez probablement importer Data.Map qualifié. Quand vous faites un import qualifié,Data.Map les constructeurs de type doivent aussi être précédés du nom du module. Donc vous écririez type IntMap = Map.Map Int .type IntMap = Map.Map Int

Soyez certains d’avoir bien saisi la distinction entre constructeurs de types et constructeurs de valeurs. Juste parce qu’on a créé un synonyme de types IntMapIntMap ou AssocListAssocList ne signifie pas que l’on peut faire quelque chose comme AssocList [(1, 2), (4, 5), (7, 9)]AssocList [(1, 2), (4, 5), (7, 9)] . Tout ce que cela signifie, c’est qu’on peut parler de ce type en utilisant des noms différents. On peut faire

[(1, 2), (3, 5), (8, 9)] :: AssocList Int Int

[(1, 2), (3, 5), (8, 9)] :: AssocList Int Int , ce qui va faire que les nombres à l’intérieur auront pour type IntInt , mais on peut continuer à utiliser cette liste comme une liste normale qui contient des paires d’entiers. Les synonymes de types (et plus généralement les types) ne peuvent être utilisés que dans la partie types d’Haskell. On se situe dans cette partie lorsqu’on définit de nouveaux types (donc, dans une déclaration data ou type), ou lorsqu’on se situe après un :::: . Le :::: peut être dans des déclarations de types ou pour des annotations de types. Un autre type de données cool qui prend deux types en paramètres est Either a bEither a b . Voici grossièrement sa définition :

data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

Il a deux constructeurs de valeurs. Si le constructeur Left est utilisé, alors le contenu a pour type aLeft a , si RightRight est utilisé, le contenu a pour type b

Either a b

Either a b , on filtre généralement par motif sur LeftLeft et RightRight pour obtenir différentes choses en fonction.

ghci> Right 20

Right 20

ghci> Left "w00t"

Left "w00t"

ghci> :t Right 'a'

Right 'a' :: Either a Char

ghci> :t Left True

Left True :: Either Bool b

Jusqu’ici, nous avons vu que Maybe aMaybe a était principalement utilisé pour représenter des résultats de calculs qui pouvaient avoir soit échoué, soit réussi. Mais parfois, Maybe aMaybe a n’est pas assez bien, parce que NothingNothing n’indique pas d’information autre que le fait que quelque chose a échoué. C’est bien pour les fonctions qui peuvent échouer seulement d’une façon, ou lorsqu’on se fiche de savoir comment elles ont échoué. Une recherche de clé dans Data.MapData.Map ne peut échouer que lorsqu’une clé n’était pas présente, donc on sait ce qui s’est passé. Cependant, lorsqu’on s’intéresse à comment une fonction a échoué ou pourquoi, on utilise généralement le type Either a bEither a b , où aa est un type qui peut d’une certaine façon indiquer les raisons d’un éventuel échec, et bb est le type du résultat d’un calcul réussi. Ainsi, les erreurs utilisent le constructeur de valeurs

Left

Left , alors que les résultats utilisent le constructeur de valeurs RightRight .

Exemple : un lycée a des casiers dans lesquels les étudiants peuvent ranger leurs posters de Guns’n’Roses. Chaque casier a une combinaison. Quand un étudiant veut un nouveau casier, il indique au superviseur des casiers le numéro de casier qu’il voudrait, et celui-ci lui donne le code. Cependant, si quelqu’un utilise déjà ce casier, il ne peut pas lui donner le casier, et l’étudiant doit en choisir un autre. On va utiliser une map de

Data.Map

Data.Map pour représenter les casiers. Elle associera à chaque numéro de casier une paire indiquant si le casier est utilisé ou non, et le code du casier.

import qualified Data.Map as Map

data LockerState = Taken | Free deriving (Show, Eq) type Code = String

type LockerMap = Map.Map Int (LockerState, Code)

Simple. On introduit un nouveau type de données pour représenter un casier occupé ou libre, et on crée un synonyme de types pour le code du casier. On crée aussi un synonyme de types pour les maps qui prennent un entier et renvoient une paire d’état et de code de casier. À présent, on va créer une fonction qui cherche le code dans une map de casiers. On va utiliser un type Either String CodeEither String Code pour représenter le résultat, parce que la recherche peut échouer de deux façons - le casier peut être déjà pris, auquel cas on ne peut pas dévoiler le code, ou bien le numéro de casier peut tout simplement ne pas exister. Si la recherche échoue, on va utiliser une StringString pour décrire ce qui s’est passé.

lockerLookup :: Int -> LockerMap -> Either String Code

lockerLookup lockerNumber map =

case Map.lookup lockerNumber map of

Nothing -> Left $ "Locker number " ++ show lockerNumber ++ " doesn't exist!"

Just (state, code) -> if state /= Taken then Right code

else Left $ "Locker " ++ show lockerNumber ++ " is already taken!"

On fait une recherche normale dans la map. Si l’on obtient Nothing , on retourne une valeur de type Left StringNothing Left String , qui dit que le casier n’existe pas. Si on trouve le casier, on effectue une vérification supplémentaire pour voir s’il est utilisé. Si c’est le cas, on retourne Left en indiquant qu’ilLeft est déjà pris. Autrement, on retourne une valeur de type Right CodeRight Code , dans laquelle on donne le code dudit casier. En fait, c’est un

Right String

Right String , mais on a introduit un synonyme de types pour introduire un peu de documentation dans la déclaration de types. Voici une map à titre d’exemple :

lockers :: LockerMap

lockers = Map.fromList [(100,(Taken,"ZD39I")) ,(101,(Free,"JAH3I")) ,(103,(Free,"IQSA9")) ,(105,(Free,"QOTSA")) ,(109,(Taken,"893JJ")) ,(110,(Taken,"99292")) ]

Maintenant, cherchons le code de quelques casiers.

ghci> lockerLookup 101 lockers Right "JAH3I"

ghci> lockerLookup 100 lockers Left "Locker 100 is already taken!"

ghci> lockerLookup 102 lockers

Left "Locker number 102 doesn't exist!"

ghci> lockerLookup 110 lockers Left "Locker 110 is already taken!"

ghci> lockerLookup 105 lockers Right "QOTSA"

On aurait pu utiliser un Maybe aMaybe a pour représenter le résultat, mais alors on ne saurait pas pour quelle raison on n’a pas pu obtenir le code. Ici, l’information en cas d’échec est dans le type de retour.