• Aucun résultat trouvé

type

vs.

newtypenewtype

vs. data

data

À ce point, vous êtes peut-être perdu concernant les différences entre type, data et newtype, alors rafraîchissons-nous la mémoire.

Le mot-clé type crée des synonymes de types. Cela signifie qu’on donne simplement un autre nom à un type existant, pour qu’il soit plus simple d’en parler. Par exemple, ceci :

type IntList = [Int]

Tout ce que cela fait, c’est nous permettre de faire référence à [Int] en tapant IntList[Int] IntList . Les deux peuvent être utilisés de manière interchangeable. On n’obtient pas de constructeur de valeurs IntList ou quoi que ce soit du genre. Puisque [Int]IntList [Int] et IntListIntList sont deux façons de parler du même type, peu importe laquelle on utilise dans nos annotations de types :

ghci> ([1,2,3] :: IntList) ++ ([1,2,3] :: [Int]) [1,2,3,1,2,3]

quelque chose sur le contexte des fonctions où ils sont utilisés. Par exemple, quand on a utilisé une liste associative qui a pour type [(String,String)]

[(String,String)] pour représenter un répertoire téléphonique, on lui a donné le synonyme de type PhoneBookPhoneBook afin que les signatures de type des fonctions soient plus simples à lire.

Le mot-clé newtype sert à prendre des types existants et les emballer dans de nouveaux types, principalement parce que ça facilite la création d’instances de certaines classes de types. Quand on utilise newtype pour envelopper un type existant, le type qu’on obtient est différent du type original. Si l’on crée le nouveau type suivant :

newtype CharList = CharList { getCharList :: [Char] }

On ne peut pas utiliser ++++ pour accoler une CharListCharList et une liste ayant pour type [Char][Char] . On ne peut même pas utiliser ++++ pour coller ensemble deux CharList , parce que ++CharList ++ ne fonctionne que pour les listes, et le type de CharListCharList n’est pas une liste, même si l’on peut dire qu’il en contient une. On peut, toutefois, convertir deux CharList en listes, utiliser ++CharList ++ sur celles-ci, puis les reconvertir en CharListCharList . Quand on utilise la syntaxe des enregistrements dans notre déclaration newtype, on obtient des fonctions pour convertir entre le type original et le nouveau type : dans une sens, avec le constructeur de valeurs de notre newtype, et dans l’autre sens, avec la fonction qui extrait la valeur du champ. Le nouveau type n’est pas automatiquement fait instance des classes de types du type original, donc il faut les dériver ou les écrire manuellement.

En pratique, on peut imaginer les déclarations newtype comme des déclarations data qui n’ont qu’un constructeur de valeurs, et un seul champ. Si vous vous retrouvez à écrire une telle déclaration data, pensez à utiliser newtype.

Le mot-clé data sert à créer vos propres types de données, et avec ceux-ci, tout est possible. Ils peuvent avoir autant de constructeurs de valeurs et de champs que vous le voulez, et peuvent être utilisés pour implémenter n’importe quel type de données algébrique vous-même. Tout, des listes aux Maybe en passant par les arbres.Maybe

Si vous voulez seulement de plus jolies signatures de type, vous voulez probablement des synonymes de types. Si vous voulez prendre un type existant et l’envelopper dans un nouveau type pour en faire une instance d’une classe de types, vous voulez sûrement un newtype. Et si vous voulez créer quelque chose de neuf, les chances sont bonnes que le mot-clé data soit ce qu’il vous faut.

Monoïdes

Les classes de types en Haskell sont uilisées pour présenter une interface pour des types qui partagent un comportement. On a commencé avec des classes de types simples comme Eq , pour lesEq types dont on peut tester l’égalité, et OrdOrd pour ceux qu’on peut mettre en ordre, puis on est passé à des classes plus intéressantes, comme

Functor

Functor et ApplicativeApplicative .

Lorsqu’on crée un type, on réfléchit aux comportements qu’il supporte, i.e. pour quoi peut-il se faire passer, et à partir de cela, on décide de quelles classes de types on en fera une instance. Si cela a un sens de tester l’égalité de nos valeurs, alors on crée une instance d’EqEq . Si l’on voit que notre type est un foncteur, alors on crée une instance de

Functor

Functor , et ainsi de suite.

Maintenant, considérez cela : * est une fonction qui prend deux* nombres et les multiplie entre eux. Si l’on multiplie un nombre par 1 , le1 résultat est toujours égal à ce nombre. Peu importe qu’on fasse 1 * x1 * x ou x * 1x * 1 , le résultat est toujours xx . De façon similaire, ++++ est aussi une fonction qui prend deux choses et en retourne une troisième. Au

lieu de les multiplier, elle les concatène. Et tout comme * , elle a aussi une valeur qui ne change pas l’autre quand on la combine avec ++* ++ . Cette valeur est la liste vide : [][] .

ghci> 4 * 1 4 ghci> 1 * 9 9 ghci> [1,2,3] ++ [] [1,2,3]

ghci> [] ++ [0.5, 2.5] [0.5,2.5]

On dirait que ** et 11 partagent une propriété commune avec ++++ et [][] : La fonction prend deux paramètres.

Les paramètres et la valeur retournée ont le même type.

Il existe une valeur de ce type qui ne change pas l’autre valeur lorsqu’elle est appliquée à la fonction binaire.

Autre chose que ces deux opérations ont en commun et qui n’est pas si évident : lorsqu’on a trois valeurs ou plus et qu’on utilise la fonction binaire pour les réduire à une seule valeur, l’ordre dans lequel on applique la fonction n’importe pas. Peu importe qu’on fasse (3 * 4) * 5(3 * 4) * 5 ou

3 * (4 * 5)

3 * (4 * 5) . De toute manière, le résultat est 6060 . De même pour ++++ :

ghci> (3 * 2) * (8 * 5)

240

ghci> 3 * (2 * (8 * 5))

240

ghci> "la" ++ ("di" ++ "da")

"ladida"

ghci> ("la" ++ "di") ++ "da" "ladida"

On appelle cette propriété l’associativité. * est associative, de même que ++* ++ , mais par exemple -- ne l’est pas. Les expressions (5 - 3) - 4(5 - 3) - 4 et 5 - (3 - 4)

5 - (3 - 4) n’ont pas le même résultat.

En remarquant ces propriétés et en les écrivant, on vient de tomber sur les monoïdes ! On a un monoïde lorsqu’on dispose d’une fonction binaire associative et d’un élément neutre pour cette fonction. Quand quelque chose est un élément neutre pour une fonction, cela signifie que lorsqu’on appelle la fonction avec cette chose et une autre valeur, le résultat est toujours égal à l’autre valeur. 1 est l’élément neutre vis-à-vis de *1 * , et [][] est le neutre de ++++ . Il y a beaucoup d’autres monoïdes à trouver dans le monde d’Haskell, c’est pourquoi la classe de types MonoidMonoid existe. Elle est pour ces types qui peuvent agir comme des monoïdes. Voyons comment la classe de types est définie :

class Monoid m where mempty :: m

mappend :: m -> m -> m mconcat :: [m] -> m

mconcat = foldr mappend mempty

La classe de types Monoid est définie dans Data.MonoidMonoid Data.Monoid . Prenons un peu de temps pour la découvrir.

Tout d’abord, on voit que seuls des types concrets peuvent être faits instance de MonoidMonoid , parce que le mm de la définition de classe ne prend pas de paramètres. C’est différent de FunctorFunctor et

Applicative

Applicative qui nécessitent que leurs instances soient des constructeurs de types prenant un paramètre.

La première fonction est mempty . Ce n’est pas vraiment une fonction puisqu’elle ne prend pas demempty paramètres, c’est plutôt une constante polymorphique, un peu comme minBound de BoundedminBound Bounded .

mempty

mempty représente l’élément neutre de ce monoïde.

Ensuite, on a mappendmappend , qui, comme vous l’avez probablement deviné, est la fonction binaire. Elle prend deux valeurs du même type et retourne une valeur de ce type. Il est bon de mentionner que la décision d’appeler mappendmappend de la sorte est un peu malheureuse, parce que ce nom semble impliquer qu’on essaie de juxtaposer deux choses (NDT : en anglais, “append” signifie “juxtaposer”). Bien que ++++

prenne deux listes et les juxtapose, * ne juxtapose pas vraiment, elle multiplie seulement deux nombres. Quand nous rencontrerons d’autres* instances de Monoid , on verra que la plupart d’entre elles ne juxtaposent pas non plus leurs valeurs, évitez donc de penser en termes deMonoid juxtaposition, et pensez plutôt que mappend est une fonction binaire qui prend deux valeurs monoïdales et en retourne une troisième.mappend La dernière fonction de la définition de la classe de types est mconcatmconcat . Elle prend une liste de valeurs monoïdales et les réduit à une unique

valeur en faisant mappendmappend entre les éléments de la liste. Elle a une implémentation par défaut, qui prend memptymempty comme valeur initiale et plie la liste depuis la droite avec mappend . Puisque l’implémentation par défaut convient pour la plupart des instances, on ne se souciera pas trop demappend

mconcat

mconcat pour l’instant. Quand on crée une instance de MonoidMonoid , il suffit d’implémenter memptymempty et mappendmappend . La raison de la présence de mconcat

mconcat ici est que, pour certaines instances, il peut y avoir une manière plus efficace de l’implémenter, mais pour la plupart des instances, l’implémentation par défaut convient.

Avant de voir des instances spécifiques de MonoidMonoid , regardons brièvement les lois des monoïdes. On a mentionné qu’il devait exister une valeur neutre vis-à-vis de la fonction binaire, et que la fonction binaire devait être associative. Il est possible de créer des instances de MonoidMonoid ne suivant pas ces règles, mais ces instances ne servent à personne parce que, quand on utilise la classe de types Monoid , on se repose sur le fait que sesMonoid instances agissent comme des monoïdes. Sinon, à quoi cela servirait-il ? C’est pourquoi, en créant des instances, on doit s’assurer qu’elles obéissent à ces lois :

mempty `mappend` x = x x `mappend` mempty = x

(x `mappend` y) `mappend` z = x `mappend` (y `mappend` z)

Les deux premières déclarent que memptymempty doit être le neutre de mappendmappend , la troisième dit que mappendmappend doit être associative, i.e. l’ordre dans lequel on applique mappend pour réduire plusieurs valeurs monoïdales en une n’importe pas. Haskell ne fait pas respecter ces lois, c’est donc àmappend nous en tant que programmeur de faire attention à ce que nos instances y obéissent.

Les listes sont des monoïdes

Oui, les listes sont des monoïdes ! Comme on l’a vu, la fonction ++++ et la liste vide [][] forment un monoïde. L’instance est très simple : instance Monoid [a] where

mempty = [] mappend = (++)

Les listes sont une instance de la classe de types MonoidMonoid quel que soit le type des éléments qu’elles contiennent. Remarquez qu’on a écrit instance Monoid [a]

instance Monoid [a] et pas instance Monoid []instance Monoid [] , parce que MonoidMonoid nécessite un type concret en instance. En testant cela, pas de surprises :

ghci> [1,2,3] `mappend` [4,5,6] [1,2,3,4,5,6]

ghci> ("one" `mappend` "two") `mappend` "tree" "onetwotree"

ghci> "one" `mappend` ("two" `mappend` "tree")

"onetwotree"

ghci> "one" `mappend` "two" `mappend` "tree" "onetwotree"

ghci> "pang" `mappend` mempty

"pang"

ghci> mconcat [[1,2],[3,6],[9]] [1,2,3,6,9]

ghci> mempty :: [a] []

Remarquez qu’à la dernière ligne, nous avons dû écrire une annotation de type explicite, parce que si l’on faisait seulement mempty , GHCi ne saurait pas quelle instance utiliser, on indique donc qu’on veut celle des listes. On a pumempty utiliser le type général [a] (plutôt qu’un type spécifique comme [Int][a] [Int] ou [String][String] ) parce que la liste vide peut se faire passer pour n’importe quel type de liste.

Puisque mconcat a une implémentation par défaut, on l’obtient gratuitement lorsqu’on crée une instance de Monoidmconcat Monoid . Dans le cas des listes, mconcatmconcat s’avère être juste concatconcat . Elle prend une liste de listes et l’aplatit, parce que c’est équivalent à faire ++ entre toutes les listes adjacentes d’une liste.++

Les lois des monoïdes sont en effet respectées par l’instance des listes. Quand on a plusieurs listes et qu’on les mappend (ou ++mappend ++ ) ensemble, peu importe l’ordre dans lequel l’opération est effectuée, parce qu’au final, elles sont toutes à la suite l’une de l’autre de toute façon. De plus, la liste vide se comporte bien comme un élément neutre, donc tout va bien. Remarquez que les monoïdes ne nécessitent pas que a `mappend` b soita `mappend` b égal à b `mappend` ab `mappend` a . Dans le cas des listes, clairement ce n’est pas le cas.

ghci> "one" `mappend` "two" "onetwo"

ghci> "two" `mappend` "one" "twoone"

Et ce n’est pas un problème. Le fait que pour la multiplication, 3 * 5 et 5 * 33 * 5 5 * 3 soient identiques n’est qu’une propriété de la multiplication, mais n’a pas à être vrai pour tous les monoïdes (et ne l’est pas pour la plupart d’ailleurs).