• Aucun résultat trouvé

Dans la section Classes de types 101, nous avons vu les bases des classes de types. Nous avons dit qu’une classe de types est une sorte d’interface qui définit un comportement. Un type peut devenir une instance d’une classe de types s’il supporte ce comportement. Par exemple : le type Int est une instance d’ EqInt Eq parce que cette classe définit le comportement de ce qui peut être testé pour l’égalité. Et puisqu’on peut tester l’égalité de deux entiers, Int est membre de la classe EqInt Eq . La vraie utilité vient des fonctions qui agissent comme l’interface d’Eq , à savoir ==Eq == et /=/= . Si un type est membre d’ EqEq , on peut utiliser les fonctions == et /=== /= avec des valeurs de ce type. C’est pourquoi des expressions comme 4 == 44 == 4 et "foo" /= "bar""foo" /= "bar" sont correctement typées.

Nous avons aussi mentionné qu’elles sont souvent confondues avec les classes de langages comme Java, Python, C++ et autres, ce qui sème la confusion dans l’esprit de beaucoup de gens. Dans ces langages, les classes sont des patrons à partir desquels sont créés des objets qui contiennent un état et peuvent effectuer des actions. Les classes de types sont plutôt comme des interfaces. On ne crée pas de données à partir de classes de types. Plutôt, on crée d’abord notre type de données, puis on se demande pour quoi il peut se faire passer. S’il peut être comparé pour l’égalité, on le rend instance de la classe EqEq . S’il peut être ordonné, on le rend instance de la classe OrdOrd .

Dans la prochaine section, nous verrons comment créer manuellement nos instances d’une classe de types en implémentant

les fonctions définies par cette classe. Pour l’instant, voyons comment Haskell peut magiquement faire de nos types des instances de n’importe laquelle des classes de types suivantes : Eq , OrdEq Ord , EnumEnum , BoundedBounded , ShowShow et ReadRead . Haskell peut dériver le comportement de nos types dans ces contextes si l’on utilise le mot-clé deriving lors de la création du type de données.

Considérez ce type de données :

data Person = Person { firstName :: String , lastName :: String , age :: Int

}

Il décrit une personne. Posons comme hypothèse que deux personnes n’ont jamais les mêmes nom, prénom et âge. Maintenant, si l’on a un enregistrement pour deux personnes, est-ce que cela a un sens de vérifier s’il s’agit de la même personne ? Bien sûr. On peut essayer de voir si les enregistrements sont les mêmes ou non. C’est pourquoi il serait sensé que ce type soit membre de la classe Eq . Nous allons dériver cette instance.Eq

data Person = Person { firstName :: String , lastName :: String , age :: Int

} deriving (Eq)

Lorsqu’on dérive l’instance Eq pour un type, puis qu’on essaie de comparer deux valeurs de ce type avec ==Eq == ou /=/= , Haskell va regarder si les deux constructeurs sont égaux (ici il n’y en a qu’un possible cependant), puis va tester si les données contenues dans les valeurs sont égales deux à deux en testant chaque paire de champ avec ==== . Ainsi, il y a un détail important, qui est que les types des champs doivent aussi êtres membres de la classe EqEq . Puisque StringString et IntInt le sont, tout va bien. Testons notre instance d’ EqEq .

ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}

ghci> let adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41}

ghci> let mca = Person {firstName = "Adam", lastName = "Yauch", age = 44}

ghci> mca == adRock False

ghci> mikeD == adRock False

ghci> mikeD == mikeD True

True

Bien sûr, puisque Person est maintenant dans EqPerson Eq , on peut utiliser comme aa pour toute fonction ayant une contrainte de classe Eq aEq a dans sa signature, comme elemelem .

ghci> let beastieBoys = [mca, adRock, mikeD]

ghci> mikeD `elem` beastieBoys True

Les classes de types Show et ReadShow Read sont pour les choses qui peuvent être converties respectivement vers et depuis une chaîne de caractères. Comme avec EqEq , si un constructeur de types a des champs, leur type doit aussi être membre de ShowShow ou ReadRead si on veut faire de notre type une instance de l’une de ces classes. Faisons de notre type de données PersonPerson un membre de ShowShow et de ReadRead .

data Person = Person { firstName :: String , lastName :: String , age :: Int

} deriving (Eq, Show, Read)

Maintenant, on peut afficher une personne dans le terminal.

ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}

ghci> mikeD

Person {firstName = "Michael", lastName = "Diamond", age = 43}

ghci> "mikeD is: " ++ show mikeD

"mikeD is: Person {firstName = \"Michael\", lastName = \"Diamond\", age = 43}"

Si nous avions voulu afficher une personne dans le terminal avant de rendre PersonPerson membre de ShowShow , Haskell se serait plaint du fait qu’il ne sache pas représenter une personne comme une chaîne de caractères. Mais maintenant qu’on a dérivé Show , il sait comment faire.Show

Read

Read est en gros l’inverse de ShowShow . ShowShow convertit des valeurs de notre type en des chaînes de caractères, ReadRead convertit des chaîne de caractères en valeurs de notre type. Souvenez-vous cependant, lorsqu’on avait utilisé la fonction read , on avait dû annoter explicitement le typeread qu’on désirait. Sans cela, Haskell ne sait pas vers quel type on veut convertir.

ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" :: Person Person {firstName = "Michael", lastName = "Diamond", age = 43}

Si on utilise le résultat de readread dans un calcul plus élaboré, Haskell peut inférer le type qu’on attend, et l’on n’a alors pas besoin d’annoter le type.

ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" == mikeD True

On peut aussi lire des types paramétrés, et il faut alors annoter le type avec les paramètres complétés. Ainsi, on ne peut pas faire read "Just 't'" :: Maybe a

read "Just 't'" :: Maybe a , mais on peut faire read "Just 't'" :: Maybe Charread "Just 't'" :: Maybe Char .

On peut dériver des instances la classe de types Ord , qui est pour les types dont les valeurs peuvent être ordonnées. Si l’on compare deux valeursOrd du même type ayant été construites par deux constructeurs différents, la valeur construite par le constructeur défini en premier sera considérée plus petite. Par exemple, considérez le type Bool , qui peut avoir pour valeur FalseBool False ou TrueTrue . Pour comprendre comment il fonctionne lorsqu’il est comparé, on peut l’imaginer défini comme :

data Bool = False | True deriving (Ord)

Puisque le constructeur de valeurs False est spécifié avant le constructeur de valeurs TrueFalse True , on peut considérer que TrueTrue est plus grand que False

False .

ghci> True `compare` False GT

ghci> True > False True

False

Dans le type de données Maybe aMaybe a , le constructeur de valeurs NothingNothing est spécifié avant le constructeur de valeurs JustJust , donc une valeur Nothing

Nothing est toujours plus petite qu’une valeur Just somethingJust something , même si ce something est moins un milliard de milliards. Mais si l’on compare deux valeurs Just , alors Haskell compare ce qu’elles contiennent.Just

ghci> Nothing < Just 100

True

ghci> Nothing > Just (-49999) False

ghci> Just 3 `compare` Just 2

GT

ghci> Just 100 > Just 50

True

Mais on ne peut pas faire Just (*3) > Just (*2) , parce que (*3)Just (*3) > Just (*2) (*3) et (*2)(*2) sont des fonctions, qui ne sont pas des instances d’ OrdOrd . On peut facilement utiliser des types de données algébriques pour créer des énumérations, et les classes de types EnumEnum et BoundedBounded nous aident dans la tâche. Considérez les types de données suivants :

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday

Puisque tous les constructeurs de valeurs sont nullaires (ils ne prennent pas de paramètres, ou champs), on peut rendre le type membre de la classe EnumEnum . La classe de types EnumEnum est pour les choses qui ont des prédécesseurs et des successeurs. On peut aussi le rendre membre de la classe BoundedBounded , qui est pour les choses avec une plus petite valeur et une plus grande valeur. Tant qu’on y est, faisons-en aussi une instance des autres classes qu’on a vues.

data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum)

Puisque le type est membre des classes ShowShow et ReadRead , on peut le convertir vers et depuis des chaînes de caractères.

ghci> Wednesday Wednesday

ghci> show Wednesday

"Wednesday"

ghci> read "Saturday" :: Day Saturday

Puisqu’il est membre des classes EqEq et OrdOrd , on peut comparer et tester l’égalité de deux jours.

ghci> Saturday == Sunday False

ghci> Saturday == Saturday True

ghci> Saturday > Friday True

ghci> Monday `compare` Wednesday LT

Il est aussi membre de BoundedBounded , donc on peut demander le plus petit jour et le plus grand jour.

ghci> minBound :: Day Monday

ghci> maxBound :: Day Sunday

C’est aussi une instance d’EnumEnum . On peut obtenir le prédécesseur et le successeur d’un jour, et créer une progression de jours !

ghci> succ Monday Tuesday

ghci> pred Saturday Friday

ghci> [Thursday .. Sunday] [Thursday,Friday,Saturday,Sunday]

ghci> [minBound .. maxBound] :: [Day]

[Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday]

C’est plutôt génial.