• Aucun résultat trouvé

Les constructeurs de types prennent d’autres types en paramètres pour finalement produire des types concrets. Ça me rappelle un peu les fonctions, qui prennent des valeurs en paramètres pour produire des valeurs. On a vu que les constructeurs de types peuvent être appliqués partiellement (Either String est un constructeur de types qui prend un type et produit un type concret,Either String comme Either String Int ), tout comme le peuvent les fonctions. C’est effectivement trèsEither String Int intéressant. Dans cette section, nous allons voir comment définir formellement la manière dont les constructeurs de types sont appliqués aux types, tout comme nous avions défini formellement comment les fonctions étaient appliquées à des valeurs en utilisant des déclarations de type. Vous

n’avez pas nécessairement besoin de lire cette section pour continuer votre quête magique d’Haskell, et si vous ne la comprenez pas, ne vous inquiétez pas. Cependant,

comprendre ceci vous donnera une compréhension très poussée du système de types. Donc, les valeurs comme 33 , "YEAH""YEAH" , ou takeWhiletakeWhile (les fonctions sont aussi des valeurs, puisqu’on peut les passer et les retourner) ont chacune un type. Les types sont des petites étiquettes que les valeurs transportent afin qu’on puisse raisonner sur leur valeur. Mais les types ont eux-même leurs petites étiquettes, appelées sortes. Une sorte est plus ou moins le type d’un type. Ça peut sembler bizarre et déroutant, mais c’est en fait un concept très cool.

ghci> :k Int Int :: *

Une étoile ? Comme c’est bizarre. Qu’est-ce que cela signifie ? Une ** signifie que le type est un type concret. Un type concret est un type qui ne prend pas de paramètre de types, et les valeurs ne peuvent avoir que des types concrets. Si je devais lire * tout haut (ce qui n’a jamais été le cas),* je dirais juste étoile ou type.

Ok, voyons maintenant le type de Maybe .Maybe

ghci> :k Maybe Maybe :: * -> *

Le constructeur de types MaybeMaybe prend un type concret (comme IntInt ) et retourne un type concret comme Maybe IntMaybe Int . Et c’est ce que nous dit cette sorte. Tout comme Int -> Int signifie qu’une fonction prend un IntInt -> Int Int et retourne un IntInt , * -> ** -> * signifie qu’un constructeur de types prend un type concret et retourne un type concret. Appliquons MaybeMaybe à un paramètre de type et voyons la sorte du résultat.

ghci> :k Maybe Int Maybe Int :: *

Comme prévu ! On a appliqué Maybe à un paramètre de type et obtenu un type concret (c’est ce que * -> *Maybe * -> * signifie). Un parallèle (bien que non équivalent, les types et les sortes étant des choses différentes) à cela est, lorsque l’on fait :t isUpper:t isUpper et :t isUpper 'A':t isUpper 'A' . isUpperisUpper a pour type Char -> BoolChar -> Bool et isUpper 'A'isUpper 'A' a pour type BoolBool , parce que sa valeur est simplement TrueTrue . Ces deux types ont tout de même pour sorte ** .

On a utilisé :k:k sur un type pour obtenir sa sorte, comme on a utilisé :t:t sur une valeur pour connaître son type. Comme dit précédemment, les types sont les étiquettes des valeurs, et les sortes les étiquettes des types, et on peut voir des parallèles entre les deux.

Regardons une autre sorte.

ghci> :k Either Either :: * -> * -> *

Aha, cela nous indique qu’EitherEither prend deux types concrets en paramètres pour produire un type concret. Ça ressemble aussi à la déclaration de type d’une fonction qui prend deux choses et en retourne une troisième. Les constructeurs de types sont curryfiés (comme les fonctions), donc on peut les appliquer partiellement.

ghci> :k Either String Either String :: * -> *

ghci> :k Either String Int Either String Int :: *

Lorsqu’on voulait faire d’EitherEither une instance de FunctorFunctor , on a dû l’appliquer partiellement parce que FunctorFunctor voulait des types qui attendent un paramètre, alors qu’EitherEither en prend deux. En d’autres mots, FunctorFunctor veut des types de sorte * -> ** -> * et nous avons dû appliquer partiellement EitherEither pour obtenir un type de sorte * -> ** -> * au lieu de sa sorte originale * -> * -> ** -> * -> * . Si on regarde à nouveau la définition de FunctorFunctor :

class Functor f where

fmap :: (a -> b) -> f a -> f b

on peut voir que f est utilisé comme un type qui prend un type concret et produit un type concret. On sait qu’il doit produire un type concret parcef que ce type est utilisé comme valeur dans une fonction. Et de ça, on peut déduire que les types qui peuvent être amis avec Functor doivent avoirFunctor pour sorte * -> * .* -> *

Maintenant, faisons un peu de type-fu. Regardez cette classe de types que je vais inventer maintenant : class Tofu t where

Wow, ça a l’air bizarre. Comment ferions-nous un type qui soit une instance de cette étrange classe de types ? Eh bien, regardons quelle sorte ce type devrait avoir. Puisque j a est utilisé comme le type d’une valeur que la fonction tofuj a tofu prend en paramètre, j aj a doit avoir pour sorte ** . En supposant que a a pour sorte *a * , alors jj doit avoir pour sorte * -> ** -> * . On voit que tt doit aussi produire un type concret et doit prendre deux types. Et sachant que aa a pour sorte ** et que jj a pour sorte * -> ** -> * , on infère que tt doit avoir pour sorte * -> (* -> *) -> ** -> (* -> *) -> * . Ainsi, il prend un type concret (aa ), un constructeur de types qui prend un type concret ( jj ) et produit un type concret. Wow.

OK, faisons alors un type qui a pour sorte * -> (* -> *) -> * . Voici une façon d’y arriver.* -> (* -> *) -> * data Frank a b = Frank {frankField :: b a} deriving (Show)

Comment sait-on que ce type a pour sorte * -> (* -> *) -> ** -> (* -> *) -> * ? Eh bien, les champs des TAD (types abstraits de données) doivent contenir des valeurs, donc doivent avoir pour sorte ** , évidemment. On suppose ** pour aa , ce qui veut dire que bb prend un paramètre de type et donc sa sorte est * -> ** -> * . On connaît les sortes de aa et bb , et puisqu’ils sont les paramètres de FrankFrank , on voit que FrankFrank a pour sorte

* -> (* -> *) -> *

* -> (* -> *) -> * . Le premier ** représente aa et le (* -> *)(* -> *) représente bb . Créons quelques valeurs de type FrankFrank et vérifions leur type.

ghci> :t Frank {frankField = Just "HAHA"}

Frank {frankField = Just "HAHA"} :: Frank [Char] Maybe

ghci> :t Frank {frankField = Node 'a' EmptyTree EmptyTree}

Frank {frankField = Node 'a' EmptyTree EmptyTree} :: Frank Char Tree

ghci> :t Frank {frankField = "YES"}

Frank {frankField = "YES"} :: Frank Char []

Hmm. Puisque frankFieldfrankField a un type de la forme a ba b , ses valeurs doivent avoir des types de forme similaire. Ainsi, elles peuvent être Just "HAHA"

Just "HAHA" , qui a pour type Maybe [Char]Maybe [Char] ou une valeur ['Y', 'E', 'S']['Y', 'E', 'S'] , qui a pour type [Char][Char] (si on utilisait notre propre type liste, ce serait List Char ). Et on voit que les types des valeurs FrankList Char Frank correspondent bien à la sorte de FrankFrank . [Char][Char] a pour sorte ** et

Maybe

Maybe a pour sorte * -> ** -> * . Puisque pour être une valeur, elle doit avoir un type concret et donc entièrement appliqué, chaque valeur Frank blah blaah

Frank blah blaah a pour sorte ** .

Faire de FrankFrank une instance de TofuTofu est plutôt facile. On voit que tofutofu prend un j aj a (par exemple un Maybe IntMaybe Int ) et retourne un t a jt a j . Si l’on remplaçait jj par FrankFrank , le type résultant serait Frank Int MaybeFrank Int Maybe .

instance Tofu Frank where tofu x = Frank x

ghci> tofu (Just 'a') :: Frank Char Maybe Frank {frankField = Just 'a'}

ghci> tofu ["HELLO"] :: Frank [Char] [] Frank {frankField = ["HELLO"]}

Pas très utile, mais bon pour l’entraînement de nos muscles de types. Encore un peu de type-fu. Mettons qu’on ait ce type de données : data Barry t k p = Barry { yabba :: p, dabba :: t k }

Et maintenant, on veut créer une instance de FunctorFunctor . FunctorFunctor prend des types de sorte * -> ** -> * mais BarryBarry n’a pas l’air de cette sorte. Quelle est la sorte de BarryBarry ? Eh bien, on voit qu’il prend trois paramètres de types, donc ça va être

something -> something -> something -> *

something -> something -> something -> * . Il est prudent de considérer que pp est un type concret et a pour sorte ** . Pour kk , on suppose ** , et par extension, tt a pour sorte * -> ** -> * . Remplaçons les something plus tôt par ces sortes, et l’on obtient

(* -> *) -> * -> * -> *

(* -> *) -> * -> * -> * . Vérifions avec GHCi.

ghci> :k Barry

Barry :: (* -> *) -> * -> * -> *

Ah, on avait raison. Comme c’est satisfaisant. Maintenant, pour rendre ce type membre de FunctorFunctor il nous faut appliquer partiellement les deux premiers paramètres de type de façon à ce qu’il nous reste un * -> ** -> * . Cela veut dire que le début de la déclaration d’instance sera :

instance Functor (Barry a b) where

instance Functor (Barry a b) where . Si on regarde fmapfmap comme si elle était faite spécifiquement pour BarryBarry , elle aurait pour type fmap :: (a -> b) -> Barry c d a -> Barry c d b

paramètre de type de BarryBarry devra changer, et on voit qu’il est convenablement placé dans son propre champ. instance Functor (Barry a b) where

fmap f (Barry {yabba = x, dabba = y}) = Barry {yabba = f x, dabba = y}

Et voilà ! On vient juste de mapper ff sur le premier champ.

Dans cette section, on a eu un bon aperçu du fonctionnement des paramètres de types et on les a en quelque sorte formalisés avec les sortes, comme on avait formalisé les paramètres de fonctions avec des déclarations de type. On a vu qu’il existe des parallèles intéressants entre les fonctions et les constructeurs de types. Cependant, ce sont deux choses complètement différentes. En faisant du vrai Haskell, vous n’aurez généralement pas à travailler avec des sortes et à inférer des sortes à la main comme on a fait ici. Généralement, vous devez juste appliquer partiellement * -> * ou ** -> * * à votre type personnalisé pour en faire une instance d’une des classes de types standard, mais il est bon de savoir comment et pourquoi cela fonctionne. Il est aussi intéressant de s’apercevoir que les types ont leur propre type. Encore une fois, vous n’avez pas à comprendre tout ce qu’on vient de faire pour continuer à lire, mais si vous comprenez comment les sortes fonctionnent, il y a des chances pour que vous ayez développé une compréhension solide du système de types d’Haskell.