• Aucun résultat trouvé

Jusqu’ici, on a découvert des classes de types standard d’Haskell et on a vu quels types les habitaient. On a aussi appris à créer automatiquement des instances de ces classes en demandant à Haskell de les dériver pour nous. Dans cette section, on va créer nos propres classes de types, et nos propres instances, le tout à la main.

Bref récapitulatif sur les classes de types : elles sont comme des interfaces. Une classe de types définit un comportement (tester l’égalité, comparer l’ordre, énumérer), puis les types qui peuvent se comporter de la sorte sont fait instances de ces classes. Le comportement des classes de types est défini en définissant des fonctions ou juste des déclarations de types que les instances doivent implémenter. Ainsi, lorsqu’on dit qu’un type est une instance d’une classe de types, on veut dire qu’on peut utiliser les fonctions que cette classe définit sur des éléments de ce type.

Les classes de types n’ont presque rien à voir avec les types dans des langages comme Java ou Python. Cela désoriente beaucoup de gens, aussi je veux que vous oubliiez tout ce que vous savez des classes dans les langages impératifs à partir de maintenant.

Par exemple, la classe Eq est pour les choses qui peuvent être testées égales. Elle définit les fonctions == et /=. Si on a un type (mettons, Car) et que comparer deux voitures avec == a un sens, alors il est sensé de rendre Car instance d’ Eq.

Voici comment Eq est définie dans le prélude :

class Eq a where

(==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y)

Wow, wow, wow ! Quels nouveaux syntaxe et mot-clés étranges ! Pas d‘inquiétude, tout cela va s’éclaircir bientôt. Tout d’abord, quand on écrit

class Eq a where , cela signifie qu’on définit une nouvelle classe Eq . Le a est une variable de types et indique que a jouera le rôle du type qu’on souhaitera bientôt rendre instance d’ Eq. Cette variable n’a pas à s’appeler a, ou même à être une lettre, elle doit juste être un mot en minuscules. Ensuite, on définit plusieurs fonctions. Il n’est pas obligatoire d’implémenter le corps de ces fonctions, on doit juste spécifier les déclarations de type des fonctions.

Certaines personnes comprendraient peut-être mieux si l’on avait écrit class Eq equatable where et ensuite spécifié les déclarations de type comme (==) :: equatable -> equatable -> Bool .

Toutefois, on a implémenté le corps des fonctions que Eq définit, mais on les a définies en termes de récursion mutuelle. On a dit que deux instances d’ Eq sont égales si elles ne sont pas différentes, et différentes si elles ne sont pas égales. Ce n’était pas nécessaire, mais on l’a fait, et on va bientôt voir en quoi cela nous aide.

Si nous avons, disons, class Eq a where et qu’on définit une déclaration de type dans cette classe comme (==) :: a -> a -> Bool, alors si on examine le type de cette fonction, celui-ci sera (Eq a) => a -> a -> Bool.

Maintenant qu’on a une classe, que faire avec ? Eh bien, pas grand chose, vraiment. Mais une fois qu’on a des instances, on commence à bénéficier de fonctionnalités sympathiques. Regardez donc ce type :

data TrafficLight = Red | Yellow | Green

Il définit les états d‘un feu de signalisation. Voyez comme on n’a pas dérivé d’instances de classes. C’est parce qu’on va les écrire à la main, bien qu’on aurait pu dériver celles-ci pour des classes comme Eq et Show. Voici comment on fait de notre type une instance d’ Eq.

instance Eq TrafficLight where

Red == Red = True Green == Green = True Yellow == Yellow = True _ == _ = False

On l’a fait en utilisant le mot-clé instance. class définit de nouvelles classes de types, et instance définit de nouvelles instances d’une classe de types. Quand nous définissions Eq, on a écrit class Eq a where et on a dit que a jouait le rôle du type qu’on voudra rendre instance de la classe plus tard. On le voit clairement ici, puisque quand on crée une instance, on écrit instance Eq TrafficLight where. On a remplacé le a par le type réel de cette instance particulière. Puisque == était défini en termes de /= et vice versa dans la déclaration de classe, il suffit d‘en définir un dans l’instance pour obtenir l’autre automatiquement. On dit que c’est une définition complète minimale d’une classe de types - un des plus petits ensembles de fonctions à implémenter pour que le type puisse se comporter comme une instance de la classe. Pour remplir la définition complète minimale d’ Eq, il faut redéfinir soit ==, soit /=. Cependant, si Eq était définie seulement ainsi :

class Eq a where

(==) :: a -> a -> Bool (/=) :: a -> a -> Bool

alors nous aurions dû implémenter ces deux fonctions lors de la création d’une instance, car Haskell ne saurait pas comment elles sont reliées. La définition complète minimale serait alors : == et /=.

Vous pouvez voir qu’on a implémenté == par un simple filtrage par motif. Puisqu’il y a beaucoup de cas où deux lumières sont différentes, on a spécifié les cas d’égalité, et utilisé un motif attrape-tout pour dire que dans tous les autres cas, les lumières sont différentes.

Créons une instance de Show à la main également. Pour satisfaire une définition complète minimale de Show, il suffit d’implémenter show, qui prend une valeur et retourne une chaîne de caractères.

instance Show TrafficLight where

show Red = "Red light"

show Yellow = "Yellow light" show Green = "Green light"

Encore une fois, nous avons utilisé le filtrage par motif pour arriver à nos fins. Voyons cela en action :

ghci> Red == Red True

ghci> Red == Yellow False

ghci> Red `elem` [Red, Yellow, Green] True

ghci> [Red, Yellow, Green]

Joli. On aurait pu simplement dériver Eq et obtenir la même chose (on ne l’a pas fait, dans un but éducatif). Cependant, dériver Show aurait simplement traduit les constructeurs en chaînes de caractères. Si on veut afficher "Reg light", il faut faire la déclaration d’instance à la main.

Vous pouvez également créer des classes de types qui sont des sous-classes d’autres classes de types. La déclaration de classe de Num est un peu longue, mais elle débute ainsi :

class (Eq a) => Num a where

...

Comme on l‘a mentionné précédemment, il y a beaucoup d’endroits où l’on peut glisser des contraintes de classe. Ici c’est comme écrire class Num a where , mais on déclare que a doit être une instance d’ Eq au préalable. Ainsi, un type doit être une instance d‘ eq avant de pouvoir prétendre être une instance de Num . Il est logique qu’avant qu’un type soit considéré comme numérique, on puisse attendre de lui qu’il soit testable pour l’égalité. C’est tout pour le sous-typage, c’est seulement une contrainte de classes sur une déclaration de classe ! À partir de là, quand on définit le corps des fonctions, que ce soit dans la déclaration de classe ou bien dans une déclaration d’instance, on peut toujours présumer que a est membre d’ Eq et ainsi utiliser == sur des valeurs de ce type.

Mais comment est-ce que Maybe, ou le type des listes, sont rendus instances de classes de types ? Ce qui rend Maybe différent de, par exemple,

TrafficLight , c’est que Maybe tout seul n’est pas un type concret, mais un constructeur de types qui prend un type en paramètre (comme Char ou un autre) pour produire un type concret (comme Maybe Char). Regardons à nouveau la classe de types Eq :

class Eq a where

(==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y)

De ces déclarations de types, on voit que a est utilisé comme un type concret car tous les types entre les flèches d’une fonction doivent être concrets (souvenez- vous, on ne peut avoir de fonction de type a -> Maybe, mais on peut avoir des fonctions a -> Maybe a ou Maybe Int -> Maybe String). C’est pour cela qu’on ne peut pas faire :

instance Eq Maybe where

...

Parce que, comme on l’a vu, a doit être un type concret, et Maybe ne l’est pas. C’est un constructeur de types qui prend un paramètre pour produire un type concret. Il serait également très fastidieux d’écrire des instances instance Eq (Maybe Int) where, instance Eq (Maybe Char) where, etc. pour chaque type qu’on utilise. Ainsi, on peut écrire :

instance Eq (Maybe m) where

Just x == Just y = x == y Nothing == Nothing = True _ == _ = False

C‘est-à-dire qu’on déclare tous les types de la forme Maybe something comme instances d’ Eq. On aurait à vrai dire pu écrire (Maybe something), mais on opte généralement pour des identifiants en une lettre pour rester proche du style Haskell. Le (Maybe m) ici joue le rôle du a de class Eq a where. Alors que

Maybe n‘était pas un type concret, Maybe m l’est. En spécifiant un paramètre de type ( m , qui est en minuscule), on dit qu’on souhaite parler de tous les types de la forme Maybe m , où m est n’importe quel type, afin d’en faire une instance d’ Eq.

Il y a un problème à cela tout de même. Le voyez-vous ? On utilise == sur le contenu de Maybe, mais on n‘a pas de garantie que ce que contient Maybe est membre d’ Eq ! C‘est pourquoi nous devons modifier la déclaration d’instance ainsi :

instance (Eq m) => Eq (Maybe m) where

Just x == Just y = x == y Nothing == Nothing = True _ == _ = False

On a dû ajouter une contrainte de classe ! Avec cette déclaration d‘instance, on dit ceci : nous voulons que tous les types de la forme Maybe m soient membres de la classe Eq, à condition que le type m (celui dans le Maybe) soit lui-même membre d’ Eq. C’est ce qu’Haskell dériverait d’ailleurs.

La plupart du temps, les contraintes de classe dans les déclarations de classes sont utilisées pour faire d‘une classe de types une sous-classe d’une autre classe de types, alors que les contraintes de classe dans les déclarations d’instance sont utilisées pour exprimer des pré-requis sur le contenu de certains types. Par exemple, ici nous avons requis que le contenu de Maybe soit membre de la classe Eq.

a -> a -> Bool ), alors il faut fournir à l’instance un type concret en fournissant si besoin est des paramètres de type et en parenthésant le tout, de manière à obtenir un type concret.

Prenez en compte le fait que le type dont vous essayez de faire une instance remplacera le paramètre dans la déclaration de classe. Le a de

class Eq a where sera remplacé par un type réel lorsque vous écrirez une instance, donc essayez de mettre mentalement le type dans les déclarations de types des fonctions. (==) :: Maybe -> Maybe -> Bool ne veut ainsi par dire grand chose, alors que

(==) :: (Eq m) => Maybe m -> Maybe m -> Bool est sensé. C’est juste pour mieux y voir dans votre tête, en réalité, == conservera toujours son type (==) :: (Eq a) => a -> a -> Bool , peu importe le nombre d’instances que l’on crée.

Oh, encore un truc, regardez ça ! Si vous voulez connaître les instances d’une classe de types, faites juste :info YourTypeClass dans GHCi. En tapant

:info Num , vous verrez toutes les fonctions que définit cette classe de types, suivies d’une liste de tous les types qui habitent cette classe. :info marche aussi sur les types et les constructeurs de types. :info Maybe vous montre toutes les classes dont Maybe est une instance. :info peut aussi montrer la déclaration de type d’une fonction. Je trouve ça plutôt cool.