• Aucun résultat trouvé

À 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. 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] et IntList 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]

On utilise les synonymes de types lorsqu’on veut rendre nos signatures de type plus descriptives en donnant des noms aux types qui nous disent 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)] pour représenter un répertoire téléphonique, on lui a donné le synonyme de type PhoneBook 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 CharList et une liste ayant pour type [Char]. On ne peut même pas utiliser ++ pour coller ensemble deux CharList , parce que ++ ne fonctionne que pour les listes, et le type de CharList 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 ++ sur celles-ci, puis les reconvertir en CharList.

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.

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 les types dont on peut tester l’égalité, et Ord pour ceux qu’on peut mettre en ordre, puis on est passé à des classes plus intéressantes, comme Functor et Applicative.

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’ Eq. Si l’on voit que notre type est un foncteur, alors on crée une instance de 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, le résultat est toujours égal à ce nombre. Peu importe qu’on fasse 1 * x ou x * 1, le résultat est toujours x. 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 1 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 ou 3 * (4 * 5). De toute manière, le résultat est 60. 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 et 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 *, 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 Monoid 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.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 Monoid, parce que le m de la définition de classe ne prend pas de paramètres. C’est différent de Functor et 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 de paramètres, c’est plutôt une constante polymorphique, un peu comme minBound de Bounded. mempty représente l’élément neutre de ce monoïde.

Ensuite, on a mappend, 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 mappend 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 de juxtaposition, et pensez plutôt que mappend est une fonction binaire qui prend deux valeurs monoïdales et en retourne une troisième.

La dernière fonction de la définition de la classe de types est mconcat. Elle prend une liste de valeurs monoïdales et les réduit à une unique valeur en faisant mappend entre les éléments de la liste. Elle a une implémentation par défaut, qui prend mempty 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 de mconcat pour l’instant. Quand on crée une instance de Monoid, il suffit d’implémenter mempty et mappend. La raison de la présence de 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 Monoid, 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 Monoid 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 ses 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