• Aucun résultat trouvé

Dans cette section, nous allons nous intéresser aux foncteurs applicatifs, qui sont des foncteurs gonflés aux hormones, représentés en Haskell par la classe de types Applicative, située dans le module

Control.Applicative .

Comme vous le savez, les fonctions en Haskell sont curryfiées par défaut, ce qui signifie qu’une fonction qui semble prendre plusieurs paramètres en prend en fait un seul et retourne une fonction qui prend le prochain paramètre, et ainsi de suite. Si une fonction a pour type a -> b -> c, on dit généralement qu’elle prend deux paramètres et retourne un c, mais en réalité, elle prend un a et retourne une fonction b -> c. C’est pourquoi on peut appeler une fonction en faisant f x y ou bien (f x) y. Ce mécanisme nous permet d’appliquer partiellement des fonctions en les appelant avec trop peu de paramètres, ce qui résulte en des fonctions que l’on peut passer à d’autres fonctions.

Jusqu’ici, lorsqu’on mappait des fonctions sur des foncteurs, on mappait généralement des fonctions qui ne

prenaient qu’un paramètre. Mais que se passe-t-il lorsqu’on souhaite mapper *, qui prend deux paramètres, sur un foncteur ? Regardons quelques exemples concrets. Si l’on a Just 3 et que l’on fait fmap (*) (Just 3), qu’obtient-on ? En regardant l’implémentation de l’instance de Functor de Maybe, on sait que si c’est une valeur Just something, elle applique la fonction sur le something à l’intérieur du Just. Ainsi, faire fmap (*) (Just 3) retourne

Just ((*) 3) , qui peut être écrit Just (* 3) en utilisant une section. Intéressant ! On obtient une fonction enveloppée dans un Just !

ghci> :t fmap (++) (Just "hey")

fmap (++) (Just "hey") :: Maybe ([Char] -> [Char])

ghci> :t fmap compare (Just 'a')

fmap compare (Just 'a') :: Maybe (Char -> Ordering)

ghci> :t fmap compare "A LIST OF CHARS"

fmap compare "A LIST OF CHARS" :: [Char -> Ordering]

ghci> :t fmap (\x y z -> x + y / z) [3,4,5,6]

fmap (\x y z -> x + y / z) [3,4,5,6] :: (Fractional a) => [a -> a -> a]

Si l’on mappe compare, qui a pour type (Ord a) => a -> a -> Ordering sur une liste de caractères, on obtient une liste de fonctions ayant pour type Char -> Ordering , parce que la fonction compare est partiellement appliquée sur les caractères de la liste. Ce n’est pas une liste de fonctions

(Ord a) => a -> Ordering , parce que le premier a appliqué était un Char et donc le deuxième a doit également être un Char .

On voit qu’en mappant des fonctions à plusieurs paramètres sur des foncteurs, on obtient des foncteurs qui contiennent des fonctions. Que peut-on faire de ceux- ci ? Pour commencer, on peut mapper sur ceux-ci des fonctions qui prennent en paramètre une fonction, parce que ce qui est dans le foncteur sera donné à la fonction mappée comme paramètre.

ghci> let a = fmap (*) [1,2,3,4]

ghci> :t a

a :: [Integer -> Integer]

ghci> fmap (\f -> f 9) a [9,18,27,36]

Mais si l’on a une valeur fonctorielle Just (3 *) et une autre valeur fonctorielle Just 3, et qu’on souhaite sortir la fonction de Just (3 *) pour la mapper sur Just 5 ? Avec des foncteurs normaux, on est bloqué, parce qu’ils ne supportent que la mappage de fonctions normales sur des foncteurs. Même lorsqu’on a mappé \f -> f 9 sur un foncteur qui contenait une fonction, on ne mappait qu’une fonction normale sur celui-ci. Mais fmap ne nous permet pas de mapper une fonction qui est dans un foncteur sur un autre foncteur. On pourrait filtrer par motif sur le constructeur Just pour récupérer la fonction, puis la mapper sur

Just 5 , mais on voudrait une solution plus générale et abstraite à ce problème, qui fonctionnerait pour tous les foncteurs.

Je vous présente la classe de types Applicative. Elle réside dans le module Control.Applicative et définit deux méthodes, pure et <*>. Elle ne fournit pas d’implémentation par défaut pour celles-ci, il faut donc les définir toutes deux nous-mêmes si l’on veut faire de quelque chose un foncteur applicatif. La classe est définie ainsi :

class (Functor f) => Applicative f where

pure :: a -> f a

(<*>) :: f (a -> b) -> f a -> f b

Ces trois petites lignes de définition nous disent beaucoup de choses ! Commençons par la première ligne. Elle débute la définition de la classe Applicative et introduit une contrainte de classe. Elle dit que si l‘on veut faire d’un constructeur de types une instance d’ Applicative, il doit d‘abord être membre de Functor. C’est pourquoi, si l’on sait qu’un constructeur de type est membre d’ Applicative, alors c’est aussi un Functor, et on peut donc utiliser fmap sur lui.

La première méthode qu’elle définie est appelée pure. Sa déclaration de type est pure :: a -> f a. f joue le rôle de notre instance de foncteur applicatif. Puisqu’Haskell a un très bon système de types, et puisqu’une fonction ne peut que prendre des paramètres et retourner une valeur, on peut dire beaucoup de choses avec une déclaration de type, et ceci n’est pas une exception. pure prend une valeur de n’importe quel type, et retourne un foncteur applicatif

encapsulant cette valeur en son intérieur. Quand on dit en son intérieur , on se réfère à nouveau à l’analogie de la boîte, bien qu’on ait vu qu’elle ne soit pas toujours la plus à même d’expliquer ce qu’il se trame. La déclaration a -> f a est tout de même plutôt descriptive. On prend une valeur et on l’enveloppe dans

un foncteur applicatif.

Une autre façon de penser à pure consiste à dire qu’elle prend une valeur et la met dans un contexte par défaut (ou pur) - un contexte minimal qui retourne cette valeur.

La fonction <*> est très intéressante. Sa déclaration de type est f (a -> b) -> f a -> f b. Cela ne vous rappelle rien ? Bien sûr,

fmap :: (a -> b) -> f a -> f b . C’est un peu un fmap amélioré. Alors que fmap prend une fonction et un foncteur, et applique cette fonction à l’intérieur du foncteur, <*> prend un foncteur contenant une fonction et un autre foncteur, et d’une certaine façon extrait la fonction du premier foncteur pour la mapper sur l’autre foncteur. Quand je dis extrait, je veux en fait presque dire exécute puis extrait, peut-être même séquence. On verra pourquoi bientôt.

Regardons l‘implémentation de l’instance d’ Applicative de Maybe.

instance Applicative Maybe where

pure = Just

Nothing <*> _ = Nothing

(Just f) <*> something = fmap f something

Encore une fois, de la définition de la classe, on voit que le f qui joue le rôle du foncteur applicatif doit prendre un type concret en paramètre, donc on écrit instance Applicative Maybe where plutôt que instance Applicative (Maybe a) where .

Tout d’abord, pure. On a dit précédemment qu’elle était supposée prendre quelque chose et l’encapsuler dans un foncteur applicatif. On a écrit pure = Just, parce que les constructeurs de valeurs comme Just sont des fonctions normales. On aurait pu écrire pure x = Just x.

Ensuite, on a la définition de <*>. On ne peut pas extraire de fonction d‘un Nothing, parce qu’il ne contient rien. Donc si l’on essaie d’extraire une fonction d’un Nothing , le résultat est Nothing . Si vous regardez la définition de classe d’ Applicative , vous verrez qu’il y a une classe de contrainte Functor , qui signifie que les deux paramètres de <*> sont des foncteurs. Si le premier paramètre n’est pas un Nothing, mais un Just contenant une fonction, on veut mapper cette fonction sur le second paramètre. Ceci prend également en compte le cas où le second paramètre est Nothing, puisque faire fmap sur un Nothing retourne

Nothing .

Donc, pour Maybe, <*> extrait la fonction de la valeur de gauche si c’est un Just et la mappe sur la valeur de droite. Si n’importe lequel des paramètres est Nothing , le résultat est Nothing .

Ok, cool, super. Testons cela.

ghci> Just (+3) <*> Just 9 Just 12

ghci> pure (+3) <*> Just 10 Just 13

ghci> pure (+3) <*> Just 9 Just 12

ghci> Just (++"hahah") <*> Nothing Nothing

ghci> Nothing <*> Just "woot" Nothing

On voit que faire pure (+3) où Just (+3) est équivalent dans ce cas. Utilisez pure quand vous utilisez des valeurs Maybe dans un contexte applicatif

(i.e. quand vous les utilisez avec <*>), sinon utilisez Just. Les quatre premières lignes entrées montrent comment la fonction extraite est mappée, mais dans ces exemples, elle aurait tout aussi bien pu être mappée immédiatement sur les foncteurs. La dernière ligne est intéressante parce qu’on essaie d’extraire une

fonction d’un Nothing et de le mapper sur quelque chose, ce qui résulte en un Nothing évidemment.

Avec des foncteurs normaux, on peut juste mapper une fonction sur un foncteur, et on ne peut plus récupérer ce résultat hors du foncteur de manière générale, même lorsque le résultat est une fonction appliquée partiellement. Les foncteurs applicatifs, quant à eux, permettent d’opérer sur plusieurs foncteurs avec la même fonction. Regardez ce bout de code :

ghci> pure (+) <*> Just 3 <*> Just 5 Just 8

ghci> pure (+) <*> Just 3 <*> Nothing Nothing

ghci> pure (+) <*> Nothing <*> Just 5 Nothing

Que se passe-t-il là ? Regardons, étape par étape. <*> est associatif à gauche, ce qui signifie que

pure (+) <*> Just 3 <*> Just 5 est équivalent à (pure (+) <*> Just 3) <*> Just 5 . Tout d’abord, la fonction + est mise dans un foncteur, qui est dans ce cas une valeur Maybe. Donc, au départ, on a pure (+) qui est égal à

Just (+) . Ensuite, Just (+) <*> Just 3 a lieu. Le résultat est Just (3+) . Ceci à cause de l’application partielle. Appliquer la fonction + seulement sur 3 résulte en une fonction qui prend un paramètre, et lui ajoute 3. Finalement,

Just (3+) <*> Just 5 est effectuée, ce qui résulte en Just 8 .

N’est-ce pas génial !? Les foncteurs applicatifs et le style applicatif d’écrire pure f <*> x <*> y <*> … nous permettent de prendre une fonction qui attend des paramètres qui ne sont pas nécessairement enveloppés dans des foncteurs, et

d’utiliser cette fonction pour opérer sur des valeurs qui sont dans des contextes fonctoriels. La fonction peut prendre autant de paramètres qu’on le souhaite, parce qu’elle est partiellement appliquée étape par étape, à chaque occurrence de <*>.

Cela devient encore plus pratique et apparent si l’on considère le fait que pure f <*> x est égal à fmap f x. C’est une des lois des foncteurs applicatifs. Nous les regarderons plus en détail plus tard, pour l’instant, on peut voir intuitivement que c’est le cas. Pensez-y, ça tombe sous le sens. Comme on l’a dit plus tôt,

pure place une valeur dans un contexte par défaut. Si l’on place une fonction dans un contexte par défaut, et qu’on l’extrait de ce contexte pour l’appliquer à une valeur dans un autre foncteur applicatif, on a fait la même chose que de juste mapper cette fonction sur le second foncteur applicatif. Plutôt que d’écrire

pure f <*> x <*> y <*> … , on peut écrire fmap f x <*> y <*> … . C’est pourquoi Control.Applicative exporte une fonction <$> qui est simplement fmap en tant qu’opérateur infixe. Voici sa définition :

(<$>) :: (Functor f) => (a -> b) -> f a -> f b

f <$> x = fmap f x

Yo ! Petit rappel : les variables de types sont indépendantes des noms des paramètres ou des noms des valeurs en général. Le f de la déclaration de la fonction ici est une variable de type avec une contrainte de classe disant que tout type remplaçant f doit être membre de la classe Functor. Le f dans le corps de la fonction dénote une fonction qu’on mappe sur x. Le fait que f soit utilisé pour représenter ces deux choses ne signifie pas qu’elles représentent la même chose.

En utilisant <$>, le style applicatif devient brillant, parce qu’à présent, si l’on veut appliquer une fonction f sur trois foncteurs applicatifs, on peut écrire f <$> x <*> y <*> z. Si les paramètres n’étaient pas des foncteurs applicatifs mais des valeurs normales, on aurait écrit f x y z.

Regardons cela de plus près. On a une valeur Just "johntra" et une valeur Just "volta", et on veut joindre les deux en une String dans un foncteur Maybe . On fait cela :

ghci> (++) <$> Just "johntra" <*> Just "volta" Just "johntravolta"

Avant qu’on se penche là-dessus, comparez la ligne ci-dessus avec celle-ci :

ghci> (++) "johntra" "volta" "johntravolta"

Génial ! Pour utiliser une fonction sur des foncteurs applicatifs, parsemez quelques <$> et <*> et la fonction opérera sur des foncteurs applicatifs et retournera un foncteur applicatif.

Quand on fait (++) <$> Just "johntra" <*> Just "volta", (++), qui a pour type (++) :: [a] -> [a] -> [a] est tout d’abord mappée sur Just "johntra" , résultant en une valeur comme Just ("johntra"++) ayant pour type Maybe ([Char] -> [Char]) . Remarquez comme le premier paramètre de (++) a été avalé et les a sont devenus des Char. À présent, Just ("johntra"++) <*> Just "volta" est exécuté, ce qui sort la fonction du

Just et la mappe sur Just "volta" , retournant Just "johntravolta" . Si l’une de ces deux valeurs était un Nothing , le résultat serait Nothing .

Jusqu‘ici, on a seulement regardé Maybe dans nos exemples, et vous vous dites peut-être que les foncteurs applicatifs sont juste pour les Maybe. Il y a beaucoup d’autres instances d’ Applicative, alors découvrons en plus !

Les listes (ou plutôt, le constructeur de types listes, []) sont des foncteurs applicatifs. Quelle surprise ! Voici l‘instance d’ Applicative de [] :

instance Applicative [] where

pure x = [x]

fs <*> xs = [f x | f <- fs, x <- xs]

Plus tôt, on a dit que pure prenait des valeurs, et les plaçait dans un contexte par défaut. En d’autres mots, dans un contexte minimal qui retourne cette valeur. Le contexte minimal pour les listes serait une liste vide, [], mais la liste vide représente l’absence de valeurs, donc elle ne peut pas contenir l’élément qu’on passe à pure. C’est pourquoi pure prend un élément, et le place dans une liste singleton. De manière similaire, le contexte minimal du foncteur applicatif Maybe aurait été Nothing, mais comme il représentait l’absence de valeurs, pure était implémenté avec Just.

ghci> pure "Hey" :: [String] ["Hey"]

Just "Hey"

Qu’en est-il de <*> ? Si on imagine le type de <*> si elle était limitée aux listes, ce serait (<*>) :: [a -> b] -> [a] -> [b]. On l’implémente à l’aide d’une liste en compréhension. <*> doit d’une certaine façon extraire la fonction de son paramètre de gauche, et la mapper sur son paramètre de droite. Mais ici, la liste de gauche peut contenir zéro, une ou plusieurs fonctions. La liste de droite peut elle aussi contenir plusieurs valeurs. C’est pourquoi on utilise une liste en

compréhension pour piocher dans les deux listes. On applique toutes les fonctions possibles de la liste de gauche sur toutes les valeurs possibles de la liste de droite. La liste résultante contient toutes les combinaisons possibles d’application d’une fonction de la liste de gauche sur une valeur de la liste de droite.

ghci> [(*0),(+100),(^2)] <*> [1,2,3] [0,0,0,101,102,103,1,4,9]

La liste de gauche contient trois fonctions, et la liste de droite contient trois valeurs, donc la liste résultante contient neuf éléments. Chaque fonction de la liste de gauche est appliquée à chaque valeur de la liste de droite. Si l’on a une liste de fonctions qui prennent deux paramètres, on peut les appliquer entre deux listes.

ghci> [(+),(*)] <*> [1,2] <*> [3,4] [4,5,5,6,3,4,6,8]

Puisque <*> est associative à gauche, [(+), (*)] <*> [1, 2] est exécuté en premier, résultant en une liste équivalente à [(1+), (2+), (1*), (2*)], parce que chaque fonction à gauche est appliquée sur chaque valeur de droite. Puis, [(1+),(2+),(1*),(2*)] <*> [3,4] est exécuté, produisant le résultat final.

Utiliser le style applicatif avec les listes est fun ! Regardez :

ghci> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."]

["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."]

À nouveau, remarquez qu’on a utilisé une fonction normale qui prend deux chaînes de caractères entre deux foncteurs applicatifs à l’aide des opérateurs applicatifs appropriés.

Vous pouvez imaginer les listes comme des calculs non déterministes. Une valeur comme 100 ou "what" peut être vu comme un calcul déterministe qui ne renvoie qu’un seul résultat, alors qu’une liste [1, 2, 3] peut être vue comme un calcul qui ne peut pas se décider sur le résultat qu’il doit avoir, et nous présente donc tous les résultats possibles. Donc, quand vous faites quelque chose comme (+) <$> [1, 2, 3] <*> [4, 5, 6], vous pouvez imaginer ça comme la somme de deux calculs non déterministes avec +, qui produit ainsi un nouveau calcul non déterministe encore moins certain de son résultat.

Le style applicatif sur les listes est souvent un bon remplaçant des listes en compréhension. Dans le deuxième chapitre, on souhaitait connaître tous les produits possibles de [2, 5, 10] et [8, 10, 11], donc on a fait :

ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]] [16,20,22,40,50,55,80,100,110]

On pioche simplement dans deux listes et on applique la fonction à toutes les combinaisons d’éléments. Cela peut être fait dans le style applicatif :

ghci> (*) <$> [2,5,10] <*> [8,10,11] [16,20,22,40,50,55,80,100,110]

Cela me paraît plus clair, parce qu’il est plus simple de voir qu’on appelle seulement * entre deux calculs non déterministes. Si l’on voulait tous les produits possibles de deux listes supérieurs à 50, on ferait :

ghci> filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11] [55,80,100,110]

Il est facile de voir comment pure f <*> xs est égal à fmap f xs sur les listes. pure f est juste [f], et [f] <*> xs appliquera chaque fonction de la liste de gauche à chaque valeur de la liste de droite, mais puisqu’il n’y a qu’une fonction à gauche, c’est comme mapper.

Une autre instance d’ Applicative qu’on a déjà rencontrée est IO. Voici comment son instance est implémentée :

instance Applicative IO where

pure = return a <*> b = do

f <- a x <- b

Puisque pure ne fait que mettre des valeurs dans un contexte minimal qui puisse toujours renvoyer ce résultat, il est sensé que pure soit juste return , parce que c’est exactement ce que fait return : elle crée une action I/O qui ne fait rien, mais retourne la valeur passée en résultat, sans rien écrire sur le terminal ni écrire dans un fichier.

Si <*> était spécialisée pour IO, elle aurait pour type (<*>) :: IO (a -> b) -> IO a -> IO b. Elle prendrait une action I/O qui retourne une fonction en résultat, et une autre action I/O, et retournerait une action I/O à partir de ces deux, qui,

lorsqu’elle serait exécutée, effectuerait d’abord la première action pour obtenir la fonction, puis effectuerait la seconde pour obtenir une valeur, et retournerait le résultat de la fonction appliquée à la valeur. On utilise la syntaxe do pour l’implémenter ici. Souvenez-vous, la syntaxe do prend plusieurs actions I/O et les colle les unes aux autres, ce qui est exactement ce que l’on veut ici.

Avec Maybe et [], on pouvait imaginer que <*> extrayait simplement une fonction de son paramètre de gauche et l‘appliquait d’une certaine façon sur son paramètre de droite. Avec IO, l’extraction est toujours de la partie, mais on a également une notion d’ordonnancement, parce qu’on prend deux actions I/O et qu’on les ordonne, en les collant l’une à l’autre. On doit extraire la fonction de la première action I/O, mais pour pouvoir l’extraire, il faut exécuter l’action.

Considérez ceci : myAction :: IO String myAction = do a <- getLine b <- getLine return $ a ++ b

C’est une action I/O qui demande à l’utilisateur d’entrer deux lignes, et retourne en résultat ces deux lignes concaténées. Ceci est obtenu en collant deux actions I/O getLine ensemble avec un return, afin que le résultat soit a ++ b. Une autre façon d’écrire cela en style applicatif serait :

myAction :: IO String

myAction = (++) <$> getLine <*> getLine

Ce qu’on faisait précédemment, c’était créer une action I/O qui appliquait une fonction entre les résultats de deux actions I/O, et ici c’est pareil. Souvenez-vous, getLine est une action I/O qui a pour type getLine :: IO String . Quand on utilise <*> entre deux foncteurs applicatifs, le résultat est un foncteur applicatif, donc tout va bien.

Le type de l’expression (++) <$> getLine <*> getLine est IO String, ce qui signifie que cette expression est une action I/O comme une autre, qui contient