• Aucun résultat trouvé

On a déjà parlé des foncteurs dans leur propre petite section. Si vous ne l’avez pas encore lue, vous devriez probablement le faire à présent, ou plus tard, quand vous aurez plus de temps. Ou faire semblant de l’avoir lue.

Ceci étant, un petit rappel : les foncteurs sont des choses sur lesquelles on peut mapper, comme des listes, des MaybeMaybe , des arbres, et d’autres. En Haskell, ils sont définis par la classe de types Functor , qui n’a qu’une méthode de classe de type,Functor

fmap

fmap , ayant pour type fmap :: (a -> b) -> f a -> f bfmap :: (a -> b) -> f a -> f b . Cela dit : donne- moi une fonction qui prend un aa et retourne un bb , et une boîte avec un (ou plusieurs)

a

a à l’intérieur, et je te donnerai une boîte avec un (ou plusieurs) bb à l’intérieur. Elle applique grosso-modo la fonction aux éléments dans la boîte.

Un conseil. Souvent, l’analogie de la boîte aide à se faire une intuition de la façon dont fonctionnent les foncteurs, et plus tard, on utilisera

probablement la même analogie pour les foncteurs applicatifs et les monades. C’est une analogie correcte pour aider les débutants à comprendre les foncteurs, mais ne la prenez pas trop littéralement, parce que pour certains foncteurs, l’analogie est fortement tirée par les cheveux. Un terme plus correct pour définir ce qu’est un foncteur serait un contexte de calcul. Le contexte peut être que le calcul peut avoir renvoyé une valeur ou échoué (MaybeMaybe et Either aEither a ) ou que le calcul renvoie plusieurs valeurs (les listes), ce genre de choses.

Si on veut faire d’un constructeur de types une instance de FunctorFunctor , il doit avoir pour sorte * -> ** -> * , ce qui signifie qu’il doit prendre exactement un type concret en paramètre de type. Par exemple, MaybeMaybe peut être une instance parce qu’il prend un paramètre de type pour produire un type concret, comme Maybe IntMaybe Int ou Maybe StringMaybe String . Si un constructeur de types prend deux paramètres, comme EitherEither , il faut l’appliquer partiellement jusqu’à ce qu’il ne prenne plus qu’un paramètre de type. Ainsi, on ne peut pas écrire instance Functor Either where , maisinstance Functor Either where on peut écrire instance Functor (Either a) where , et alors en imaginant que fmapinstance Functor (Either a) where fmap ne fonctionne que pour les Either aEither a , elle aurait pour déclaration de type fmap :: (b -> c) -> Either a b -> Either a c . Comme vous pouvez le voir, la partie Either afmap :: (b -> c) -> Either a b -> Either a c Either a est fixée, parce que Either a ne prend qu’un paramètre de type, alors qu’ EitherEither a Either en prend deux, et ainsi

fmap :: (b -> c) -> Either b -> Either c

fmap :: (b -> c) -> Either b -> Either c ne voudrait rien dire.

On sait à présent comment plusieurs types (ou plutôt, des constructeurs de types) sont des instances de Functor , comme []Functor [] , MaybeMaybe , Either a

Either a et un type TreeTree qu’on a créé nous-mêmes. On a vu comment l’on pouvait mapper des fonctions sur ceux-ci pour notre plus grand bien. Dans cette section, on va découvrir deux autres instances de foncteurs, IOIO et (->) r(->) r .

Si une valeur a pour type, mettons, IO String , cela signifie que c’est une action I/O qui, lorsqu’elle est exécutée, ira dans le monde réel et nousIO String récupèrera une chaîne de caractères, qu’elle rendra comme son résultat. On peut utiliser <-<- dans la syntaxe do pour lier ce résultat à un nom. On a mentionné que les actions I/O sont comme des boîtes avec des petits pieds qui sortent chercher des valeurs dans le monde à l’extérieur pour nous. On peut inspecter ce qu’elles ont ramené, mais après inspection, on doit les envelopper à nouveau dans IO . En pensant à cette analogie deIO boîte avec des petits pieds, on peut voir comment IOIO agit comme un foncteur.

Voyons comment faire d’IOIO une instance de FunctorFunctor . Quand on fmapfmap une fonction sur une action I/O, on veut obtenir une action I/O en retour qui fait la même chose, mais applique notre fonction sur la valeur résultante.

instance Functor IO where fmap f action = do result <- action return (f result)

Le résultat du mappage de quelque chose sur une action I/O sera une action I/O, donc on utilise immédiatement la notation do pour coller deux actions en une. Dans l’implémentation de fmapfmap , on crée une nouvelle action I/O qui commence par exécuter l’action I/O originale, et on appelle son résultat resultresult . Puis, on fait return (f result)return (f result) . returnreturn est, comme vous le savez, une fonction qui crée une action I/O qui ne fait rien, mais présente un résultat. L’action produite par un bloc do aura toujours pour résultat celui de sa dernière action. C’est pourquoi on utilise returnreturn pour créer une action I/O qui ne fait pas grand chose, mais présente f result comme le résultat de l’action I/O composée.f result

On peut jouer un peu avec pour se faire une intuition. C’est en fait assez simple. Regardez ce code :

main = do line <- getLine

let line' = reverse line

putStrLn $ "You said " ++ line' ++ " backwards!"

putStrLn $ "Yes, you really said" ++ line' ++ " backwards!"

On demande une ligne à l’utilisateur, et on la lui rend, mais renversée. Voici comment réécrire ceci en utilisant fmap :fmap

main = do line <- fmap reverse getLine

putStrLn $ "You said " ++ line ++ " backwards!"

putStrLn $ "Yes, you really said" ++ line ++ " backwards!"

Tout comme on peut fmap reverse sur Just "blah"fmap reverse Just "blah" pour obtenir Just "halb"Just "halb" , on peut fmap reverse

fmap reverse sur getLinegetLine . getLinegetLine est une action I/O qui a pour type IO StringIO String et mapper reverse

reverse sur elle nous donne une action I/O qui va aller dans le monde réel et récupérer une ligne, puis appliquer reversereverse dessus. Tout comme on peut appliquer une fonction à quelque chose enfermé dans une boîte Maybe , on peut appliquer une fonction à quelque chose enfermé dans uneMaybe boîte IOIO , seulement la boîte doit toujours aller dans le monde réel pour obtenir quelque chose. Ensuite, lorsqu’on la lie à quelque chose avec <- , le nom sera lié au résultat auquel reverse<- reverse aura déjà été appliquée.

L’action I/O fmap (++"!") getLine se comporte comme getLinefmap (++"!") getLine getLine , mais ajoute toujours "!""!" à son résultat !

Si on regarde ce que le type de fmap serait si elle était limitée à IOfmap IO , ce serait fmap :: (a -> b) -> IO a -> IO bfmap :: (a -> b) -> IO a -> IO b . fmapfmap prend une fonction et une action I/O et retourne une nouvelle action I/O comme l’ancienne, mais avec la fonction appliquée à son résultat.

Si jamais vous liez le résultat d’une action I/O à un nom, juste pour ensuite appliquer une fonction à ce nom et lui donner un nouveau nom, utilisez plutôt fmap , ce sera plus joli. Si vous voulez appliquez des transformations multiples à une donnée dans un foncteur, vous pouvez soit déclarerfmap une fonction dans l’espace de nom global, soit utiliser une lambda expression, ou idéalement, utiliser la composition de fonctions :

import Data.Char

main = do line <- fmap (intersperse '-' . reverse . map toUpper) getLine putStrLn line

$ runhaskell fmapping_io.hs

hello there

E-R-E-H-T- -O-L-L-E-H

Comme vous le savez probablement, intersperse '-' . reverse . map toUpperintersperse '-' . reverse . map toUpper est une fonction qui prend une chaîne de caractères, mappe toUpper sur celle-ci, applique reversetoUpper reverse au résultat, et applique intersperse '-'intersperse '-' à ce résultat. C’est comme écrire

(\xs -> intersperse '-' (reverse (map toUpper xs)))

(\xs -> intersperse '-' (reverse (map toUpper xs))) , mais plus joli.

Une autre instance de Functor qu’on a utilisée tout du long sans se douter qu’elle était un foncteur est (->) rFunctor (->) r . Vous êtes sûrement un peu perdu à présent, qu’est-ce que ça veut dire (->) r(->) r ? Le type de fonctions r -> ar -> a peut être réécrit (->) r a(->) r a , tout comme 2 + 32 + 3 peut être réécrit (+) 2 3(+) 2 3 . Quand on regarde (->) r a(->) r a , on peut voir (->)(->) sous un nouveau jour, et s’apercevoir que c’est juste un constructeur de types qui prend deux paramètres de types, tout comme EitherEither . Mais souvenez-vous, on a dit qu’un constructeur de types doit prendre exactement un paramètre pour être une instance de FunctorFunctor . C’est pourquoi (->)(->) ne peut pas être une instance de FunctorFunctor , mais si on l’applique partiellement en (->) r(->) r , ça ne pose plus de problème. Si la syntaxe nous permettait d’appliquer partiellement les constructeurs de types avec des sections (comme l’on peut partiellement appliquer + en faisant (2+)+ (2+) , qui est équivalent à (+) 2(+) 2 ), vous pourriez réécrire (->) r(->) r comme (r ->) . Comment est-ce que les fonctions sont-elles des foncteurs ? Eh bien, regardons l’implémentation, qui se trouve dans(r ->)

Control.Monad.Instances Control.Monad.Instances .

Généralement, on indique une fonction qui prend n’importe quoi et retourne n’importe quoi a -> b . r -> aa -> b r -> a est identique, on a juste choisi d’autres lettres pour les variables de type.

instance Functor ((->) r) where fmap f g = (\x -> f (g x))

Si la syntaxe le permettait, on aurait pu écrire : instance Functor (r ->) where fmap f g = (\x -> f (g x))

Mais elle ne le permet pas, donc on doit l’écrire de la première façon.

Tout d’abord, pensons au type de fmap . C’est fmap :: (a -> b) -> f a -> f bfmap fmap :: (a -> b) -> f a -> f b . À présent, remplaçons mentalement les ff , qui ont pour rôle d’être notre foncteur, par des (->) r . On fait cela pour voir comment fmap(->) r fmap doit se comporter pour cette instance. On obtient

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

fmap :: (a -> b) -> ((->) r a) -> ((->) r b) . Maintenant, on peut réécrire (->) r a(->) r a et (->) r b(->) r b de manière infixe en r -> a

r -> a et r -> br -> b , comme on l’écrit habituellement pour les fonctions. On a donc fmap :: (a -> b) -> (r -> a) -> (r -> b)fmap :: (a -> b) -> (r -> a) -> (r -> b) . Hmmm OK. Mapper une fonction sur une fonction produit une fonction, tout comme mapper une fonction sur un MaybeMaybe produit un MaybeMaybe et mapper une fonction sur une liste produit une liste. Qu’est-ce que le type fmap :: (a -> b) -> (r -> a) -> (r -> b)fmap :: (a -> b) -> (r -> a) -> (r -> b) nous indique-t-il ? Eh bien, on voit que la fonction prend une fonction de a vers ba b et une fonction de rr vers aa , et retourne une fonction de rr vers bb . Cela ne vous rappelle rien ? Oui ! La composition de fonctions ! On connecte la sortie de r -> ar -> a à l’entrée de a -> ba -> b pour obtenir une fonction r -> br -> b , ce qui est exactement ce que fait la composition de fonctions. Si vous regardez comment l’instance est définie ci-dessus, vous verrez qu’on a juste composé les fonctions. Une autre façon d’écrire cette instance serait :

instance Functor ((->) r) where fmap = (.)

Cela rend évident le fait qu’utiliser fmap sur des fonctions sert juste à composer. Faites :m + Control.Monad.Instancesfmap :m + Control.Monad.Instances , puisque c’est là que l’instance est définie, et essayez de jouer à mapper sur des fonctions.

ghci> :t fmap (*3) (+100)

ghci> fmap (*3) (+100) 1 303 ghci> (*3) `fmap` (+100) $ 1 303 ghci> (*3) . (+100) $ 1 303

ghci> fmap (show . (*3)) (*100) 1

"300"

On peut appeler fmap de façon infixe pour souligner la ressemblance avec .fmap . . Dans la deuxième ligne d’entrée, on mappe (*3)(*3) sur (+100)(+100) , ce qui retourne une fonction qui prend une entrée, appelle (+100)(+100) sur celle-ci, puis appelle (*3)(*3) sur ce résultat. On appelle cette fonction sur la valeur 11 .

Est-ce que l’analogie des boîtes fonctionne toujours ici ? Avec un peu d’imagination, oui. Quand on fait fmap (+3) sur Just 3fmap (+3) Just 3 , il est facile d’imaginer le Maybe boîte qui a un contenu sur lequel on applique la fonction (+3)Maybe (+3) . Mais qu’en est-il quand on fait fmap (*3) (+100)fmap (*3) (+100) ? Eh bien, vous pouvez imaginer (+100)(+100) comme une boîte qui contient son résultat futur. Comme on imaginait une action I/O comme une boîte qui irait chercher son résultat dans le monde réel. Faire fmap (*3) sur (+100)fmap (*3) (+100) crée une autre fonction qui se comporte comme (+100)(+100) , mais avant de produire son résultat, applique (*3) dessus. Ainsi, on voit que fmap(*3) fmap se comporte comme .. pour les fonctions.

Le fait que fmap soit la composition de fonctions quand elle est utilisée sur des fonctions n’est pas très utile pour l’instant, mais c’est tout du moinsfmap intéressant. Cela tord aussi un peu notre esprit et nous fait voir comment des choses agissant plutôt comme des calculs que comme des boîtes (tel

IO

IO et (->) r(->) r ) peuvent elles aussi être des foncteurs. La fonction mappée sur un calcul agit comme ce calcul, mais modifie son résultat avec cette fonction.

Avant de regarder les règles que fmapfmap doit respecter, regardons encore une fois son type. Celui-ci est fmap :: (a -> b) -> f a -> f bfmap :: (a -> b) -> f a -> f b . Il manque la contrainte de classe (Functor f) => , mais on l’oublie par(Functor f) => concision, parce qu’on parle de foncteurs donc on sait ce que signifie ff . Quand on a découvert les fonctions curryfiées, on a dit que toutes les fonctions Haskell prennent un unique paramètre. Une fonction

a -> b -> c

a -> b -> c ne prend en réalité qu’un paramètre aa et retourne une fonction b -> c , qui prend un paramètre et retourne un cb -> c c . C’est pourquoi, si l’on appelle une fonction avec trop peu de paramètres (c’est- à-dire qu’on l’applique partiellement), on obtient en retour une fonction qui prend autant de paramètres qu’il en manquait (on repense à nouveau à nos fonctions comme prenant plusieurs paramètres). Ainsi,

a -> b -> c

a -> b -> c peut être écrit a -> (b -> c)a -> (b -> c) pour faire apparaître la curryfication.

Dans la même veine, si l’on écrit

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

fmap :: (a -> b) -> (f a -> f b) , on peut imaginer fmapfmap non pas comme une fonction qui prend une fonction et un foncteur pour retourner un foncteur, mais plutôt comme une fonction qui prend une fonction, et retourne une nouvelle fonction, similaire à l’ancienne, mais qui

prend et retourne des foncteurs. Elle prend une fonction a -> ba -> b , et retourne une fonction f a -> f bf a -> f b . On dit qu’on lifte la fonction. Jouons avec cette idée en utilisant la commande :t de GHCi ::t

ghci> :t fmap (*2)

fmap (*2) :: (Num a, Functor f) => f a -> f a

ghci> :t fmap (replicate 3)

fmap (replicate 3) :: (Functor f) => f a -> f [a]

L’expression fmap (*2) est une fonction qui prend un foncteur ffmap (*2) f sur des nombres, et retourne un foncteur sur des nombres. Ce foncteur peut être une liste, un MaybeMaybe , un Either StringEither String , peu importe. L’expression fmap (replicate 3)fmap (replicate 3) prend un foncteur de n’importe quel type et retourne un foncteur sur des listes d’éléments de ce type.

Quand on dit foncteur sur des nombres, vous pouvez imaginer un foncteur qui contient des nombres. La première version est un peu plus sophistiquée et techniquement correcte, mais la seconde est plus simple à saisir.

Ceci est encore plus apparent si l’on applique partiellement, mettons, fmap (++"!")fmap (++"!") et qu’on lie cela à un nom dans GHCi.

Vous pouvez imaginer fmap soit comme une fonction qui prend une fonction et un foncteur, et mappe cette fonction sur le foncteur, ou bien commefmap une fonction qui prend une fonction et la lifte en une fonction sur des foncteurs. Les deux visions sont correctes et équivalentes en Haskell. Le type fmap (replicate 3) :: (Functor f) => f a -> f [a]fmap (replicate 3) :: (Functor f) => f a -> f [a] signifie que la fonction marchera sur n’importe quel foncteur. Ce qu’elle fera exactement dépendra du foncteur en question. Si on utilise fmap (replicate 3)fmap (replicate 3) sur une liste, l’implémentation de fmapfmap pour les listes sera choisie, c’est-à-dire map . Si on l’utilise sur Maybe amap Maybe a , cela appliquera replicate 3replicate 3 à la valeur dans le JustJust , alors qu’un NothingNothing restera un Nothing .Nothing

ghci> fmap (replicate 3) [1,2,3,4] [[1,1,1],[2,2,2],[3,3,3],[4,4,4]]

ghci> fmap (replicate 3) (Just 4) Just [4,4,4]

ghci> fmap (replicate 3) (Right "blah") Right ["blah","blah","blah"]

ghci> fmap (replicate 3) Nothing Nothing

ghci> fmap (replicate 3) (Left "foo") Left "foo"

Maintenant, nous allons voir les lois des foncteurs. Afin que quelque chose soit un foncteur, il doit satisfaire quelques lois. On attend de tous les foncteurs qu’ils présentent certaines propriétés et comportements fonctoriels. Ils doivent se comporter de façon fiable comme des choses sur lesquelles on peut mapper. Appeler fmapfmap sur un foncteur devrait seulement mapper une fonction sur le foncteur, rien de plus. Ce comportement est décrit dans les lois des foncteurs. Il y en a deux, et toute instance de Functor doit les respecter. Elles ne sont cependant pas vérifiéesFunctor automatiquement par Haskell, il faut donc les tester soi-même.

La première loi des foncteurs dit que si l’on mappe la fonction idid sur un foncteur, le foncteur retourné doit être identique au

foncteur original. Si on écrit cela plus formellement, cela signifie que fmap id = id. En gros, cela signifie que si l’on fait fmap idfmap id sur un foncteur, cela doit être pareil que de faire simplement id sur ce foncteur. Souvenez-vous, idid id est la fonction identité, qui retourne son paramètre à l’identique. Elle peut également être écrite \x -> x . Si l’on voit le foncteur comme quelque chose sur laquelle on peut mapper, alors la loi\x -> x

fmap id peut sembler triviale ou évidente. Voyons si cette loi tient pour quelques foncteurs.

ghci> fmap id (Just 3) Just 3 ghci> id (Just 3) Just 3 ghci> fmap id [1..5] [1,2,3,4,5] ghci> id [1..5] [1,2,3,4,5] ghci> fmap id [] []

ghci> fmap id Nothing Nothing

Si on regarde l’implémentation de fmap , par exemple pour Maybefmap Maybe , on peut se rendre compte que la première loi des foncteurs est respectée. instance Functor Maybe where

fmap f (Just x) = Just (f x) fmap f Nothing = Nothing

Imaginons qu’idid soit à la place de ff dans l’implémentation. On voit que si l’on fmap idfmap id sur Just xJust x , le résultat sera Just (id x)Just (id x) , et puisqu’idid retourne son paramètre à l’identique, on peut en déduire que Just (id x)Just (id x) est égal à Just xJust x . Ainsi, mapper idid sur une valeur

Maybe

Maybe construite avec JustJust retourne la même valeur.

Voir que mapper idid sur une valeur NothingNothing retourne la même valeur est trivial. De ces deux équations de l’implémentation de fmapfmap , on déduit que fmap id = idfmap id = id est vrai.

La seconde loi dit que composer deux fonctions, et mapper le résultat sur un foncteur doit être identique à mapper d’abord une des fonctions sur le foncteur, puis mapper l’autre sur le résultat. Formellement, on veut

fmap (f . g) = fmap f . fmap g. Ou, d’une autre façon, pour tout foncteur F, on souhaite : fmap (f . g) F = fmap f (fmap g F). Si l’on peut montrer qu’un type obéit à ces deux lois des foncteurs, alors on peut avoir l’assurance qu’il aura les mêmes propriétés fondamentales vis-à-vis du mappage. On sait que lorsque l’on fait fmapfmap sur ce type, il ne se passera rien d’autre qu’un mappage, et il se comportera comme une chose sur laquelle on mappe, i.e. un foncteur. On peut voir si un type respecte la seconde loi en regardant l’implémentation de fmapfmap pour ce type, et en utilisant la même méthode qu’on a utilisée pour voir si MaybeMaybe obéissait à la première loi.

Si vous le voulez, on peut vérifier que la seconde loi des foncteurs est vérifiée par Maybe

Maybe . Si on fait fmap (f . g)fmap (f . g) sur NothingNothing , on obtient NothingNothing , parce que quelle que soit la fonction mappée sur NothingNothing , on obtient NothingNothing . De même, faire fmap f (fmap g Nothing)

fmap f (fmap g Nothing) retourne NothingNothing , pour la même raison. OK, voir que la seconde loi tient pour une valeur Nothing de MaybeNothing Maybe était plutôt facile, presque trivial.

Et si c’est une valeur Just something ? Eh bien, si l’on fait fmap (f . g) (Just x)Just something fmap (f . g) (Just x) , on voit de l’implémentation que c’est Just ((f . g) x)

Just ((f . g) x) , qui est, évidemment, Just (f (g x))Just (f (g x)) . Si l’on fait fmap f (fmap g (Just x))fmap f (fmap g (Just x)) , on voit que fmap g (Just x)fmap g (Just x) est Just (g x)Just (g x) . Donc, fmap f (fmap g (Just x))fmap f (fmap g (Just x)) est égal à fmap f (Just (g x))fmap f (Just (g x)) , et de l’implémentation, on voit que cela est égal à Just (f (g x)) .Just (f (g x))

Si vous êtes un peu perdu dans cette preuve, ne vous inquiétez pas. Soyez sûr de bien comprendre comme fonctionne la composition de fonctions. Très souvent, on peut voir intuitivement que ces lois sont respectées parce que le type se comporte comme un conteneur ou comme une fonction. Vous pouvez aussi essayer sur un tas de valeurs et vous convaincre que le type suit bien les lois.

Intéressons-nous au cas pathologique d’un constructeur de types instance de Functor mais qui n’est pas vraiment un foncteur, parce qu’il neFunctor satisfait pas les lois. Mettons qu’on ait un type :

data CMaybe a = CNothing | CJust Int a deriving (Show)

Le C ici est pour compteur. C’est un type de données qui ressemble beaucoup à Maybe a , mais la partie JustMaybe a Just contient deux champs plutôt qu’un. Le premier champ du constructeur de valeurs CJust aura toujours pour type IntCJust Int , et ce sera une sorte de compteur, et le second champ