• Aucun résultat trouvé

Introduction aux types de données algébriques

Jusqu’ici, nous avons croisé beaucoup de types de données. BoolBool , IntInt , CharChar , MaybeMaybe , etc. Mais comment créer les nôtres ? Eh bien, un des moyens consiste à utiliser le mot-clé data pour définir un type. Voyons comment le type Bool est défini dans la bibliothèque standard.Bool

data Bool = False | True

data

data signifie qu’on crée un nouveau type de données. La partie avant le == dénote le type, ici BoolBool . Les choses après le == sont des

constructeurs de valeurs. Ils spécifient les différentes valeurs que peut prendre ce type. Le | se lit comme un ou. On peut donc lire cette| déclaration : le type Bool peut avoir pour valeur TrueBool True ou FalseFalse . Le nom du type tout comme les noms des constructeurs de valeurs doivent commencer par une majuscule.

De manière similaire, on peut imaginer Int défini ainsi :Int

data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647

Le premier et le dernier constructeurs sont les valeurs minimales et maximales d’un Int . En vrai le type n’estInt pas défini ainsi, les points de suspension cachent l’omission d’un paquet énorme de nombres, donc ceci est juste à titre illustratif.

Maintenant, imaginons comment l’on représenterait une forme en Haskell. Une possibilité serait d’utiliser des tuples. Un cercle pourrait être (43.1, 55.0, 10.4)(43.1, 55.0, 10.4) où les deux premières composantes seraient les coordonnées du centre, et la troisième le rayon. Ça a l’air raisonnable, mais ça pourrait tout aussi bien représenter un vecteur 3D ou je ne sais quoi. Une meilleure solution consiste à créer notre type pour représenter les données. Disons qu’une forme puisse être un cercle ou un rectangle. Voilà :

data Shape = Circle Float Float Float | Rectangle Float Float Float Float

Qu’est-ce que c’est que ça ? Pensez-y ainsi. Le constructeur de valeurs Circle a trois champs, qui sont tous des FloatCircle Float . Ainsi, lorsqu’on écrit un constructeur de valeurs, on peut optionnellement ajouter des types à la suite qui définissent les valeurs qu’il peut contenir. Ici, les deux premiers champs sont les coordonnées du centre, le troisième est son rayon. Le constructeur de valeurs Rectangle a quatre champs qui acceptent desRectangle

Float

Float . Les deux premiers sont les coordonnées du coin supérieur gauche, et les deux autres celles du coin inférieur droit.

Quand je dis champ, il s’agit en fait de paramètres. Les constructeurs de valeurs sont ainsi des fonctions qui retournent ultimement un type de données. Regardons les signatures de type de ces deux constructeurs de valeurs.

ghci> :t Circle

Circle :: Float -> Float -> Float -> Shape

ghci> :t Rectangle

Rectangle :: Float -> Float -> Float -> Float -> Shape

Cool, donc les constructeurs de valeurs sont des fonctions comme les autres. Qui l’eût cru ? Créons une fonction qui prend une forme et retourne sa surface.

surface :: Shape -> Float

surface (Circle _ _ r) = pi * r ^ 2

surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)

La première chose notable, c’est la déclaration de type. Elle dit que la fonction prend un ShapeShape et retourne un FloatFloat . On n’aurait pas pu écrire une déclaration de type Circle -> FloatCircle -> Float parce que CircleCircle n’est pas un type, alors que ShapeShape en est un. Tout comme on ne peut pas écrire une fonction qui a pour déclaration de type True -> IntTrue -> Int . La prochaine chose remarquable, c’est le filtrage par motif sur les constructeurs de valeurs. Nous l’avons en fait déjà fait (tout le temps à vrai dire), lorsque l’on filtrait des valeurs contre [] ou False[] False ou 55 , seulement ces valeurs n’avaient pas de champs. On écrit seulement le constructeur, puis on lie ses champs à des noms. Puisqu’on s’intéresse au rayon, on se fiche des autres champs, qui n’indiquent que la position du cercle.

ghci> surface $ Circle 10 20 10 314.15927

ghci> surface $ Rectangle 0 0 100 100 10000.0

Yay, ça marche ! Mais si on essaie d’afficher Circle 10 20 5Circle 10 20 5 dans l’invite, on obtient une erreur. C’est parce qu’Haskell ne sait pas (encore) comment afficher ce type de données sous une forme littérale. Rappelez-vous, quand on essaie d’afficher une valeur dans l’invite, Haskell exécute la fonction show sur cette valeur pour obtenir une représentation en chaîne de caractères, puis affiche celle-ci dans le terminal. Pour rendreshow

Shape

Shape membre de la classe de types ShowShow , on peut le modifier ainsi :

data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

On ne s’intéressera pas trop à la dérivation pour l’instant. Disons seulement qu’en ajoutant deriving (Show) à la fin d’une déclaration data,deriving (Show) Haskell rend magiquement ce type membre de la classe de types Show . Ainsi, on peut à présent faire :Show

ghci> Circle 10 20 5

Circle 10.0 20.0 5.0

ghci> Rectangle 50 230 60 90

Rectangle 50.0 230.0 60.0 90.0

Les constructeurs de valeurs sont des fonctions, on peut donc les mapper, les appliquer partiellement, etc. Si l’on veut créer une liste de cercles concentriques de différents rayons, on peut faire :

ghci> map (Circle 10 20) [4,5,6,6]

[Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]

Notre type de données est satisfaisant, bien qu’il pourrait être amélioré. Créons un type de données intermédiaire qui définit un point dans l’espace bidimensionnel. On pourra l’utiliser pour rendre nos formes plus compréhensibles.

data Point = Point Float Float deriving (Show)

data Shape = Circle Point Float | Rectangle Point Point deriving (Show)

Remarquez qu’en définissant un point, on a utilisé le même nom pour le type de données et pour le constructeur de valeurs. Ça n’a pas de sens particulier, mais il est assez usuel de donner le même nom lorsqu’un type n’a qu’un seul constructeur de valeurs. À présent, le Circle a deuxCircle champs, un de type Point et un autre de type FloatPoint Float . Cela simplifie la compréhension de ce qu’ils représentent. Idem pour le rectangle. On doit maintenant ajuster surface pour refléter ces changements.surface

surface :: Shape -> Float

surface (Circle _ r) = pi * r ^ 2

surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)

On a seulement changé les motifs. Dans le motif du cercle, on ignore complètement le point. Dans celui du rectangle, on utilise des motifs imbriqués pour récupérer les champs des points. Si l’on voulait référencer le point lui-même pour une raison quelconque, on aurait pu utiliser des motifs nommés.

ghci> surface (Rectangle (Point 0 0) (Point 100 100))

10000.0

ghci> surface (Circle (Point 0 0) 24)

1809.5574

Et pourquoi pas une fonction qui bouge une forme ? Elle prend une forme, une quantité sur l’axe x et sur l’axe y, et retourne une forme aux mêmes dimensions, mais déplacée selon ces quantités.

nudge :: Shape -> Float -> Float -> Shape

nudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r

nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b)) Plutôt simple. On ajoute simplement les déplacements aux coordonnées correspondantes.

ghci> nudge (Circle (Point 34 34) 10) 5 10

Circle (Point 39.0 44.0) 10.0

Si on ne veut pas manipuler directement les points, on peut créer des fonctions auxiliaires qui créent des formes d’une taille donnée à l’origine, puis les déplacer.

baseCircle :: Float -> Shape

baseCircle r = Circle (Point 0 0) r

baseRect :: Float -> Float -> Shape

baseRect width height = Rectangle (Point 0 0) (Point width height)

ghci> nudge (baseRect 40 100) 60 23

Rectangle (Point 60.0 23.0) (Point 100.0 123.0)

Vous pouvez bien sûr exporter vos types de données de vos modules. Pour ce faire, ajoutez simplement les types que vous souhaitez exporter au même endroit que les fonctions à exporter, puis ouvrez des parenthèses et spécifiez les constructeurs de valeurs que vous voulez exporter, séparés par des virgules. Si vous voulez exporter tous les constructeurs, vous pouvez écrire .... .

Si on voulait exporter les fonctions et types définis ici dans un module, on pourrait commencer ainsi : module Shapes ( Point(..) , Shape(..) , surface , nudge , baseCircle , baseRect ) where

En faisant Shape(..)Shape(..) , on exporte tous les constructeurs de valeurs de ShapeShape , donc quiconque importe le module peut créer des formes à l’aide de RectangleRectangle et CircleCircle . C’est équivalent à Shape (Rectangle, Circle)Shape (Rectangle, Circle) .

On pourrait aussi choisir de ne pas exporter de constructeurs de valeurs pour Shape en écrivant simplement ShapeShape Shape dans la déclaration d’export. Ainsi, quelqu’un qui voudrait créer une forme serait obligé de le faire en passant par les fonctions baseCirclebaseCircle et baseRectbaseRect . Data.MapData.Map utilise cette approche. Vous ne pouvez pas créer de map en faisant Map.Map [(1,2),(3,4)]Map.Map [(1,2),(3,4)] parce qu’il n’exporte pas ce constructeur de valeurs. Cependant, vous pouvez en créer à l’aide de fonctions auxiliaires comme Map.fromListMap.fromList . Souvenez-vous, les constructeurs de valeurs sont juste des fonctions qui prennent des champs en paramètres et retournent une valeur d’un certain type (comme ShapeShape ). Donc quand on choisit de ne pas les exporter, on empêche seulement les personnes qui importent notre module d’utiliser ces fonctions, mais si d’autres fonctions exportées retournent un type, on peut les utiliser pour créer des valeurs de ce type de données.

Ne pas exporter les constructeurs de valeurs d’un type de données le rend plus abstrait en cachant son implémentation. Également, quiconque utilise ce module ne peut pas filtrer par motif sur les constructeurs de valeurs.