Vous êtes sur la page 1sur 210

Table of Content Table of Content Introduction

propos de ce tutoriel Donc, quest-ce quHaskell ? Ce dont vous avez besoin avant de plonger

1 4
4 4 5

Dmarrons
Prts, feu, partez ! Nos premires fonctions Introduction aux listes Texas ranges Je suis une liste en comprhension Tuples

6
6 8 9 12 13 15

Types et classes de types


Faites confiance aux types Variables de type Classes de types 101

18
18 19 20

Syntaxe des fonctions


Filtrage par motif Gardes, gardes ! O !? Let it be Expressions case

24
24 26 28 29 30

Rcursivit
Bonjour rcursivit ! Maximum de fun Un peu plus de fonctions rcursives Vite, triez ! Penser rcursif

32
32 32 33 34 35

Fonctions dordre suprieur


Fonctions curryfies lordre du jour : de lordre suprieur Maps et filtres Lambdas Plie mais ne rompt pas Appliquer des fonctions avec $ Composition de fonctions

37
37 38 40 42 43 46 46

Modules
Charger des modules Data.List Data.Char Data.Map Data.Set Crer nos propres modules

49
49 50 56 59 62 64

Crer nos propres types et classes de types


Introduction aux types de donnes algbriques Syntaxe des enregistrements Paramtres de types Instances drives Synonymes de types Structures de donnes rcursives Classes de types 102 Une classe de types oui-non La classe de types Functor Sortes et un peu de type-fu

67
67 69 71 73 76 78 81 84 85 87

Entres et Sorties
Hello, world! Fichiers et flots Arguments de ligne de commande Alatoire Chanes doctets Exceptions

91
91 98 106 109 114 116

Rsoudre des problmes fonctionnellement


Calculatrice de notation polonaise inverse DHeathrow Londres

121
121 123

Foncteurs, foncteurs applicatifs et monodes


Foncteurs revisits Foncteurs applicatifs Le mot-cl newtype
Utiliser newtype pour crer des instances de classes de types De la paresse de newtype type vs. newtype vs. data

129
129 134 144
145 146 147

Monodes

148

Les listes sont des monodes Product et Sum Any et All Le monode Ordering Maybe le monode Utiliser des monodes pour plier des structures de donnes

149 150 151 152 153 154

Pour une poigne de monades


Trempons-nous les pieds avec Maybe La classe de types Monad Le funambule Notation do La monade des listes
La qute dun cavalier

157
158 159 160 164 167
170

Les lois des monades


Composition gauche par return Composition droite par return Associativit

172
172 172 173

Et pour quelques monades de plus


Lui crire ? Je la connais peine !
Monodes la rescousse Le type Writer Utiliser la notation do avec Writer Ajouter de la tenue de registre nos programmes Construction inefficace de listes Listes diffrentielles Comparer les performances

175
175
176 177 178 179 180 180 182

La lire ? Pas cette blague encore. Calculs tats dans tous leurs tats
Piles et rochers La monade State Alatoire et monade dtats

182 184
184 185 187

Erreur, erreur, ma belle erreur Quelques fonctions monadiques utiles


liftM et ses amies La fonction join filterM foldM Crer une calculatrice NPI scurise Composer des fonctions monadiques

188 189
189 191 192 193 194 196

Crer des monades

197

Zippeurs
Une petite balade Une trane de miettes
Retourner en haut Manipuler des arbres sous focalisation Im going straight to the top, oh yeah, up where the air is fresh and clean!

200
200 202
202 204 204

Se focaliser sur des listes Un systme de fichiers lmentaire


Un zippeur pour notre systme de fichiers Manipuler notre systme de fichiers

205 206
206 208

Attention la marche

208

Introduction
Table des matires Dmarrons

propos de ce tutoriel
Bienvenue dans Apprendre Haskell vous fera le plus grand bien ! Si vous lisez ceci, il est possible que vous souhaitiez apprendre Haskell. Vous tes au bon endroit, mais discutons un peu de ce tutoriel avant tout. Jai dcid dcrire ce tutoriel parce que je voulais consolider ma propre connaissance dHaskell, et parce que je pensais pouvoir aider les dbutants lapprendre selon mon point de vue. Il y a plusieurs tutoriels traitant de Haskell qui tranent sur Internet. Quand je dbutais en Haskell, je nai pas appris que dune seule source. Ma mthode dapprentissage consistait lire plusieurs tutoriels et articles, car chacun deux expliquait dune manire un peu diffrente de lautre. En confrontant plusieurs ressources, jai pu assembler les pices du puzzle et tout sest mis sa place. Ceci est une tentative visant produire une ressource de plus afin dapprendre Haskell, de sorte que vous ayez plus de chance den trouver une que vous apprciez. Ce tutoriel vise les personnes ayant de lexprience dans un langage de programmation imprative (C, C++, Java, Python, ) mais qui nont jamais programm dans un langage de programmation fonctionnel auparavant (Haskell, ML, OCaml, ). Bien que je me doute que mme sans vraie exprience en programmation, une chouette personne comme vous pourra suivre et apprendre Haskell. Le canal #haskell sur le rseau freenode est un bon endroit pour poser des questions si vous tes bloqu (NdT: Le canal #haskell-fr du mme rseau vous permettra de poser vos questions en franais). Les gens y sont extrmement gentils, patients et comprhensifs envers les dbutants. Jai chou dans mon apprentissage de Haskell peu prs deux fois avant darriver le saisir, car cela me semblait trop bizarre et je ne comprenais pas. Mais alors, jai eu le dclic, et aprs avoir pass cet obstacle initial, le reste est venu plutt facilement. Ce que jessaie de vous dire : Haskell est gnial, et si vous tes intresss par la programmation, vous devriez lapprendre mme si cela semble bizarre au dbut. Apprendre Haskell est trs similaire lapprentissage de la programmation la premire fois - cest fun ! a vous force penser diffremment, ce qui nous amne la section suivante

Donc, quest-ce quHaskell ?


Haskell est un langage de programmation fonctionnel pur . Dans les langages impratifs, vous effectuez des choses en donnant lordinateur une squence de tches, et il les excute. Lors de cette excution, il peut changer dtat. Par exemple, vous affectez une variable a la valeur 5, puis vous faites quelque chose, puis lui affectez une autre valeur. Vous avez des structures de contrle de flot pour rpter des actions plusieurs fois. Dans un langage de programmation fonctionnel pur, vous nindiquez pas lordinateur quoi faire, mais plutt ce que les choses sont. La factorielle dun nombre est le produit de tous les nombres de 1 celui-ci, la somme dune liste de nombres est gale au premier de ceux-ci additionn la somme des autres, etc. Vous exprimez ceci sous la forme de fonctions. galement, vous naffectez pas une valeur une variable, puis une autre valeur plus tard. Si vous dites que a est gal 5, vous ne pouvez pas dire quil est gal autre chose plus tard, car vous venez de dire quil tait gal 5. Quoi, vous tes un menteur ? Donc, dans les langages fonctionnels purs, une fonction na pas deffets de bord. La seule chose quune fonction puisse faire, cest calculer quelque chose et retourner le rsultat. Au dpart, cela semble plutt limitant, mais il savre que a a de trs intressantes consquences : si une fonction est appele deux fois, avec les mmes paramtres, alors il est garanti quelle renverra le mme rsultat. On appelle ceci la transparence rfrentielle, et cela permet non seulement au compilateur de raisonner propos du comportement du programme, mais galement vous de dduire facilement (et mme de prouver) quune fonction est correcte, puis de construire des fonctions plus complexes en collant de simples fonctions lune lautre. Haskell est paresseux. Cela signifie qu moins de lui demander explicitement, Haskell nexcutera pas les fonctions et ne calculera pas les choses tant quil nest pas forc de nous montrer les rsultats. Cela va bien de pair avec la transparence rfrentielle et permet de penser ses programmes comme des transformations sur des donnes. Cela permet aussi des choses cool comme des structures de donnes infinies. Supposons que vous ayez une liste de nombres immuable xs = [1, 2, 3, 4, 5, 6, 7, 8] et une fonction doubleMe qui multiplie chaque lment par 2 et retourne la nouvelle liste. Si nous voulions multiplier notre liste par 8 dans un langage impratif en faisant doubleMe(doubleMe(doubleMe(xs))) , il traverserait probablement la liste une fois, ferait une copie, et la retournerait. Puis, il traverserait cette liste encore deux fois et retournerait le rsultat. Dans un langage paresseux, appeler doubleMe sur une liste sans la forcer safficher rsulte en gros ce que le programme vous dise Ouais ouais, je le ferai plus tard !. Mais ds que vous voulez voir le rsultat, le premier doubleMe dit au deuxime quil a besoin du rsultat, maintenant ! Le deuxime le dit son tour au troisime, et le troisime, avec rticence, renvoie un 1 doubl, qui est donc un 2. Le deuxime reoit cela, et renvoie un 4 au premier. Le premier voit cela, et vous rpond que le premier lment est un 8. Ainsi, la liste nest traverse quune fois, et seulement quand vous en avez vraiment

besoin. De cette faon, lorsque vous voulez quelque chose dun langage paresseux, vous pouvez juste prendre les donnes initiales, et efficacement les transformer de toutes les faons possibles jusqu arriver vos fins. Haskell est typ statiquement. Lorsque vous compilez votre programme, le compilateur sait quel bout de code est un nombre, lequel est une chane de caractres, etc. Cela veut dire que beaucoup derreurs potentielles sont dtectes la compilation. Si vous essayez de sommer un nombre et une chane de caractres, le compilateur vous criera dessus. Haskell utilise un systme de types trs bon qui bnficie de linfrence de types. Cela signifie que vous navez pas marquer explicitement chaque bout de code avec un type, car le systme de types peut intelligemment comprendre beaucoup de choses. Si vous dites a = 5 + 4 , pas besoin dindiquer Haskell que a est un nombre, il peut sen rendre compte tout seul. Linfrence de types permet galement votre code dtre plus gnral. Si une fonction que vous faites prend deux paramtres et les somme lun lautre, et que vous ne prcisez rien sur leur type, la fonction marchera sur nimporte quelle paire de paramtres qui se comportent comme des nombres. Haskell est lgant et concis. Grce son usage de concepts de haut niveau, les programmes Haskell sont gnralement plus courts que leurs quivalents impratifs. Et des programmes plus courts sont plus simples maintenir et ont moins de bogues. Haskell a t cr par des personnes trs intelligentes (ayant un doctorat). Le travail sur Haskell a dbut en 1987 lorsquun comit de chercheurs sest rassembl pour concevoir un langage rvolutionnaire. En 2003, le rapport Haskell fut publi, et dfinit une version stable du langage.

Ce dont vous avez besoin avant de plonger


Un diteur de texte, et un compilateur Haskell. Vous avez probablement dj un diteur de texte favori, donc ne perdons pas de temps avec a. Pour les besoins de ce tutoriel, nous utiliserons GHC, le compilateur Haskell le plus utilis. Le meilleur moyen de dmarrer est de tlcharger la plate-forme Haskell, qui contient simplement un Haskell prt tourner. GHC peut prendre un script Haskell (gnralement avec lextension .hs) et le compiler, mais il a aussi un mode interactif qui permet dinteragir interactivement avec les scripts. Interactivement. Vous pouvez appeler des fonctions depuis les scripts que vous chargez, et les rsultats seront immdiatement affichs. Pour apprendre, cest beaucoup plus simple et rapide que de compiler chaque fois que vous faites une modification, et ensuite relancer le programme depuis votre terminal. Le mode interactif est invoqu en tapant ghci dans votre terminal. Si vous avez dfini des fonctions dans un fichier, par exemple myfunctions.hs , vous pouvez les charger en tapant :l myfunctions , puis jouer avec, condition que myfunctions.hs soit dans le dossier do ghci a t invoqu. Si vous modifiez le script .hs, tapez juste :l myfunctions nouveau, ou encore :r , qui est quivalent puisquil recharge le script courant. Ma procdure habituelle quand je joue avec des choses est de dfinir des fonctions dans un fichier a.hs, de les charger, et de bidouiller avec, puis de retourner changer le .hs, recharger, etc. Cest aussi ce quon va faire prsent.

Table des matires

Dmarrons

Dmarrons
Introduction Table des matires Types et classes de types

Prts, feu, partez !


Bien, dmarrons ! Si vous tes le genre de personne horrible qui ne lit pas les introductions et que vous lavez saute, vous feriez peut-tre bien de tout de mme lire la dernire section de lintroduction, car elle explique ce dont vous avez besoin pour suivre ce tutoriel et comment lon va charger des fonctions. La premire chose quon va faire, cest lancer un GHC interactif et appeler quelques fonctions pour se faire une premire ide de Haskell. Ouvrez donc votre terminal et tapez ghci . Vous serez accueilli par un message semblable celui-ci. GHCi, version 6.8.2: http://www.haskell.org/ghc/ Loading package base ... linking ... done. Prelude> :? for help

Bravo, vous tes dans GHCi ! Linvite Prelude> peut devenir de plus en plus longue lorsquon importera des choses dans la session, alors on va la remplacer par ghci> . Si vous voulez la mme invite, tapez simplement :set prompt "ghci> " . Voici un peu darithmtique lmentaire. ghci> 17 ghci> 4900 ghci> 420 ghci> 2.5 ghci> 2 + 15 49 * 100 1892 - 1472 5 / 2

Cest plutt simple. Nous pouvons galement utiliser plusieurs oprateurs sur la mme ligne, et les rgles de prcdence habituelles sappliquent alors. On peut galement utiliser des parenthses pour rendre la prcdence explicite, ou laltrer. ghci> (50 * 100) - 4999 1 ghci> 50 * 100 - 4999 1 ghci> 50 * (100 - 4999) -244950

Plutt cool, non ? Ouais, je sais que cest pas terrible, mais supportez-moi encore un peu. Attention, un cueil viter ici est la ngation des nombres. Pour obtenir un nombre ngatif, il est toujours mieux de lentourer de parenthses. Faire 5 * -3 vous causera les fureurs de GHCi, alors que 5 * (-3) fonctionnera sans souci. Lalgbre boolenne est galement simple. Comme vous le savez srement, && reprsente un et boolen, || un ou boolen, et not retourne la ngation de True et False (NdT: respectivement vrai et faux). ghci> False ghci> True ghci> True ghci> True ghci> False True && False True && True False || True not False not (True && True)

Un test dgalit scrit ainsi : ghci> 5 == 5

ghci> True ghci> False ghci> False ghci> True ghci> True

5 == 5 1 == 0 5 /= 5 5 /= 4 "hello" == "hello"

Et que se passe-t-il si lon fait 5 + "llama" ou 5 == True ? Eh bien, si on essaie le premier, on obtient un gros message derreur effrayant ! No instance for (Num [Char]) arising from a use of `+' at <interactive>:1:0-9 Possible fix: add an instance declaration for (Num [Char]) In the expression: 5 + "llama" In the definition of `it': it = 5 + "llama"

Beurk ! Ce que GHCi essaie de nous dire, cest que "llama" nest pas un nombre, et donc quil ne sait pas ladditionner 5. Mme si ce ntait pas "llama" mais "four" ou mme "4" , Haskell ne considrerait pas cela comme un nombre. + attend comme oprandes droite et gauche des nombres. Si on essayait de faire True == 5 , GHCi nous dirait que les types ne correspondent pas. Ainsi, + ne fonctionne quavec des paramtres pouvant tre considrs comme des nombres, alors que == marche sur nimporte quelles deux choses condition quon puisse les comparer. Le pige, cest quelles doivent tre toutes les deux du mme type de choses. On ne compare pas des pommes et des oranges. Nous nous intresserons de plus prs aux types plus tard. Notez : vous pouvez tout de mme faire 5 + 4.0 car 5 est vicieux et peut se faire passer pour un entier ou pour un nombre virgule flottante. 4.0 ne peut pas se faire passer pour un entier, donc cest 5 de sadapter lui. Vous ne le savez peut-tre pas, mais nous venons dutiliser des fonctions tout du long. Par exemple, * est une fonction qui prend deux nombres et les multiplie entre eux. Comme vous lavez constat, on lappelle en le mettant en sandwich entre ces paramtres. Cest pour a quon dit que cest une fonction infixe. La plupart des fonctions quon nutilise pas avec des nombres sont des fonctions prfixes. Intressons-nous celles-ci. Les fonctions sont gnralement prfixes, donc partir de maintenant, nous ne prciserons pas quune fonction est prfixe, on le supposera par dfaut. Dans la plupart des langages impratifs, les fonctions sont appeles en crivant le nom de la fonction, puis ses paramtres entre parenthses, gnralement spars par des virgules. En Haskell, les fonctions sont appeles en crivant le nom de la fonction, puis un espace, puis ses paramtres, spars par des espaces. Par exemple, essayons dappeler une des fonctions les plus ennuyantes dHaskell. ghci> succ 8 9

La fonction succ prend nimporte quoi qui a un successeur, et renvoie ce successeur. Comme vous pouvez le voir, on spare le nom de la fonction du paramtre par un espace. Appeler une fonction avec plusieurs paramtres est aussi simple. Les fonctions min et max prennent deux choses quon peut ordonner (comme des nombres !). min retourne la plus petite, max la plus grande. Voyez vous-mme : ghci> min 9 10 9 ghci> min 3.4 3.2 3.2 ghci> max 100 101 101

Lapplication de fonction (appeler une fonction en mettant un espace aprs puis ses paramtres) a la plus grande des prcdences. Cela signifie pour nous que les deux dclarations suivantes sont quivalentes. ghci> succ 9 + max 5 4 + 1 16 ghci> (succ 9) + (max 5 4) + 1 16

Cependant, si nous voulions obtenir le successeur du produit des nombres 9 et 10, on ne pourrait pas crire succ 9 * 10 , car cela chercherait le successeur de 9, et le multiplierait par 10. Donc 100. Nous devrions crire succ (9 * 10) pour obtenir 91. Si une fonction prend deux paramtres, on peut aussi lappeler de faon infixe en lentourant dapostrophes renverses. Par exemple, la fonction div prend deux entiers et effectue leur division entire. Faire div 92 10 retourne 9. Mais quand on lappelle de cette faon, on peut se demander quel nombre est divis par quel nombre. On peut donc lcrire plutt 92 `div` 10 , ce qui est tout de suite plus clair.

Beaucoup de personnes venant de langages impratifs ont pour habitude de penser que les parenthses indiquent lapplication de fonctions. Par exemple, en C, on utilise des parenthses pour appeler des fonctions comme foo() , bar(1) ou baz(3, "haha") . Comme nous lavons vu, les espaces sont utiliss pour lapplication de fonctions en Haskell. Donc, en Haskell, on crirait foo , bar 1 et baz 3 "haha" . Si vous voyez quelque chose comme bar (bar 3) , cela ne veut donc pas dire que bar est appel avec les paramtres bar et 3 . Cela signifie quon appelle la fonction bar avec un paramtre 3 pour obtenir un nombre, et quon appelle bar nouveau sur ce nombre. En C, cela serait bar(bar(3)) .

Nos premires fonctions


Dans la section prcdente, nous avons eu un premier aperu de lappel de fonctions. Essayons maintenant de crer les ntres ! Ouvrez votre diteur de texte favori et entrez cette fonction qui prend un nombre et le multiplie par deux. doubleMe x = x + x

Les fonctions sont dfinies de la mme faon quelles sont appeles. Le nom de la fonction est suivi de ses paramtres, spars par des espaces. Mais lors de la dfinition dune fonction, un = suivi de la dfinition de ce que la fonction fait suivent. Sauvez ceci en tant que baby.hs ou quoi que ce soit. prsent, naviguez jusqu lendroit o vous lavez sauvegard, et lancez ghci dici. Une fois lanc, tapez :l baby . Maintenant que notre script est charg, on peut jouer avec la fonction que lon vient de dfinir. ghci> :l baby [1 of 1] Compiling Main Ok, modules loaded: Main. ghci> doubleMe 9 18 ghci> doubleMe 8.3 16.6

( baby.hs, interpreted )

Puisque + fonctionne sur des entiers aussi bien que sur des nombres virgule flottante (tout ce que lon peut considrer comme un nombre en fait), notre fonction fonctionne galement sur nimporte quel nombre. Crons une fonction prenant deux nombres et les multipliant chacun par deux, puis les sommant ensemble. doubleUs x y = x*2 + y*2

Simple. Nous aurions galement pu lcrire doubleUs x y = x + x + y + y . Un test produit les rsultats attendus (rappelez-vous bien dajouter cette fonction la fin de baby.hs , de sauvegarder le fichier, et de faire :l baby dans GHCi). ghci> doubleUs 4 9 26 ghci> doubleUs 2.3 34.2 73.0 ghci> doubleUs 28 88 + doubleMe 123 478

Comme attendu, vous pouvez appeler vos propres fonctions depuis les autres fonctions que vous aviez cres. Avec cela en tte, redfinissons doubleUs ainsi : doubleUs x y = doubleMe x + doubleMe y

Ceci est un exemple simple dun motif rcurrent en Haskell. Crer des fonctions basiques, qui sont visiblement correctes, puis les combiner pour faire des fonctions plus complexes. De cette manire, on vite la rptition. Que se passerait-il si un mathmaticien se rendait compte que 2 est en fait 3 et quil fallait changer votre programme ? Vous pourriez simplement rdfinir doubleMe comme x + x + x et, puisque doubleUs appelle doubleMe , elle fonctionnerait automatiquement dans cet trange nouveau monde o 2 est 3. Les fonctions en Haskell nont pas tre dans un ordre particulier, donc il nimporte pas que vous dfinissiez doubleMe puis doubleUs ou linverse. Maintenant, nous allons crire une fonction qui multiplie un nombre par 2, mais seulement si ce nombre est infrieur ou gal 100, parce que les nombres suprieurs 100 sont dj bien assez gros comme a ! doubleSmallNumber x = if x > 100 then x else x*2

Ici, nous avons introduit la construction if de Haskell. Vous tes probablement habitu aux constructions if des autres langages. La diffrence entre le if de Haskell et celui des autres langages, cest quen Haskell, le else est obligatoire. Dans les langages impratifs, vous pouvez sauter quelques tapes si la condition nest pas satisfaite, mais en Haskell, chaque expression doit renvoyer quelque

chose. Nous aurions aussi pu crire ce if en une ligne mais je trouve cette version plus lisible. Un autre point noter est que la construction if en Haskell est une expression. Une expression correspond simplement tout bout de code retournant une valeur. 5 est une expression car elle retourne 5, 4 + 8 est une expression, x + y est une expression car elle retourne la somme de x et y . Puisque le else est obligatoire, une construction if retournera toujours quelque chose, cest pourquoi cest une expression. Si nous voulions ajouter 1 chaque nombre produit dans la fonction prcdente, nous aurions pu lcrire ainsi. doubleSmallNumber' x = (if x > 100 then x else x*2) + 1

Si nous avions omis les parenthses, nous aurions ajout 1 seulement si x tait plus petit que 100. Remarquez le ' la fin du nom de la fonction. Cette apostrophe na pas de signification spciale en Haskell. Cest un caractre valide utiliser dans un nom de fonction. On utilise habituellement ' pour indiquer la version stricte dune fonction (une version qui nest pas paresseuse) ou pour la version lgrement modifie dune fonction ou dune variable. Puisque ' est un caractre valide dans le nom dune fonction, on peut crire : conanO'Brien = "It's a-me, Conan O'Brien!"

Il y a deux choses noter ici. La premire, cest que dans le nom de la fonction, nous navons pas mis de majuscule au prnom de Conan. Cest parce que les fonctions ne peuvent pas commencer par une majuscule. Nous verrons pourquoi un peu plus tard. La seconde chose, cest que la fonction ne prend aucun paramtre. Lorsquune fonction ne prend pas de paramtre, on dit gnralement que cest une dfinition (ou un nom). Puisquon ne peut pas changer ce que les noms (et les fonctions) signifient une fois quon les a dfinis, conanO'Brien et la chane "It's a-me, Conan O'Brien!" peuvent tre utiliss de manire interchangeable.

Introduction aux listes


Tout comme les listes de courses dans le monde rel, les listes Haskell sont trs utiles. Cest la structure de donnes la plus utilise, et elle peut ltre dune multitude de faons pour modliser et rsoudre tout un tas de problmes. Les listes sont TROP gniales. Dans cette section, nous allons dcouvrir les bases des listes, des chanes de caractres (qui sont en fait des listes) et des listes en comprhension. En Haskell, les listes sont des structures de donnes homognes. Elles contiennent plusieurs lments du mme type. Cela signifie quon peut avoir une liste dentiers, une liste de caractres, mais jamais une liste qui a la fois des entiers et des caractres. Place une liste !

Note : Nous utilisons le mot-cl let pour dfinir un nom directement dans GHCi. crire let a = 1 dans GHCi est quivalent crire a = 1 dans un script puis charger ce script.

ghci> let lostNumbers = [4,8,15,16,23,42] ghci> lostNumbers [4,8,15,16,23,42]

Comme vous pouvez le constater, les listes sont dnotes par des crochets, et les valeurs dune liste sont spares par des virgules. Si on essayait une liste comme [1, 2, 'a', 3, 'b', 'c', 4] , Haskell se plaindrait que des caractres (qui, dailleurs, sont dnots comme un caractre entour dapostrophes) ne sont pas des nombres. En parlant de caractres, les chanes de caractres sont simplement des listes de caractres. "hello" est simplement du sucre syntaxique pour ['h', 'e', 'l', 'l', 'o'] . Puisque les chanes de caractres sont des listes, on peut utiliser les fonctions de listes sur celles-ci, ce qui savre trs pratique. Une tche courante consiste coller deux listes lune lautre. Ceci est ralis laide de loprateur ++ . ghci> [1,2,3,4] ++ [9,10,11,12] [1,2,3,4,9,10,11,12] ghci> "hello" ++ " " ++ "world" "hello world" ghci> ['w','o'] ++ ['o','t'] "woot"

Attention lors de lutilisation rpte de loprateur ++ sur de longues chanes. Lorsque vous accolez deux listes (et mme si vous accolez une liste singleton une autre liste, par exemple : [1, 2, 3] ++ [4] ), en interne, Haskell doit parcourir la liste de gauche en entier. Ceci ne pose pas de problme tant que les listes restent de taille raisonnable. Mais accoler une liste la fin dune liste qui contient cinquante millions dlments risque de prendre du temps. Cependant, placer quelque chose au dbut dune liste en utilisant loprateur : (aussi appel loprateur cons) est instantan. ghci> 'A':" SMALL CAT" "A SMALL CAT"

ghci> 5:[1,2,3,4,5] [5,1,2,3,4,5]

Remarquez comme : prend un nombre et une liste de nombres, ou bien un caractre et une liste de caractres, alors que ++ prend deux listes. Mme si vous voulez ajouter un seul lment la fin dune liste avec ++ , vous devez lentourer de crochets pour en faire dabord une liste. [1, 2, 3] est en fait du sucre syntaxique pour 1:2:3:[] . [] est la liste vide. Si nous ajoutons 3 devant elle, elle devient [3] . Si nous ajoutons encore 2 devant, elle devient [2, 3] , etc.

Note : [] , [[]] et [[], [], []] sont trois choses diffrentes. La premire est une liste vide, la deuxime est une liste qui contient un lment, cet lment tant une liste vide, la troisime est une liste qui contient trois lments, qui sont tous des listes vides.

Si vous voulez obtenir un lment dune liste par son index, utilisez !! . Les indices dmarrent 0. ghci> "Steve Buscemi" !! 6 'B' ghci> [9.4,33.2,96.2,11.2,23.25] !! 1 33.2

Mais si vous essayez dobtenir le sixime lment dune liste qui nen a que quatre, vous obtiendrez une erreur, soyez donc vigilant ! Les listes peuvent aussi contenir des listes. Elles peuvent mme contenir des listes qui contiennent des listes ghci> let b = [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]] ghci> b [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]] ghci> b ++ [[1,1,1,1]] [[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3],[1,1,1,1]] ghci> [6,6,6]:b [[6,6,6],[1,2,3,4],[5,3,3,3],[1,2,2,3,4],[1,2,3]] ghci> b !! 2 [1,2,2,3,4]

Plusieurs listes lintrieur dune liste peuvent avoir des longueurs diffrentes, mais elles ne peuvent pas tre de types diffrents. Tout comme lon ne peut pas avoir une liste contenant quelques caractres et quelques nombres, on ne peut pas avoir de liste qui contient quelques listes de caractres et quelques listes de nombres. Les listes peuvent tre compares si ce quelles contiennent peut tre compar. En utilisant < , <= , > et >= pour comparer des listes, celles-ci sont compares par ordre lexicographique. Dabord, les ttes sont compares. Si elles sont gales, alors les lments en deuxime position sont compars, etc. ghci> True ghci> True ghci> True ghci> True ghci> True [3,2,1] > [2,1,0] [3,2,1] > [2,10,100] [3,4,2] > [3,4] [3,4,2] > [2,4] [3,4,2] == [3,4,2]

Que peut-on faire dautre avec des listes ? Voici quelques fonctions de base qui oprent sur des listes. head prend une liste et retourne sa tte. La tte est simplement le premier lment. ghci> head [5,4,3,2,1] 5

tail prend une liste et retourne sa queue. En dautres termes, elle coupe la tte de la liste. ghci> tail [5,4,3,2,1] [4,3,2,1]

last prend une liste et retourne son dernier lment.

ghci> last [5,4,3,2,1] 1

init prend une liste et retourne tout sauf son dernier lment. ghci> init [5,4,3,2,1] [5,4,3,2]

En imaginant une liste comme un monstre, voil ce que a donne.

Mais que se passe-t-il si lon essaie de prendre la tte dune liste vide ? ghci> head [] *** Exception: Prelude.head: empty list

Mon dieu ! Tout nous pte la figure ! Sil ny a pas de monstre, il ne peut pas avoir de tte. Lors de lutilisation de head , tail , last et init , faites attention ne pas les utiliser sur des listes vides. Cette erreur ne peut pas tre dtecte la compilation, donc il est toujours de bonne pratique de prendre ses prcautions pour ne pas demander Haskell des lments dune liste vide. length prend une liste et retourne sa longueur. ghci> length [5,4,3,2,1] 5

null teste si une liste est vide. Si cest le cas, elle retourne True , sinon False . Utilisez cette fonction plutt que dcrire xs == [] (si votre liste sappelle xs ). ghci> null [1,2,3] False ghci> null [] True

reverse renverse une liste. ghci> reverse [5,4,3,2,1] [1,2,3,4,5]

take prend un nombre et une liste. Elle extrait ce nombre dlments du dbut de la liste. Regardez. ghci> take [5,4,3] ghci> take [3] ghci> take [1,2] ghci> take [] 3 [5,4,3,2,1] 1 [3,9,3] 5 [1,2] 0 [6,6,6]

Voyez comme, si lon essaie de prendre plus dlments que la liste nen contient, elle retourne la liste entire. Si on essaie den prendre 0, on obtient une liste vide. drop marche dune manire similaire, mais elle jette le nombre dlments demand du dbut de la liste.

ghci> drop 3 [8,4,2,1,5,6] [1,5,6] ghci> drop 0 [1,2,3,4] [1,2,3,4] ghci> drop 100 [1,2,3,4] []

maximum prend une liste de choses qui peuvent tre ordonnes et retourne la plus grande dentre elles. minimum retourne la plus petite. ghci> minimum [8,4,2,1,5,6] 1 ghci> maximum [1,9,2,3,4] 9

sum prend une liste de nombres et retourne leur somme. product prend une liste de nombres et retourne leur produit. ghci> sum [5,2,1,6,3,2,5,7] 31 ghci> product [6,2,1,2] 24 ghci> product [1,2,5,6,7,9,2,0] 0

elem prend une chose et une liste de choses, et nous indique si cette premire apparat dans la liste. On lutilise gnralement de manire infixe car cest plus simple lire. ghci> 4 `elem` [3,4,5,6] True ghci> 10 `elem` [3,4,5,6] False

Ctait un premier tour des fonctions qui oprent sur des listes. Nous en verrons dautres plus tard.

Texas ranges
Que faire si lon veut la liste des nombres de 1 20 ? Bien sr, on pourrait les taper un par un, mais ce nest pas une solution pour des gentlemen qui demandent lexcellence de leurs langages de programmation. Nous utiliserons plutt des progressions (NDT : qui sont appeles ranges en anglais, do le titre de cette section). Les progressions sont un moyen de crer des listes qui sont des suites arithmtiques dlments qui peuvent tre numrs. Les nombres peuvent tre numrs. Un, deux, trois, quatre, etc. Les caractres peuvent aussi tre numrs. Lalphabet est une numration des caractres de A Z. Les noms ne peuvent pas tre numrs. Quest-ce qui suit John ? Je ne sais pas. Pour crer une liste contenant tous les entiers naturels de 1 20, on crit [1..20] . Cest quivalent [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] , et il ny a aucune diffrence si ce nest qucrire la squence manuellement est stupide. ghci> [1..20] [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] ghci> ['a'..'z'] "abcdefghijklmnopqrstuvwxyz" ghci> ['K'..'Z'] "KLMNOPQRSTUVWXYZ"

Les progressions sont cool car on peut prciser un pas. Que faire si lon cherche les nombres pairs entre 1 et 20 ? Ou un nombre sur trois ? ghci> [2,4..20] [2,4,6,8,10,12,14,16,18,20] ghci> [3,6..20] [3,6,9,12,15,18]

Il suffit juste de sparer les deux premiers lments par une virgule, puis de spcifier la borne suprieure. Bien que plutt intelligentes, les progressions ne sont pas aussi intelligentes que ce que certaines personnes attendent. Vous ne pouvez pas crire [1, 2, 4, 8, 16..100] en esprant obtenir les puissances de

2. Premirement, parce quon ne peut spcifier quun pas. Secondement, parce que certaines progressions non arithmtiques sont ambiges lorsquon ne les nonce que par leurs premiers lments. Pour crer une liste des nombres de 20 1, vous ne pouvez pas crire [20..1] , vous devez crire [20, 19..1] . Attention lors de lutilisation de nombres virgule flottante dans les progressions ! Ntant pas entirement prcis (par dfinition), leur utilisation peut donner des rsultats originaux. ghci> [0.1, 0.3 .. 1] [0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

Mon avis est de ne pas les utiliser dans les progressions. Vous pouvez aussi utiliser les progressions pour dfinir des listes infinies, simplement en ne prcisant pas de borne suprieure. Nous verrons les dtails des listes infinies un peu plus tard. Pour linstant, examinons comment lon pourrait obtenir les 24 premiers multiples de 13. Bien sr, vous pourriez crire [13, 26..24*13] . Mais il y a une meilleure faon de faire : take 24 [13, 26..] . PuisquHaskell est paresseux, il ne va pas essayer dvaluer la liste infinie immdiatement et ne jamais terminer. Il va plutt attendre de voir ce que vous voulez obtenir de cette liste infinie. Ici, il voit que vous ne voulez que les 24 premiers lments, et il sexcute poliment. Une poigne de fonctions produit des listes infinies : cycle prend une liste et la cycle en une liste infinie. Si vous essayiez dafficher le rsultat, cela continuerait jamais, donc il faut la couper quelque part. ghci> take 10 (cycle [1,2,3]) [1,2,3,1,2,3,1,2,3,1] ghci> take 12 (cycle "LOL ") "LOL LOL LOL "

repeat prend un lment et produit une liste infinie contenant uniquement cet lment. Cela correspond cycler une liste un lment. ghci> take 10 (repeat 5) [5,5,5,5,5,5,5,5,5,5]

Cependant, il est plus simple dutiliser la fonction replicate pour obtenir un certain nombre de fois le mme lment dans une liste. replicate 3 10 retourne [10, 10, 10] .

Je suis une liste en comprhension


Si vous avez dj suivi une classe de mathmatiques, vous avez probablement dj rencontr des ensembles dfinis en comprhension. On les utilise gnralement pour construire des ensembles plus spcifiques partir dautres ensembles plus gnraux. Une comprhension simple dun ensemble qui contient les dix premiers entiers naturels pairs est . La partie situe devant la barre verticale est la fonction qui produit la sortie, x est la variable, N est lensemble en entre et x <= 10 est un prdicat. Cela signifie que lensemble contient le double de tous les entiers naturels qui satisfont le prdicat. Si nous voulions crire cela en Haskell, nous pourrions le faire ainsi take 10 [2,4..] . Mais que faire si lon ne voulait pas les doubles des 10 premiers entiers naturels, mais quelque chose de plus compliqu ? On pourrait utiliser une liste en comprhension pour a. Les listes en comprhension sont trs semblables aux ensembles en comprhension. Restons-en au cas des 10 entiers pairs pour linstant. La liste de comprhension adquate serait [x*2 | x <- [1..10] . x est extrait de [1..10] et pour chaque lment de [1..10] (dsormais attach x ), nous prenons son double. En action. ghci> [x*2 | x <- [1..10]] [2,4,6,8,10,12,14,16,18,20]

Comme vous pouvez le voir, nous obtenons le rsultat attendu. Ajoutons prsent une condition (ou un prdicat) cette comprhension. Les prdicats se placent aprs les liaisons, et sont spars de ceux-ci par une virgule. Disons que lon cherche les lments qui, une fois doubls, sont plus grands ou gaux 12. ghci> [x*2 | x <- [1..10], x*2 >= 12] [12,14,16,18,20]

Cool, a marche. Et si nous voulions tous les nombres de 50 100 dont le reste de la division par 7 est 3 ? Facile. ghci> [ x | x <- [50..100], x `mod` 7 == 3] [52,59,66,73,80,87,94]

Succs ! Notez que nettoyer une liste laide de prdicats sappelle aussi le filtrage . Nous avons pris une liste de nombres et lavons filtre en accord avec le prdicat. Un autre exemple. Disons quon veut une comprhension qui remplace chaque nombre impair plus grand que 10 par "BANG!" et chaque nombre impair plus petit que 10 par "BOOM!" . Si un nombre est pair, on le rejette de la liste. Pour plus de facilit, on va placer cette comprhension dans une fonction, pour pouvoir la rutiliser facilement. boomBangs xs = [ if x < 10 then "BOOM!" else "BANG!" | x <- xs, odd x]

La dernire partie de la comprhension est le prdicat. La fonction odd renvoie True pour un nombre impair, et False pour un nombre pair. Llment est inclus dans la liste seulement si tous les prdicats sont valus True . ghci> boomBangs [7..13] ["BOOM!","BOOM!","BANG!","BANG!"]

On peut inclure plusieurs prdicats. Si nous voulions tous les nombres de 10 20 qui sont diffrents de 13, 15 et 19, on pourrait faire : ghci> [ x | x <- [10..20], x /= 13, x /= 15, x /= 19] [10,11,12,14,16,17,18,20]

Non seulement on peut avoir plusieurs prdicats dans une liste en comprhension (un lment doit satisfaire tous les prdicats pour tre inclus dans la liste rsultante), mais on peut galement piocher dans plusieurs listes. Lorsquon pioche dans plusieurs listes, les comprhensions produisent toutes les combinaisons des listes en entre et joignent tout a laide de la fonction que lon fournit. Une liste produite par une comprhension qui pioche dans deux listes de longueur 4 aura donc pour longueur 16, si tant est quon ne filtre pas dlment. Si lon a deux listes, [2, 5, 10] et [8, 10, 11] , et quon veut les produits possibles des combinaisons de deux nombres de chacune de ces listes, voil ce quon crit : ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]] [16,20,22,40,50,55,80,100,110]

Comme prvu, la longueur de la nouvelle liste est 9. Et si lon voulait seulement les produits suprieurs 50 ? ghci> [ x*y | x <- [2,5,10], y <- [8,10,11], x*y > 50] [55,80,100,110]

Pourquoi pas une comprhension qui combine une liste dadjectifs et une liste de noms pour provoquer une hilarit pique. ghci> let nouns = ["hobo","frog","pope"] ghci> let adjectives = ["lazy","grouchy","scheming"] ghci> [adjective ++ " " ++ noun | adjective <- adjectives, noun <- nouns] ["lazy hobo","lazy frog","lazy pope","grouchy hobo","grouchy frog", "grouchy pope","scheming hobo","scheming frog","scheming pope"]

Je sais ! crivons notre propre version de length ! Appelons-la length' . length' xs = sum [1 | _ <- xs]

_ signifie que lon se fiche de ce quon a pioch dans la liste, donc plutt que dy donner un nom quon nutilisera pas, on crit _ . Cette fonction remplace chaque lment de la liste par un 1 , et somme cette liste. La somme rsultante sera donc la longueur de la liste. Un rappel amical : puisque les chanes de caractres sont des listes, on peut utiliser les listes en comprhension pour traiter et produire des chanes de caractres. Voici une fonction qui prend une chane de caractres et supprime tout sauf les caractres en majuscule. removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

Testons : ghci> removeNonUppercase "Hahaha! Ahahaha!" "HA" ghci> removeNonUppercase "IdontLIKEFROGS" "ILIKEFROGS"

Le prdicat ici fait tout le travail. Il indique que le caractre est inclus dans la nouvelle liste uniquement sil appartient la liste ['A'..'Z'] . Il est possible

dimbriquer les comprhensions si vous oprez sur des listes qui contiennent des listes. Soit une liste qui contient plusieurs listes de nombres. Supprimons tous les nombres impairs sans aplatir la liste. ghci> let xxs = [[1,3,5,2,3,1,2,4,5],[1,2,3,4,5,6,7,8,9],[1,2,4,2,1,6,3,1,3,2,3,6]] ghci> [ [ x | x <- xs, even x ] | xs <- xxs] [[2,2,4],[2,4,6,8],[2,4,2,6,2,6]]

Vous pouvez crire les comprhensions de listes sur plusieurs lignes. Donc, en dehors de GHCi, il vaut mieux les dcouper sur plusieurs lignes, surtout si elles sont imbriques.

Tuples
Dune faon, les tuples sont comme des listes - ils permettent de stocker plusieurs valeurs dans une seule. Cependant, il y a des diffrences fondamentales. Une liste de nombres est une liste de nombres. Cest son type, et cela nimporte pas quelle ait un seul nombre ou une infinit. Les tuples, par contre, sont utiliss lorsque vous savez exactement combien de valeurs vous dsirez combiner, et son type dpend du nombre de composants quil a et du type de ces composantes. Ils sont dnots avec des parenthses, et les composantes sont spares par des virgules. Une autre diffrence cl est quils nont pas tre homognes. Contrairement une liste, un tuple peut contenir une combinaison de diffrents types. Demandez-vous comment on reprsenterait un vecteur bidimensionnel en Haskell. Une possibilit serait dutiliser une liste. a marcherait en partie. Que faire si lon voulait mettre quelques vecteurs dans une liste pour reprsenter les points dune forme du plan ? On pourrait faire [[1, 2], [8, 11], [4, 5]] . Le problme de cette mthode est que lon pourrait galement faire [[1, 2], [8, 11, 5], [4, 5]] , quHaskell accepterait puisque cela reste une liste de listes de nombres, mais a na pas vraiment de sens. Alors quun tuple de taille deux (aussi appel une paire) est un type propre, ce qui signifie quune liste ne peut pas avoir quelques paires puis un triplet (tuple de taille trois), utilisons donc cela en lieu et place. Plutt que dentourer nos vecteurs de crochets, on utilise des parenthses : [(1, 2), (8, 11), (4, 5)] . Et si lon essayait dentrer la forme [(1, 2), (8, 11, 5), (4, 5)] ? Eh bien, on aurait cette erreur : Couldn't match expected type `(t, t1)' against inferred type `(t2, t3, t4)' In the expression: (8, 11, 5) In the expression: [(1, 2), (8, 11, 5), (4, 5)] In the definition of `it': it = [(1, 2), (8, 11, 5), (4, 5)]

Cela nous dit que lon a essay dutiliser une paire et un triplet dans la mme liste, ce qui ne doit pas arriver. Vous ne pourriez pas non plus crer une liste [(1, 2), ("One", 2)] car le premier lment de cette liste est une paire de nombres alors que le second est une paire dune chane de caractres et dun nombre. Les tuples peuvent aussi tre utiliss pour reprsenter un grand ventail de donnes. Par exemple, si nous voulions reprsenter le nom et lge dune personne en Haskell, on pourrait utiliser le triplet : ("Christopher", "Walken", 55) . Comme dans lexemple, les tuples peuvent aussi contenir des listes. Utilisez des tuples lorsque vous savez lavance combien de composantes une donne doit avoir. Les tuples sont beaucoup plus rigides car chaque taille de tuple a son propre type, donc on ne peut pas crire une fonction gnrique pour ajouter un lment un tuple - il faudrait crire une fonction pour ajouter une paire, une fonction pour ajouter un triplet, un fonction pour ajouter un quadruplet, etc. Bien quil y ait des listes singleton, il nexiste pas de tuple singleton. a na pas beaucoup de sens si vous y rflchissez. Un tuple singleton serait seulement la valeur quil contient, et naurait donc pas dintrt. Comme les listes, les tuples peuvent tre compars entre eux si leurs composantes peuvent tre compares. Seulement, vous ne pouvez pas comparer des tuples de tailles diffrentes, alors que vous pouvez comparer des listes de tailles diffrentes. Deux fonctions utiles qui oprent sur des paires : fst prend une paire et renvoie sa premire composante. ghci> fst (8,11) 8 ghci> fst ("Wow", False) "Wow"

snd prend une paire et renvoie sa seconde composante. Quelle surprise ! ghci> snd (8,11) 11 ghci> snd ("Wow", False) False

Note : ces deux fonctions oprent seulement sur des paires. Elles ne marcheront pas sur des triplets, des quadruplets, etc. Nous verrons comment extraire

des donnes de tuples de diffrentes faons un peu plus tard.

Une fonction cool qui produit une liste de paires : zip . Elle prend deux listes et les zippe ensemble en joignant les lments correspondants en des paires. Cest une fonction trs simple, mais elle sert beaucoup. Cest particulirement utile pour combiner deux listes dune faon ou traverser deux listes simultanment. Dmonstration. ghci> zip [1,2,3,4,5] [5,5,5,5,5] [(1,5),(2,5),(3,5),(4,5),(5,5)] ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"] [(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]

Elle met en paires les lments et produit une nouvelle liste. Le premier lment va avec le premier, le deuxime avec le deuxime, etc. Remarquez que, puisque les paires peuvent avoir diffrents types en elles, zip peut prendre deux listes qui contiennent des lments de diffrents types et les zipper. Que se passe-t-il si les longueurs des listes ne correspondent pas ? ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"] [(5,"im"),(3,"a"),(2,"turtle")]

La liste la plus longue est simplement coupe pour correspondre la longueur de la plus courte. PuisquHaskell est paresseux, on peut zipper des listes finies avec des listes infinies : ghci> zip [1..] ["apple", "orange", "cherry", "mango"] [(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

Voici un problme qui combine tuples et listes en comprhensions : quel triangle rectangle a des cts tous entiers, tous infrieurs ou gaux 10, et a un primtre de 24 ? Premirement, essayons de gnrer tous les triangles dont les cts sont infrieurs ou gaux 10 : ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]

On pioche simplement dans trois listes et notre fonction de sortie combine les trois valeurs en triplets. Si vous valuez en le tapant triangles , dans GHCi, vous obtiendrez tous les triangles possibles dont les cts sont infrieurs ou gaux 10. Ensuite, ajoutons une condition, que ceux-ci soient rectangles. On va galement exploiter le fait que b est plus petit que lhypotnuse et que le ct a est plus petit que b. ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2]

Nous y sommes presque. Maintenant, il ne reste plus qu modifier la fonction en disant que lon veut ceux dont le primtre est 24. ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24] ghci> rightTriangles' [(6,8,10)]

Et voil notre rponse ! Cest un schma courant en programmation fonctionnelle. Vous prenez un ensemble de solutions, et vous appliquez des transformations sur ces solutions et les filtrez jusqu obtenir les bonnes.

Introduction

Table des matires

Types et classes de types

Types et classes de types


Dmarrons Table des matires Syntaxe des fonctions

Faites confiance aux types


Nous avons mentionn auparavant quHaskell avait un systme de types statique. Le type de chaque expression est connu la compilation, ce qui mne un code plus sr. Si vous crivez un programme dans lequel vous tentez de diviser un boolen par un nombre, cela ne compilera mme pas. Cest pour le mieux, car il vaut mieux trouver ces erreurs la compilation quobtenir un programme qui plante. En Haskell, tout a un type, donc le compilateur peut raisonner sur votre programme avant mme de le compiler. Au contraire de Java ou Pascal, Haskell a de linfrence des types. Si nous crivons un nombre, nous navons pas dire Haskell que cest un nombre. Il peut linfrer de lui-mme, donc pas besoin de lui crire explicitement les types des fonctions et des expressions pour arriver faire quoi que ce soit. Nous avons vu une partie des bases de Haskell en ne regardant les types que trs superficiellement. Cependant, comprendre le systme de types est une part trs importante de lapprentissage dHaskell. Un type est une sorte dtiquette que chaque expression porte. Il nous indique quelle catgorie de choses cette expression appartient. Lexpression True est un boolen, hello est une chane de caractres, etc. Utilisons prsent GHCi pour examiner le type de quelques expressions. On le fera laide de la commande :t qui, suivie dune expression valide, nous indique son type. Essayons. ghci> :t 'a' 'a' :: Char ghci> :t True True :: Bool ghci> :t "HELLO!" "HELLO!" :: [Char] ghci> :t (True, 'a') (True, 'a') :: (Bool, Char) ghci> :t 4 == 5 4 == 5 :: Bool

Ici on voit que faire :t sur une expression affiche lexpression, suivie de :: et de son type. :: se lit a pour type. Les types explicites sont toujours nots avec une initiale en majuscule. 'a' , semblerait-il, a pour type Char . On en conclue rapidement que cela signifie caractre. True est de type Bool . Cest logique. Mais, et a ? Examiner "HELLO!" renvoie un [Char] . Les crochets indiquent une liste. Nous lisons donc cela liste de caractres. Contrairement aux listes, chaque longueur de tuple a son propre type. Ainsi, lexpression (True, 'a') a pour type (Bool, Char) , alors que lexpression ('a', 'b', 'c') aurait pour type (Char, Char, Char) . 4 == 5 renverra toujours False , son type est Bool . Les fonctions aussi ont des types. Quand on crit nos propres fonctions, on peut vouloir leur donner des dclarations de type explicites. Cest gnralement considr comme une bonne pratique, sauf quand on crit des fonctions courtes. prsent, nous donnerons aux fonctions que nous dclarerons des dclarations de type. Vous souvenez-vous de la comprhension de liste que nous avions crite, qui filtre une chane de caractres pour ne garder que ceux en majuscule ? Voici sa dclaration de type : removeNonUppercase :: [Char] -> [Char] removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]

removeNonUppercase est de type [Char] -> [Char] , ce qui signifie quelle transforme une chane de caractres en une chane de caractres. Parce quelle prend une chane de caractres en paramtre, et en retourne une en rsultat. Le type [Char] est synonyme de String , donc il est plus clair dcrire removeNonUppercase :: String -> String . Nous navons pas eu donner cette fonction de dclaration de type car le compilateur pouvait infrer luimme que la fonction allait de chane de caractres chane de caractres, mais on la quand mme fait. Mais comment crire le type dune fonction qui prend plusieurs paramtres ? Voici une simple fonction qui prend trois entiers et les somme : addThree :: Int -> Int -> Int -> Int addThree x y z = x + y + z

Les paramtres sont spars par des -> et il ny a pas de distinction entre les paramtres et le type retourn. Le type retourn est le dernier lment de la dclaration, et les paramtres sont les trois premiers. Plus tard, nous verrons pourquoi ils sont spars par des -> plutt que dtre distingus plus explicitement, par exemple sous une forme ressemblant Int, Int, Int -> Int ou de ce genre. Si vous voulez donner une dclaration de type une fonction, mais ntes pas certain du type, vous pouvez toujours ne pas lcrire, puis le demander laide de :t . Les fonctions sont galement des expressions, donc :t fonctionne dessus sans souci. Voici un survol des types usuels. Int dsigne les entiers. 7 peut tre un entier, mais 7.2 ne peut pas. Int est born, ce qui signifie quil existe une valeur minimale et une valeur maximale. Habituellement, sur des machines 32-bits, le plus grand Int est 2147483647 et le plus petit -2147483648. Integer dsigne euh, aussi les entiers. La diffrence, cest quil nest pas born, donc il peut tre utilis pour reprsenter des nombres normes. Je veux dire, vraiment normes. Int est cependant plus efficace. factorial :: Integer -> Integer factorial n = product [1..n]

ghci> factorial 50 30414093201713378043612608166064768844377641568960512000000000000

Float est un nombre rel virgule flottante en simple prcision. circumference :: Float -> Float circumference r = 2 * pi * r

ghci> circumference 4.0 25.132742

Double est un nombre rel virgule flottante en double prcision ! circumference' :: Double -> Double circumference' r = 2 * pi * r

ghci> circumference' 4.0 25.132741228718345

Bool est un type boolen. Il ne peut prendre que deux valeurs : True et False . Char reprsente un caractre. Il est dnot par des apostrophes. Une liste de caractres est une chane de caractres. Les tuples sont des types mais dpendent de la taille et du type de leurs composantes, donc il existe thoriquement une infinit de types de tuples, ce qui fait trop pour tre contenu dans ce tutoriel. Remarquez que le tuple vide () est aussi un type, qui ne contient quune seule valeur : () .

Variables de type
Quel est le type de head daprs vous ? Puisque head prend une liste de nimporte quel type et retourne son premier lment, quest-ce que a peut bien tre ? Regardons ! ghci> :t head head :: [a] -> a

Hmmm ! Cest quoi ce a ? Un type ? Rappelez-vous, nous avions dit que les types doivent commencer par une majuscule, donc a ne peut pas tre exactement un type. Puisquil commence par une minuscule, cest une variable de type . Cela signifie que a peut tre de nimporte quel type. Cest trs proche des types gnriques dans dautres langages, seulement en Haskell cest encore plus puissant puisque cela nous permet dcrire des fonctions trs gnrales tant quelles nutilisent pas les spcificits dun type particulier. Des fonctions ayant des variables de type sont dites fonctions polymorphiques. La dclaration de type de head indique quelle prend une liste de nimporte quel type, et retourne un lment de ce type. Bien que les variables de type puissent avoir des noms plus longs quun caractre, gnralement on les note a, b, c, d Vous souvenez-vous de fst ? Elle retourne la premire composante dune paire. Regardons son type.

ghci> :t fst fst :: (a, b) -> a

On voit que fst prend un tuple qui contient deux types et retourne un lment du mme type que celui de la premire composante de la paire. Cest pourquoi on peut utiliser fst sur une paire qui contient nimporte quels deux types. Notez que, le fait que a et b soient deux variables de type diffrentes nimplique pas que les types doivent tre diffrents. Cela indique juste que la premire composante et la valeur retourne ont le mme type.

Classes de types 101


Une classe de types est une sorte dinterface qui dfinit un comportement. Si un type appartient une classe de types, cela signifie quil supporte et implmente le comportement dcrit par la classe. Beaucoup de gens venant de la programmation oriente objet se mprennent penser que les classes de types sont comme des classes de langages orients objet. Ce nest pas le cas. Vous pouvez plutt les penser comme des sortes dinterfaces Java, mais en mieux. Quelle est la signature de type de == ? ghci> :t (==) (==) :: (Eq a) => a -> a -> Bool

Note : loprateur dgalit, == est une fonction. De mme pour + , * , - , / et quasiment tous les oprateurs. Si une fonction ne contient que des caractres spciaux, elle est considre infixe par dfaut. Si nous voulons examiner son type, la passer une autre fonction, ou lappeler de faon prfixe, nous devons lentourer de parenthses.

Intressant. Nous voyons quelque chose de nouveau ici, le symbole => . Tout ce qui se situe avant le => est appel une contrainte de classe . On peut lire la dclaration de type prcdente ainsi : la fonction dgalit prend deux valeurs du mme type et retourne un Bool . Le type de ces valeurs doit tre membre de la classe Eq (ceci est la contrainte de classe). La classe de types Eq offre une interface pour tester lgalit. Tout type pour lequel tester lgalit de deux valeurs a du sens devrait tre membre de la classe de types Eq . Tous les types standards de Haskell lexception dIO (le type qui permet de faire des entres-sorties) et des fonctions font partie de la classe de types Eq . La fonction elem a pour type (Eq a) => a -> [a] -> Bool car elle utilise == sur les lments de la liste pour vrifier si la valeur quon cherche est dedans. Quelques classes de types de base : Eq est utilis pour les types qui supportent un test dgalit. Ses membres doivent implmenter les fonctions == ou /= dans leur dfinition. Donc, si une fonction a une contrainte de classe Eq pour une variable de type, cest que quelque part dans la fonction, elle utilise == ou /= . Tous les types mentionns prcdemment lexception des fonctions sont membres de Eq , donc ils peuvent tre tests gaux. ghci> True ghci> False ghci> True ghci> True ghci> True 5 == 5 5 /= 5 'a' == 'a' "Ho Ho" == "Ho Ho" 3.432 == 3.432

Ord est pour les types qui peuvent tre ordonns. ghci> :t (>) (>) :: (Ord a) => a -> a -> Bool

Tous les types que nous avons couverts jusquici sont membres d Ord . Ord comprend toutes les fonctions usuelles de comparaison, comme > , < , >= et <= . La fonction compare prend deux membres d Ord ayant le mme type et retourne un ordre. Ordering est son type, et les valeurs peuvent tre GT , LT ou EQ , respectivement pour plus grand, plus petit et gal. Pour tre membre d Ord , un type doit dabord tre membre du club prestigieux et exclusif Eq . ghci> "Abrakadabra" < "Zebra" True

ghci> "Abrakadabra" `compare` "Zebra" LT ghci> 5 >= 2 True ghci> 5 `compare` 3 GT

Les membres de Show peuvent tre reprsents par une chane de caractres. Tous les types couverts jusquici sont membres de Show . La fonction la plus utilise en rapport avec la classe Show est show . Elle prend une valeur dun type membre de la classe Show et nous la reprsente sous la forme dune chane de caractres. ghci> show 3 "3" ghci> show 5.334 "5.334" ghci> show True "True"

Read est en quelque sorte loppos de Show . La fonction read prend une chane de caractres, et retourne une valeur dun type membre de Read . ghci> read "True" || False True ghci> read "8.2" + 3.8 12.0 ghci> read "5" - 2 3 ghci> read "[1,2,3,4]" ++ [3] [1,2,3,4,3]

Jusquici tout va bien. Encore une fois, tous les types couverts jusquici sont dans cette classe de types. Mais que se passe-t-il si lon crit juste read "4" ? ghci> read "4" <interactive>:1:0: Ambiguous type variable `a' in the constraint: `Read a' arising from a use of `read' at <interactive>:1:0-7 Probable fix: add a type signature that fixes these type variable(s)

GHCi nous dit quil ne sait pas ce quon attend en retour. Remarquez comme, dans les exemples prcdents de read , on faisait toujours quelque chose du rsultat. Ainsi, GHCi pouvait infrer le type de rsultat attendu de read . Si nous lutilisions comme un boolen, il savait quil fallait retourner un Bool . Mais maintenant, il sait seulement quon attend un type de la classe Read , mais il ne sait pas lequel. Regardons la signature de type de read . ghci> :t read read :: (Read a) => String -> a

Vous voyez ? Il retourne un type membre de Read mais si on nessaie pas de lutiliser ensuite, il na aucun moyen de savoir de quel type il sagit. Pour remdier cela, on peut utiliser des annotations de type explicites. Les annotations de type sont un moyen dexprimer explicitement le type quune expression doit avoir. On le fait en ajoutant :: la fin de lexpression et en spcifiant un type. Observez : ghci> read "5" :: Int 5 ghci> read "5" :: Float 5.0 ghci> (read "5" :: Float) * 4 20.0 ghci> read "[1,2,3,4]" :: [Int] [1,2,3,4] ghci> read "(3, 'a')" :: (Int, Char) (3, 'a')

La plupart des expressions sont telles que le compilateur peut infrer leur type tout seul. Mais parfois, le compilateur ne sait pas sil doit plutt retourner une valeur de type Int ou Float pour une expression comme read "5" . Pour voir le type, Haskell devrait valuer read "5" . Mais puisquil est statiquement typ, il doit connatre les types avant de compiler le code (ou, dans le cas de GHCi, de lvaluer). Donc nous devons dire Haskell : H, cette expression devrait avoir ce type, au cas o tu ne saurais pas !. Les membres d Enum sont des types ordonns squentiellement - on peut les numrer. Lavantage de la classe de types Enum est quon peut lutiliser ses types dans les progressions. Elle dfinit aussi un successeur et un prdcesseur, quon peut obtenir laide des fonctions succ et pred . Les types membres de cette classe sont : () , Bool , Char , Ordering , Int , Integer , Float et Double .

ghci> ['a'..'e'] "abcde" ghci> [LT .. GT] [LT,EQ,GT] ghci> [3 .. 5] [3,4,5] ghci> succ 'B' 'C'

Les membres de Bounded ont une borne suprieure et infrieure. ghci> minBound -2147483648 ghci> maxBound '\1114111' ghci> maxBound True ghci> minBound False :: Int :: Char :: Bool :: Bool

minBound et maxBound sont intressantes car elles ont pour type (Bounded a) => a . Dune certaine faon, ce sont des constantes polymorphiques. Tous les tuples sont membres de Bounded si leurs composantes le sont galement. ghci> maxBound :: (Bool, Int, Char) (True,2147483647,'\1114111')

Num est la classe des types numriques. Ses membres ont pour proprit de pouvoir se comporter comme des nombres. Examinons le type dun nombre. ghci> :t 20 20 :: (Num t) => t

Il semblerait que tous les nombres soient aussi des constantes polymorphiques. Ils peuvent se faire passer pour tout membre de la classe Num . ghci> 20 ghci> 20 ghci> 20.0 ghci> 20.0 20 :: Int 20 :: Integer 20 :: Float 20 :: Double

Ce sont tous des types membres de Num . Si on examine le type de * , on verra quelle accepte des nombres. ghci> :t (*) (*) :: (Num a) => a -> a -> a

Elle prend deux nombres du mme type, et retourne un nombre de ce type. Cest pourquoi (5 :: Int) * (6 :: Integer) donnera une erreur de typage, alors que 5 * (6 :: Integer) fonctionnera et produira un Integer car 5 peut se faire passer pour un Integer ou un Int . Pour tre dans Num , un type doit dj tre ami avec Show et Eq . Integral est aussi une classe de types numriques. Num inclue tous les nombres, autant rels quentiers, Integral ne contient que des entiers. Int et Integer sont membres de cette classe. Floating inclue seulement des nombres virgule flottante, donc Float et Double . Une fonction trs utile pour manipuler des nombres est fromIntegral . Sa dclaration de type est fromIntegral :: (Num b, Integral a) => a -> b . De cette signature, on voit quelle prend un entier et le transforme en un nombre plus gnral. Cest utile lorsque vous voulez utiliser des entiers et des nombres virgule flottante ensemble proprement. Par exemple, length a pour dclaration de type length :: [a] -> Int au lieu davoir le type plus gnral (Num b) => length :: [a] -> b . Ainsi, si lon essaie dobtenir la taille dune liste, et de lajouter 3.2 , on aura une erreur puisquon essaie dajouter un Int un nombre virgule flottante. Pour viter ceci, on fait fromIntegral (length [1,2,3,4]) + 3.2 et tout fonctionne. Remarquez que fromIntegral a plusieurs contraintes de classe dans sa signature. Cest tout fait valide, et comme vous voyez, les contraintes sont spares par des virgules dans les parenthses.

Dmarrons

Table des matires

Syntaxe des fonctions

Syntaxe des fonctions


Types et classes de types Table des matires Rcursivit

Filtrage par motif


Ce chapitre couvrira certaines des constructions syntaxiques cool dHaskell, et lon va commencer par le filtrage par motif. Le filtrage par motif consiste spcifier des motifs auxquels une donne doit se conformer, puis vrifier si cest le cas en dconstruisant la donne selon ces motifs. Lorsque vous dfinissez des fonctions, vous pouvez dfinir des corps de fonction spars pour diffrents motifs. Cela amne un code trs clair et lisible. Vous pouvez filtrer par motif tout type de donne - des nombres, des caractres, des listes, des tuples, etc. Crons une fonction triviale qui vrifie si le nombre fourni est sept ou non. lucky :: (Integral a) => a -> String lucky 7 = "LUCKY NUMBER SEVEN!" lucky x = "Sorry, you're out of luck, pal!"

Lorsque vous appelez lucky , les motifs seront vrifis de haut en bas, et lorsque le paramtre se conforme un motif, le corps de fonction correspondant sera utilis. Ici, le seul moyen pour un nombre de correspondre au premier motif est pour lui dtre le nombre 7. Si ce nest pas le cas, on passe au second motif, qui accepte tout paramtre et lattache au nom x . Cette fonction aurait pu tre implmente laide dune construction if. Mais, si lon voulait que la fonction nonce les nombres entre 1 et 5, et dclare "Not between 1 and 5" pour tout autre nombre ? Sans filtrage par motif, nous devrions utiliser un arbre de constructions if then else plutt alambiqu. Cependant, avec le filtrage : sayMe sayMe sayMe sayMe sayMe sayMe sayMe :: (Integral a) => a -> String 1 = "One!" 2 = "Two!" 3 = "Three!" 4 = "Four!" 5 = "Five!" x = "Not between 1 and 5"

Remarquez que si nous avions mis le dernier motif (celui qui attrape tout) tout en haut, la fonction rpondrait toujours "Not between 1 and 5" , car le motif attraperait tous les nombres, et ils nauraient pas lopportunit dtre tests contre les autres motifs. Rappelez-vous de la fonction factorielle quon avait implmente auparavant. Nous lavions dfini pour un nombre n comme product [1..n] . On peut galement la dfinir de manire rcursive, comme en mathmatiques. On commence par dire que la factorielle de 0 est 1. Puis, on dit que la factorielle dun entier positif est gale au produit de cet entier et de la factorielle de son prdcesseur. En Haskell : factorial :: (Integral a) => a -> a factorial 0 = 1 factorial n = n * factorial (n - 1)

Cest la premire fois quon dfinit une fonction rcursivement. La rcursivit est importante en Haskell, et nous y reviendrons plus en dtail. Rapidement, ce qui se passe si lon demande la factorielle de 3. Il essaie de calculer 3 * factorial 2 . La factorielle de 2 est 2 * factorial 1 , donc on a 3 * (2 * factorial 1) . factorial 1 vaut 1 * factorial 0 , donc on a 3 * (2 * (1 * factorial 0)) . Ici est lastuce, on a dfini factorielle de 0 comme 1, et puisque ce motif vient avant celui qui accepte tout, cela retourne 1. Le rsultat final est donc quivalent 3 * (2 * (1 * 1)) . Si nous avions plac le second motif au dessus du premier, il attraperait tous les nombres, y compris 0, et le calcul ne terminerait jamais. Cest pourquoi lordre est important dans la spcification des motifs, et il est toujours prfrable de prciser les motifs les plus spcifiques en premier, et les plus gnraux ensuite. Le filtrage par motif peut aussi chouer. Si lon dfinit une fonction : charName charName charName charName :: Char -> String 'a' = "Albert" 'b' = "Broseph" 'c' = "Cecil"

et quon essaie de lappeler avec une entre inattendue, voil ce qui arrive :

ghci> charName 'a' "Albert" ghci> charName 'b' "Broseph" ghci> charName 'h' "*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName

Il se plaint que les motifs ne soient pas exhaustifs, et il a raison. Lorsquon utilise des motifs, il faut toujours penser inclure un motif qui attrape le reste de faon ce que le programme ne plante pas sur une entre inattendue. Le filtrage de motifs sutilise aussi sur les tuples. Comment crire une fonction qui prend deux vecteurs en 2D (sous forme de paire) et les somme ? Pour sommer les deux vecteurs, on somme sparment les composantes en x et les composantes en y. Voil ce que nous aurions fait sans filtrage par motif : addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) addVectors a b = (fst a + fst b, snd a + snd b)

Bon, a marche, mais il y a une meilleure faon de faire. Utilisons le filtrage par motif. addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a) addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

Et voil ! Beaucoup mieux. Remarquez que ceci est dj un motif attrape-tout. Le type de addVectors (dans les deux cas) est addVectors :: (Num a) => (a, a) -> (a, a) - > (a, a) , donc nous sommes certains davoir deux paires en paramtres. fst et snd extraient les composantes dune paire. Mais pour des triplets ? Eh bien, il ny a pas de fonction fournie pour faire a. On peut tout de mme lcrire nous mme. first :: (a, b, c) -> a first (x, _, _) = x second :: (a, b, c) -> b second (_, y, _) = y third :: (a, b, c) -> c third (_, _, z) = z

Le _ signifie la mme chose que dans les listes en comprhension. Il signifie quon se fiche compltement de ce que cette partie est, donc on met juste un _ . Ce qui me fait penser, vous pouvez aussi utiliser des motifs dans les listes en comprhension. Voyez par vous-mme : ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)] ghci> [a+b | (a,b) <- xs] [4,7,6,8,11,4]

Si un filtrage choue, a passera simplement au prochain lment. Les listes peuvent elles-mme tre utilises pour le filtrage. Vous pouvez filtrer la liste vide [] , ou un motif incluant : et la liste vide. Puisque [1, 2, 3] est juste du sucre syntaxique pour 1:2:3:[] , vous pouvez utiliser ce premier. Un motif de la forme x:xs attachera la tte x et le reste xs , mme sil ny a quun seul lment, auquel cas xs sera la liste vide.

Note : Le motif x:xs est trs utilis, particulirement dans les fonctions rcursives. Mais les motifs qui ont un : ne peuvent valider que des listes de longueur 1 ou plus.

Si vous dsirez lier, mettons, les trois premiers lments trois variables, et le reste de la liste une autre variable, vous pouvez utiliser un motif comme x:y:z:xs . Il ne validera que des listes qui ont trois lments ou plus. Maintenant que lon sait utiliser le filtrage par motif sur des listes, implmentons notre fonction head . head' :: [a] -> a head' [] = error "Can't call head on an empty list, dummy!" head' (x:_) = x

Vrifions :

ghci> head' [4,5,6] 4 ghci> head' "Hello" 'H'

Cool ! Remarquez, si lon souhaite lier plusieurs variables (mme si lune dentre elles est _ et ne lie pas vraiment), il faut les mettre entre parenthses. Remarquez aussi la fonction error quon a utilise. Elle prend une chane de caractres et gnre une erreur lexcution, en utilisant cette chane pour indiquer quel genre derreur sest produit. Elle fait planter le programme, donc il ne faut pas trop lutiliser. Mais appeler head sur une liste vide na pas vraiment de sens. Crons une fonction triviale qui nous dit quelques lments dune liste en anglais. tell tell tell tell tell :: (Show a) => [a] -> String [] = "The list is empty" (x:[]) = "The list has one element: " ++ show x (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y

Cette fonction est sre car elle prend soin de la liste vide, dune liste singleton, dune liste deux lments, et dune liste plus de deux lments. Remarquez que (x:[]) et (x:y:[]) pourraient tre rcrits respectivement [x] et [x, y] (ceci tant du sucre syntaxique, on na plus besoin des parenthses). On ne peut pas rcrire (x:y:_) de manire analogue car le motif attrape toutes les listes de taille suprieure 2. Nous avons dj implment length laide dune liste en comprhension. Utilisons prsent du filtrage par motif et de la rcursivit : length' :: (Num b) => [a] -> b length' [] = 0 length' (_:xs) = 1 + length' xs

Cest trs similaire la fonction factorielle crite plus tt. Dabord, on dfinit le rsultat dune entre particulire - la liste vide. On parle de cas de base. Puis dans le second motif, on dmonte une liste en sparant sa tte et sa queue. On dit alors que la longueur est gale 1 plus la longueur de la queue. On utilise _ pour filtrer la tte car on se fiche de sa valeur. Remarquez aussi quon a pris en compte tous les cas possibles. Le premier motif accepte une liste vide, le second accepte toute liste non vide. Voyons ce qui se passe lorsquon appelle length' sur "ham" . Dabord, on vrifie si cest une liste vide. Ce nest pas le cas, on descend au second motif. Le second motif est valid, et la longueur calculer est 1 + length' "am" , car on a cass la liste entre tte et queue, et jet la tte. Ok. La length' de "am" est, similairement, 1 + length' "m" . Donc, pour linstant, nous avons 1 + (1 + length' "m") . length' "m" vaut 1 + length' "" (quon peut aussi crire 1 + length' [] ). Et nous avons dfini length' [] comme 0 . Donc, au final on a 1 + (1 + (1 + 0)) . Implmentons sum . On sait que la somme dune liste vide est 0. On lcrit comme un motif. Ensuite, on sait que la somme dune liste est gale la tte plus la somme du reste. Cela donne : sum' :: (Num a) => [a] -> a sum' [] = 0 sum' (x:xs) = x + sum' xs

Il y a aussi quelque chose quon appelle des motifs nomms. Cest une manire pratique de dtruire quelque chose selon un motif tout en gardant une rfrence la chose entire. Vous pouvez utiliser un @ devant le motif cet effet. Par exemple, le motif xs@(x:y:ys) . Ce motif acceptera exactement les mmes choses que x:y:ys , mais vous pourrez facilement dsigner la liste complte via xs au lieu de rcrire x:y:ys dans le corps de la fonction. Exemple simple : capital :: String -> String capital "" = "Empty string, whoops!" capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

ghci> capital "Dracula" "The first letter of Dracula is D"

Normalement, on utilise des motifs nomms pour viter de se rpter quand on filtre un gros motif mais quon a besoin de la chose entire nouveau dans le corps de la fonction. Autre chose - vous ne pouvez pas utiliser ++ dans les motifs. Si vous essayez de filtrer (xs ++ ys) , quest-ce qui irait dans la premire ou dans la seconde liste ? a na pas trop de sens. Cela aurait du sens de filtrer contre (xs ++ [x, y, z]) ou juste (xs ++ [x]) , mais par nature des listes, cela est impossible.

Gardes, gardes !

Alors que les motifs sont un moyen de vrifier quune valeur se conforme une forme et de la dconstruire, les gardes permettent de vrifier si des proprits dune valeur (ou de plusieurs dentre elles) sont vraies ou fausses. a ressemble trangement ce que fait une structure if, et cest en fait trs similaire. Le truc, cest que les gardes sont plus lisibles lors de multiples conditions, et elles fonctionnent trs bien en combinaison avec les motifs. Plutt que dexpliquer leur syntaxe, plongeons plutt dans le bain et crons une fonction avec des gardes. Nous allons faire une fonction qui vous classe en fonction de votre IMC (indice de masse corporelle, BMI en anglais pour body mass index). Si votre IMC est infrieur 18.5, vous tes considr en sous-poids. Sil est entre 18.5 et 25, cest normal. 25 30 correspond un surpoids, et plus de 30 de lobsit. Voici la fonction (pour linstant, elle ne va pas calculer lindice, juste prendre un indice et vous donner votre classification). bmiTell :: (RealFloat a) => bmiTell bmi | bmi <= 18.5 = "You're | bmi <= 25.0 = "You're | bmi <= 30.0 = "You're | otherwise = "You're a -> String underweight, you emo, you!" supposedly normal. Pffft, I bet you're ugly!" fat! Lose some weight, fatty!" a whale, congratulations!"

Les gardes sont indiques par des barres verticales la suite dune fonction et de ses paramtres. Elles sont gnralement indentes un peu droite et alignes. Une garde est simplement une expression boolenne. Si elle est value True , le corps de la fonction correspondant est utilis. Si elle svalue False , la vrification prend la prochaine garde, etc. Si on appelle cette fonction avec 24.3 , elle va dabord vrifier si cest plus petit ou gal 18.5 . Puisque ce nest pas le cas, elle passe la prochaine garde. La vrification est effectue avec la deuxime garde, et puisque 24.3 est infrieur 25.0, la second chane est retourne. Cela rappelle fortement les grands arbres de if else dans les langages impratifs, seulement cest beaucoup mieux et plus lisible. Alors que les grands arbres de if else sont gnralement bouds, parfois un problme est dfini de telle sorte que lon ne peut pas vraiment les viter. Les gardes sont une alternative cela. Trs souvent, la dernire garde est otherwise . otherwise est simplement dfinie comme otherwise = True et attrape donc tout. Cest trs similaire aux motifs, sauf queux vrifient que lentre satisfait un motif alors que les gardes vrifient des conditions boolennes. Si toutes les gardes dune fonction sont values False (donc, quon na pas fourni de garde otherwise qui vaut toujours True ), lvaluation passe au motif suivant. Cest ainsi que les motifs et les gardes sentendent bien. Si aucune combinaison de gardes et de motifs nest satisfaite, une erreur est leve. Bien sr, on peut utiliser les gardes sur des fonctions qui prennent autant de paramtres que lon souhaite. Plutt que de calculer son IMC avant dappeler la fonction, modifions-l pour quelle prenne notre taille et notre masse et le calcule pour nous. bmiTell :: (RealFloat bmiTell weight height | weight / height | weight / height | weight / height | otherwise a) => a -> a -> String ^ 2 <= 18.5 ^ 2 <= 25.0 ^ 2 <= 30.0 = = "You're = "You're = "You're "You're a underweight, you emo, you!" supposedly normal. Pffft, I bet you're ugly!" fat! Lose some weight, fatty!" whale, congratulations!"

Voyons si je suis gros ghci> bmiTell 85 1.90 "You're supposedly normal. Pffft, I bet you're ugly!"

Youpi ! Je ne suis pas gros ! Mais Haskell vient de me traiter de moche. Peu importe !

Notez quil ny a pas de = aprs les paramtres et avant les gardes. Beaucoup de dbutants font des erreurs de syntaxe en le mettant ici.

Un autre exemple simple : implmentons notre propre fonction max . Si vous vous souvenez, elle prend deux choses comparables et retourne la plus grande dentre elles. max' :: (Ord a) => a -> a -> a max' a b | a > b = a | otherwise = b

Vous pouvez aussi crire les gardes sur la mme ligne, bien que je le dconseille car cest moins lisible, mme pour des fonctions courtes. titre dexemple, on aurait pu crire : max' :: (Ord a) => a -> a -> a max' a b | a > b = a | otherwise = b

Erf ! Pas trs lisible tout a ! Passons : implmentons notre propre compare laide de gardes. myCompare :: (Ord a `myCompare` b | a > b = | a == b = | otherwise = a) => a -> a -> Ordering GT EQ LT

ghci> 3 `myCompare` 2 GT

Note : Non seulement pouvons-nous appeler des fonctions de manire infixe avec des apostrophes renverses, on peut galement les dfinir de cette manire. Parfois, cest plus simple lire comme a.

O !?
Dans la section prcdente, nous dfinissions le calculateur dIMC ainsi : bmiTell :: (RealFloat bmiTell weight height | weight / height | weight / height | weight / height | otherwise a) => a -> a -> String ^ 2 <= 18.5 = "You're underweight, you emo, you!" ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!" ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!" = "You're a whale, congratulations!"

Remarquez-vous comme on se rpte trois fois ? On se rpte, trois fois. Se rpter (trois fois) lorsquon programme est aussi souhaitable quun coup de pied dans la tronche. Plutt que de rpter la mme expression trois fois, il serait idal de la calculer une fois, de lier le rsultat un nom, et dutiliser ce nom partout en lieu et place de lexpression. Eh bien, on peut modifier la fonction ainsi : bmiTell :: (RealFloat a) => a -> a -> String bmiTell weight height | bmi <= 18.5 = "You're underweight, you emo, you!" | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!" | bmi <= 30.0 = "You're fat! Lose some weight, fatty!" | otherwise = "You're a whale, congratulations!" where bmi = weight / height ^ 2

Le mot-cl where est plac aprs les gardes (quon indente gnralement autant que les gardes) et est suivi de plusieurs dfinitions de noms ou de fonctions. Ces noms sont visibles travers toutes les gardes, ce qui nous donne lavantage de ne pas avoir nous rpter. Si on dcide de calculer lIMC diffremment, il suffit de changer la formule un endroit. Cela facilite aussi la lisibilit en donnant des noms aux choses, et peut rendre votre programme plus rapide puisque la variable bmi est ici calcule une unique fois. On pourrait faire un peu plus de zle et crire : bmiTell :: (RealFloat a) => a -> a -> String bmiTell weight height | bmi <= skinny = "You're underweight, you emo, you!" | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!" | bmi <= fat = "You're fat! Lose some weight, fatty!" | otherwise = "You're a whale, congratulations!" where bmi = weight / height ^ 2 skinny = 18.5 normal = 25.0 fat = 30.0

Les noms quon dfinit dans la section where dune fonction ne sont visibles que pour cette fonction, donc on na pas se proccuper de polluer lespace de nommage dautres fonctions. Remarquez que tous les noms sont aligns sur une mme colonne. Si ce ntait pas le cas, Haskell serait confus car il ne saurait pas vraiment sils font partie du mme bloc de dfinitions. Les liaisons crs dans des clauses where ne sont pas partages par les corps de diffrentes fonctions. Si vous voulez que plusieurs fonctions aient accs au mme nom, il faut le dfinir globalement. Vous pouvez aussi utiliser les liaisons where pour du filtrage par motif ! On aurait pu rcrire la section where prcdente comme : where bmi = weight / height ^ 2 (skinny, normal, fat) = (18.5, 25.0, 30.0)

Crons une autre fonction triviale qui prend un prnom, un nom, et renvoie les initiales.

initials :: String -> String -> String initials firstname lastname = [f] ++ ". " ++ [l] ++ "." where (f:_) = firstname (l:_) = lastname

Bon, on aurait pu faire le filtrage par motif directement dans les paramtres de la fonction (ctait plus court et plus clair vrai dire), mais cest juste histoire de montrer quon peut aussi le faire dans les liaisons du where. Comme nous avions dfini des constantes dans les blocs where, on peut galement y dfinir des fonctions. Pour continuer sur le thme de la programmation dittique, crons une fonction qui prend une liste de paires masse-taille et retourne une liste dIMC. calcBmis :: (RealFloat a) => [(a, a)] -> [a] calcBmis xs = [bmi w h | (w, h) <- xs] where bmi weight height = weight / height ^ 2

Et cest tout ! La raison pour laquelle nous avons introduit bmi en tant que fonction ici est quil y a plus dun IMC calculer. Il faut examiner toute la liste, et calculer un IMC diffrent pour chaque paire. Les liaisons where peuvent aussi tre imbriques. Cest un idiome habituel de crer une fonction et de dfinir des fonctions auxiliaires dans sa clause where et de dfinir des fonctions auxiliaires ces fonctions auxiliaires dans leur clause where.

Let it be
Les liaisons let sont trs similaires aux liaisons where. Les liaisons where sont une construction syntaxique permettant de lier des variables en fin de fonction, visibles depuis toute la fonction, y compris les gardes. Les liaisons let vous permettent de lier des variables nimporte o, et sont elles-mmes des expressions, mais trs locales, donc elles ne sont pas visibles travers les gardes. Comme toutes les constructions Haskell qui lient des valeurs des noms, elles supportent le filtrage par motif. Voyons plutt en action ! Voici comment lon dfinit une fonction qui nous donne la surface dun cylindre partir de sa hauteur et de son rayon : cylinder :: (RealFloat cylinder r h = let sideArea = 2 * topArea = pi * in sideArea + 2 * a) => a -> a -> a pi * r * h r ^2 topArea

La liaison est de la forme let <bindings> in <expression> . Les noms que vous dfinissez dans let sont accessibles dans lexpression qui suit le in. La diffrence, cest que les liaisons let sont elles-mmes des expressions. L o les liaisons where sont seulement des constructions syntaxiques. Vous vous souvenez que nous avions vu quune construction if tait une expression, et que par consquent vous pouviez lutiliser nimporte o ? ghci> [if 5 > 3 then "Woo" else "Boo", if 'a' > 'b' then "Foo" else "Bar"] ["Woo", "Bar"] ghci> 4 * (if 10 > 5 then 10 else 0) + 2 42

Il en va de mme des liaisons let. ghci> 4 * (let a = 9 in a + 1) + 2 42

On peut aussi introduire des fonctions visibilit locale : ghci> [let square x = x * x in (square 5, square 3, square 2)] [(25,9,4)]

Pour dfinir plusieurs liaisons sur la mme ligne, on ne peut videmment pas les aligner sur la mme colonne. On peut donc utiliser des points-virgules comme sparateurs. ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar) (6000000,"Hey there!")

Le point-virgule situ aprs la dernire liaison nest pas ncessaire, mais peut tre crit votre convenance. Comme on la vu, vous pouvez utiliser du filtrage par motif. Cest trs utile pour dmanteler rapidement un tuple en ses composantes et donner un nom chacune delles : ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100 600

Vous pouvez aussi mettre des liaisons let dans une liste en comprhension. Rcrivons lexemple prcdent du calcul de lIMC sur des listes de paires massetaille, en plaant un let dans une liste en comprhension plutt que dutiliser un where. calcBmis :: (RealFloat a) => [(a, a)] -> [a] calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]

Nous incluons un let dans une liste en comprhension comme un prdicat, seulement quil ne filtre pas la liste, mais lie des noms. Les noms dfinis dans le let dans la comprhension sont visibles de la fonction de retour ( celle qui est avant le | ) et de tous les prdicats et sections qui viennent aprs la liaison. On pourrait faire une fonction qui ne retourne que les IMC des personnes obses : calcBmis :: (RealFloat a) => [(a, a)] -> [a] calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]

On ne peut pas utiliser bmi dans la partie (w, h) <- xs car elle est dfinie avant la liaison let . Nous avons omis la partie in de la liaison let lorsquon lutilisait dans des listes en comprhension, parce que la visibilit des noms est dj prdfinie ici. Cependant, on pourrait placer une liaison let dans un prdicat afin que les noms dfinis ne soient visibles que du prdicat. La partie in peut aussi tre omise lorsquon dfinit des fonctions et des constantes directement dans GHCi. Dans ce cas, les noms sont visibles de toute la session interactive. ghci> let zoot x y z = x * y + z ghci> zoot 3 9 2 29 ghci> let boot x y z = x * y + z in boot 3 4 2 14 ghci> boot <interactive>:1:0: Not in scope: `boot'

Si les liaisons let sont cool, pourquoi ne pas les utiliser tout le temps, au lieu davoir recours des liaisons where, vous vous demandez ? Eh bien, puisque les liaisons let sont des expressions trs locales dans leur visibilit, on ne peut pas les utiliser travers les gardes. Certaines personnes prfrent galement que les liaisons where se situent aprs les fonctions qui sen servent. De cette manire, le corps de la fonction est plus proche de son nom, de sa dclaration de type, et le tout est plus lisible.

Expressions case
Beaucoup de langages impratifs (C, C++, Java, etc.) ont une syntaxe case, et si vous avez dj programm dans ceux-ci, vous savez probablement de quoi il sagit. Il sagit donc de prendre une variable, et dexcuter un code spcifique en fonction de sa valeur, et ventuellement davoir un bloc attrape-tout si la variable a une valeur pour laquelle aucun cas nest crit. Haskell prend ce concept, et lamliore. Comme le nom lindique, les expressions case sont, eh bien, des expressions, comme les expressions if else et les liaisons let. Non seulement on peut valuer ces expressions selon plusieurs cas possibles de la valeur dune variable, mais on peut aussi faire du filtrage par motif. Hmmm, prendre une variable, la filtrer par motif, valuer des morceaux de code en fonction de sa valeur, est-ce quon naurait pas dj fait a auparavant ? Bien sr, le filtrage par motif des paramtres dune dfinition de fonction ! Eh bien, il sagit en fait seulement dun sucre syntaxique pour les expressions case. Ainsi, ces deux bouts de code sont parfaitement interchangeables : head' :: [a] -> a head' [] = error "No head for empty lists!" head' (x:_) = x

head' :: [a] -> a head' xs = case xs of [] -> error "No head for empty lists!" (x:_) -> x

Comme vous pouvez le voir, la syntaxe des expressions case est plutt simple : case expression of pattern -> result pattern -> result pattern -> result

expression est teste contre les motifs. Laction de filtrage est comme attendue : le premier motif qui correspond est utilis. Si lon parcourt toute lexpression case sans trouver de motif valid, une erreur dexcution a lieu. Alors que le filtrage par motif sur les paramtres dune fonction ne peut se faire que dans la dfinition, les expressions case peuvent tre utilises peu prs partout. Par exemple : describeList :: [a] -> String describeList xs = "The list is " ++ case xs of [] -> "empty." [x] -> "a singleton list." xs -> "a longer list."

Elles sont utiles pour faire du filtrage par motif sur des choses en plein milieu dune expression. Du fait que le filtrage par motif dans les dfinitions de fonction est seulement du sucre syntaxique pour des expressions case, on aurait aussi pu dfinir : describeList :: [a] -> String describeList xs = "The list is " ++ what xs where what [] = "empty." what [x] = "a singleton list." what xs = "a longer list."

Types et classes de types

Table des matires

Rcursivit

Rcursivit
Syntaxe des fonctions Table des matires Fonctions dordre suprieur

Bonjour rcursivit !
Nous avons rapidement mentionn la rcursivit dans le chapitre prcdent. Dans celui-ci, regardons-l de plus prs, pourquoi elle est importante en Haskell et comment on peut crire des solutions concises et lgantes certains problmes en rflchissant rcursivement. Si vous ne savez toujours pas ce quest la rcursivit, relisez cette phrase. Haha ! Je blague ! La rcursivit est en fait une manire de dfinir des fonctions dans laquelle on utilise la fonction dans sa propre dfinition. Les dfinitions mathmatiques sont souvent nonces rcursivement. Par exemple, la suite de Fibonacci est dfinie rcursivement. Dabord, on dfinit les deux premiers nombres de Fibonacci de manire non-rcursive. On dit que F(0) = 0 et F(1) = 1, pour dire que le 0me et le 1er chiffre sont 0 et 1, respectivement. Puis, on dit que pour tout entier naturel, ce nombre de Fibonacci est la somme des deux prcdents nombres de Fibonacci. Donc F(n) = F(n-1) + F(n-2). Ainsi, F(3) est F(2) + F(1), qui est (F(1) + F(0)) + F(1). Puisque lon vient datteindre des nombres tous dfinis non-rcursivement, on peut dclarer que F(3) vaut 2. Un ou plusieurs lments dfinis non-rcursivement dans une dfinition rcursive sont appels les cas de base de rcursivit, et sont importants afin dassurer que la fonction rcursive termine. Sans eux, si F(0) et F(1) navaient pas t dfinis ainsi, on nobtiendrait jamais de solution car on atteindrait 0, et on continuerait dcrotre. Au bout dun moment, on dirait que F(2000) est F(-2001) + F(-2002), et on en verrait toujours pas le bout ! La rcursivit est importante en Haskell car, contrairement aux langages impratifs, vous faites des calculs en Haskell en dfinissant ce que les choses sont et non pas comment on les obtient. Cest pour cela quil ny a pas de boucles while et for en Haskell, et la place, on utilise souvent la rcursivit pour dclarer ce quest quelque chose.

Maximum de fun
La fonction maximum prend une liste de choses qui peuvent tre ordonnes (i.e. des instances de la classe Ord ) et retourne la plus grande dentre elles. Comment implmenteriez-vous ceci de manire imprative ? Vous auriez srement une variable qui contiendrait la plus grande valeur rencontre jusquici, et vous boucleriez travers les lments de la liste, remplaant le maximum courant par un lment sil savre tre plus grand. La valeur maximale serait alors le maximum courant une fois arriv la fin. Wouah ! Que de mots pour dcrire un algorithme aussi simple ! Voyons comment nous le dfinirions rcursivement. Nous aurions dabord un cas de base, nous disant que le maximum dune liste singleton est son seul lment. Puis, le maximum dune liste plus longue est sa tte si elle est plus grande que le maximum de la queue. Sinon, cest ce dernier qui est le maximum de la liste ! Et voil ! Implmentons a en Haskell. maximum' :: (Ord a) => [a] -> a maximum' [] = error "maximum of empty list" maximum' [x] = x maximum' (x:xs) | x > maxTail = x | otherwise = maxTail where maxTail = maximum' xs

Comme vous le voyez, le filtrage par motif va bien de pair avec la rcursivit ! La plupart des langages impratifs ne proposent pas de filtrage par motif, donc vous tes oblig dutiliser plein de if else pour tester les cas de base. Ici, on les met simplement dans des motifs. La premire condition dit : si la liste est vide, plante ! Raisonnable, aprs tout quel est le maximum dune liste vide ? Je ne sais pas. Le deuxime motif est aussi un cas de base. Il dit que pour une liste singleton, il suffit de retourner son unique lment. Maintenant, le troisime motif est l o se passe laction. On utilise du filtrage par motif pour sparer la tte et la queue de la liste. Cest un idiome usuel pour faire de la rcursivit sur des listes, donc habituez-vous. On utilise une liaison where pour dfinir maxTail comme le maximum du reste de la liste. Ensuite, on vrifie si la tte est plus grande que le maximum du reste de la liste. Si cest le cas, la tte est retourne. Sinon, le maximum du reste de la liste est retourn. Prenons par exemple une liste de nombres et voyons comment cela marche : [2, 5, 1] . Si on appelle maximum' dessus, les deux premiers motifs ne vont pas correspondre. Le troisime marchera, et coupera le 2 du [5, 1] . La clause where veut connatre le maximum de [5, 1] , donc on suit cette route. Elle la compare avec succs au troisime motif nouveau, et [5, 1] est coup en 5 et [1] . nouveau, la clause where veut connatre le maximum de [1] . Cest un cas de base, on retourne donc 1 . Enfin ! En remontant dune tape, 5 est compar au maximum de [1] (qui est 1 ), et retourne 5 . On sait prsent que le maximum de [5, 1] est 5 . Une tape plus haut, nous comparions 2 au maximum de [5, 1] , qui est 5 , et on choisit donc 5 .

Une manire encore plus claire dcrire cette fonction est dutiliser max . Si vous vous souvenez, max est une fonction qui prend deux nombres et retourne le plus grand dentre eux. Ici, nous pourrions rcrire maximum' en utilisant max : maximum' maximum' maximum' maximum' :: (Ord a) => [a] -> a [] = error "maximum of empty list" [x] = x (x:xs) = max x (maximum' xs)

Comme cest lgant ! Dans le principe, le maximum dune liste est le max de son premier lment et du maximum de la queue.

Un peu plus de fonctions rcursives


Maintenant que lon sait penser en termes de rcursivit, essayons dimplmenter quelques fonctions rcursives. Dabord, nous allons implmenter replicate . replicate prend un Int et un lment, et retourne une liste qui a plusieurs fois cet lment. Par exemple, replicate 3 5 retourne [5, 5, 5] . Rflchissons au cas de base. mon avis, le cas de base correspond des valeurs infrieures ou gales 0. Si lon essaie de rpliquer quelque chose zro fois, on devrai renvoyer une liste vide. Et pareil pour des nombres ngatifs, sinon a ne veut pas dire grand chose. replicate' :: (Num i, Ord i) => i -> a -> [a] replicate' n x | n <= 0 = [] | otherwise = x:replicate' (n-1) x

Nous avons utilis des gardes ici plutt que des motifs car on teste une condition boolenne. Si n est plus petit que 0, retourner la liste vide. Sinon, retourner une liste qui a x comme premier lment, et ensuite x rpliqu n-1 fois pour la queue. force, la partie en (n-1) va amener notre fonction atteindre son cas de base.

Note : Num nest pas une sous-classe d Ord . Cela signifie quun nombre na pas forcment besoin dtre ordonnable. Cest pourquoi on doit spcifier la fois Num et Ord en contraintes de classe pour pouvoir faire la fois des additions, des soustractions et des comparaisons.

Maintenant, implmentons take . Elle prend un certain nombre dlments dune liste. Par exemple, take 3 [5, 4, 3, 2, 1] retourne [5, 4, 3] . Si on essaie de prendre 0 ou moins lments dune liste, on rcupre une liste vide. galement, si lont essaie de prendre quoi que ce soit dune liste vide, on rcupre une liste vide. Remarquez que ce sont nos deux cas de base. crivons tout cela : take' take' | take' take' :: (Num i, n _ n <= 0 = _ [] = n (x:xs) = Ord i) => i -> [a] -> [a] [] [] x : take' (n-1) xs

Le premier motif spcifie quessayer de prendre 0 ou moins lments retourne une liste vide. Remarquez quon utilise _ pour filtrer la liste parce que nous ne nous intressons pas vraiment sa valeur dans ce cas. Remarquez aussi quon utilise des gardes, mais pas otherwise . Cela veut dire que si n savre tre suprieur 0, le filtrage passe au motif suivant. Le deuxime motif indique que si lon essaie de prendre quoi que ce soit dans une liste vide, on rcupre une liste vide. Le troisime motif coupe la liste en tte et queue. Puis, on dit que prendre n lments dune liste quivaut une liste avec x en tte et une liste qui prend n-1 lments dans le reste pour queue. Essayez de prendre une feuille de papier pour crire les tapes de lvaluation de take 3 [4, 3, 2, 1] . reverse renverse une liste. Pensez au cas de base. Quel est-il ? Allons cest la liste vide ! Une liste

vide renverse est la liste vide elle-mme. Ok. Et le reste ? Eh bien, on peut dire que si lon coupe une liste en tte et queue, la liste renverse est gale la queue renverse, avec la tte la fin. reverse' :: [a] -> [a] reverse' [] = [] reverse' (x:xs) = reverse' xs ++ [x]

Et voil ! PuisquHaskell supporte des listes infinies, notre rcursivit na pas vraiment besoin de cas de base. Si elle nen a pas, elle va soit continuer sagiter sur un calcul linfini, ou produire une structure de donne infinie, comme une liste infinie. La chose bien propos des listes infinies, cest quon peut les couper o on veut. repeat prend un lment et retourne une liste infinie qui a cet lment uniquement. Une implmentation rcursive est trs simple, regardez. repeat' :: a -> [a] repeat' x = x:repeat' x

Appeler repeat 3 nous donnera une liste qui commence avec un 3 , et a une infinit de 3 en queue. Appeler repeat 3 svaluera 3:repeat 3 , puis 3:(3:repeat 3) , puis 3:(3:(3:repeat 3)) , etc. repeat 3 ne terminera jamais son valuation, alors que take 5 (repeat 3) nous donnera une liste de cinq 3. Cest comme si on avait fait replicate 5 3 . zip prend deux listes, et les zippe ensemble. zip [1, 2, 3] [2, 3] retourne [(1, 2), (2, 3)] , parce quelle tronque la plus grande liste pour avoir la mme taille que lautre. Et si lon zippe quelque chose la liste vide ? Eh bien, on obtient la liste vide. Voil notre cas de base. Cependant, zip prend deux listes en paramtres, donc il y a en fait deux cas de base. zip' zip' zip' zip' :: [a] _ [] = [] _ = (x:xs) -> [b] -> [(a,b)] [] [] (y:ys) = (x,y):zip' xs ys

Dabord, les deux premiers motifs disent que si une des deux listes est vide, on obtient une liste vide. Le troisime dit que deux listes zippes sont gales la paire de leur tte, suivie de leurs queues zippes. Zipper [1, 2, 3] et ['a', 'b'] va finalement essayer de zipper [3] et [] . Le cas de base arrive et le rsultat est donc (1, 'a'):(2, 'b'):[] , qui est exactement la mme chose que [(1, 'a'), (2, 'b')] . Implmentons encore une fonction de la bibliothque standard - elem . Elle prend un lment et une liste et vrifie si cet lment appartient la liste. La condition de base, comme souvent, est la liste vide. On sait quune liste vide ne contient aucun lment, donc elle ne contient certainement pas les drodes que vous recherchez. elem' elem' elem' | | :: (Eq a) => a -> [a] -> Bool a [] = False a (x:xs) a == x = True otherwise = a `elem'` xs

Plutt simple et attendu. Si la tte nest pas llment recherch, on cherche dans la queue. Si on arrive une liste vide, le rsultat est False .

Vite, triez !
Soit une liste dlments qui peuvent tre tris. Leur type est une instance de la classe Ord . prsent, on souhaite les trier ! Il existe un algorithme trs cool pour faire a, appel quicksort. Cest une faon trs astucieuse de trier des lments. Alors quelle prend facilement 10 lignes implmenter dans des langages impratifs, limplmentation Haskell est bien plus courte et lgante. Quicksort est devenu une sorte dicne dHaskell. Ainsi, implmentons le, bien quimplmenter quicksort en Haskell est assez ringard vu que tout le monde le fait dj pour montrer comme Haskell est lgant. Donc, la signature de type sera quicksort :: (Ord a) => [a] -> [a] . Pas de surprise ici. Le cas de base ? La liste vide, bien sr. Une liste vide trie est une liste vide. Maintenant, voil lalgorithme : une liste trie est une liste pour laquelle on a pris tous les lments plus petits que la tte, quon les a tris, et placs lavant, puis on a mis la tte, et la suite on a plac tous les lments plus grands que la tte, aprs les avoir aussi tris". Remarquez comme on a dit deux fois trier dans la dfinition, donc il y aura probablement deux appels rcursifs ! Remarquez aussi quon a utilis le verbe tre pour dfinir lalgorithme, et pas une suite de faire ceci, puis faire cela, ensuite faire ceci Cest la beaut de la programmation fonctionnelle ! Comment allons nous filtrer dune liste les lments plus petits que sa tte, et ceux plus grands ? Avec des listes en comprhension. Bon, plongeons. quicksort :: (Ord a) => [a] -> [a] quicksort [] = [] quicksort (x:xs) =

let smallerSorted = quicksort [a | a <- xs, a <= x] biggerSorted = quicksort [a | a <- xs, a > x] in smallerSorted ++ [x] ++ biggerSorted

Lanons-l pour voir si elle a lair de marcher. ghci> quicksort [10,2,5,3,1,6,7,4,2,3,4,8,9] [1,2,2,3,3,4,4,5,6,7,8,9,10] ghci> quicksort "the quick brown fox jumps over the lazy dog" " abcdeeefghhijklmnoooopqrrsttuuvwxyz"

Booyah ! Voil de quoi je parle ! Si on a, disons [5, 1, 9, 4, 6, 7, 3] et quon veut la trier, cet algorithme va dabord prendre sa tte, ici 5 , et la mettre au milieu de deux listes respectivement plus petite et plus grande que la tte. un moment, on a donc [1, 4, 3] ++ [5] ++ [9, 6, 7] . On sait quune fois la liste compltement trie, le 5 naura pas chang de place, car les 3 nombres sa gauche sont plus petits que lui, et les 3 sa droite sont plus grands. Maintenant, si lon trie rcursivement [1, 4, 3] et [9, 6, 7] , la liste entire est trie ! On les trie laide de la mme fonction. Finalement, on va tellement rduire les listes quil ne restera que des listes vides, qui sont tries par dfinition, puisquelles sont vides. Voil une illustration :

Un lment en place et qui ne bougera plus est reprsent en orange . Si vous les lisez de gauche droite, vous verrez la liste trie. Bien quon ait choisi de comparer tous les lments aux ttes des listes, on aurait tout fait pu prendre nimporte quel lment et le comparer tous les autres. Dans quicksort, llment choisi pour tre compar aux autres est appel le pivot. Ici, les pivots sont reprsents en vert. Nous avons choisi la tte car il est facile de lobtenir par filtrage par motif. Les lments plus petits que le pivot sont en vert clair et ceux plus grands que le pivot en vert fonc . Le gradient jaune-orang reprsente lapplication de quicksort.

Penser rcursif
Nous avons fait pas mal de rcursivit et, comme vous lavez probablement remarqu, il y a un motif sous-jacent. Gnralement, vous dfinissez des cas de base, et ensuite une fonction qui fait quelque chose avec certains lments et rappelle cette fonction sur le reste. Peu importe que ce soit une liste, un arbre ou une autre structure de donnes. Une somme est le premier lment, plus la somme du reste de la liste. Un produit est le premier lment, multipli par le produit du reste de la liste. La longueur est 1, plus la longueur du reste de la liste. Et ctera, et ctera Bien sr, il y a aussi les cas de base. Gnralement, le cas de base est un scnario pour lequel un appel rcursif na plus de sens. Pour des listes, le plus souvent cest la liste vide. Si vous traitez des arbres, ce sera gnralement une feuille, i.e. un nud qui na pas denfants. Cest la mme chose pour traiter des nombres rcursivement. Gnralement, on a un nombre et une fonction applique une modification de ce nombre. La factorielle vue plus tt tait le produit dun nombre et de la factorielle de ce nombre moins un. Une telle application rcursive na pas de sens pour zro, parce que les factorielles sont seulement dfinies pour les entiers positifs. Souvent, le cas de base savre tre un lment neutre. Llment neutre de la multiplication est 1 car si lon multiplie un nombre par 1, on rcupre le mme nombre. Lorsquon somme des listes, on dfinit la somme de la liste vide comme 0 et 0 est llment neutre pour laddition. Dans quicksort, le cas de base est la liste vide et llment neutre est la liste vide galement, car si vous rajoutez une liste vide une autre liste, vous rcuprez cette dernire. Donc, lorsque vous essayez de penser une solution rcursive pour un problme rsoudre, essayez de vous demander quand est-ce quune solution rcursive ne conviendra pas et dutiliser cela comme cas de base. Pensez aux lments neutres et demandez-vous si vous aurez dconstruire les paramtres de la fonction (par exemple, les listes sont gnralement spares en tte et queue par un filtrage par motif) et sur quelle partie vous effectuerez un appel rcursif.

Syntaxe des fonctions

Table des matires

Fonctions dordre suprieur

Fonctions dordre suprieur


Rcursivit Table des matires Modules Les fonctions Haskell peuvent prendre dautres fonctions en paramtres, et retourner des fonctions en valeur de retour. Une fonction capable dune de ces deux choses est dite dordre suprieur. Les fonctions dordre suprieur ne sont pas quune partie de lexprience Haskell, elles sont lexprience Haskell. Elles savrent indispensable pour dfinir ce que les choses sont plutt que des tapes qui changent un tat et bouclent. Elles sont un moyen trs puissant de rsoudre des problmes et de rflexion sur les programmes.

Fonctions curryfies
En Haskell, toutes les fonctions ne prennent en fait quun unique paramtre. Comment avons-nous pu dfinir des fonctions qui en prenaient plus dun alors ? Eh bien, grce un dtail astucieux ! Toutes nos fonctions qui acceptaient plusieurs paramtres jusqu prsent taient des fonctions curryfies. Quest-ce que cela signifie ? Vous comprendrez mieux avec un exemple. Prenons cette bonne vieille fonction max . Elle a lair de prendre deux paramtres, et de retourner le plus grand des deux. Faire max 4 5 cre une fonction qui prend un paramtre, et retourne soit 4 , soit ce paramtre. On applique alors cette fonction la valeur 5 pour produire le rsultat attendu. On ne dirait pas comme a, mais cest en fait assez cool. Les deux appels suivants sont ainsi quivalents : ghci> max 4 5 5 ghci> (max 4) 5 5

Mettre un espace entre deux choses consiste appliquer une fonction . Lespace est en quelque sorte un oprateur, et il a la plus grande prcdence. Examinons le type de max . Cest max :: (Ord a) => a -> a -> a . On peut aussi crire ceci : max :: (Ord a) => a -> (a -> a) . Et le lire ainsi : max prend un a et retourne (cest le premier -> )) une fonction qui prend un a et retourne un a . Cest pourquoi le type de retour et les paramtres dune fonction sont spars par des flches. En quoi cela nous aide-t-il ? Pour faire simple, si on appelle une fonction avec trop peu de paramtres, on obtient une fonction applique partiellement, cest dire une fonction qui prend autant de paramtres quon en a oublis. Utiliser lapplication partielle (appeler une fonction avec trop peu de paramtres) est un moyen gracieux de crer des fonctions la vole et de les passer dautres fonctions pour quelle les complte avec dautres donnes. Regardons cette fonction violemment simple : multThree :: (Num a) => a -> a -> a -> a multThree x y z = x * y * z

Que se passe-t-il vraiment quand on fait multThree 3 5 9 ou ((multThree 3) 5) 9 ? Dabord, 3 est appliqu multThree , car ils sont spars par un espace. Cela cre une fonction qui prend un paramtre et retourne une fonction. Ensuite, 5 est appliqu cette nouvelle fonction, ce qui cre une fonction qui prend un paramtre et le multiplie par 15. 9 est appliqu cette dernire et le rsultat est un truc comme 135. Rappelez-vous que le type de cette fonction peut tre crit comme multThree :: (Num a) => a -> (a -> (a -> a)) . La chose avant -> est lunique paramtre de la fonction, et la chose aprs est son unique type retourn. Donc, notre fonction prend un a et retourne une fonction qui a pour type (Num a) => a -> (a -> a) . De faon similaire, cette fonction prend un a et retourne une fonction qui a pour type (Num a) => a -> a . Et cette fonction, prend un a et retourne un a . Regardez a : ghci> ghci> 54 ghci> ghci> 180 let multTwoWithNine = multThree 9 multTwoWithNine 2 3 let multWithEighteen = multTwoWithNine 2 multWithEighteen 10

En appelant des fonctions avec trop peu de paramtres, en quelque sorte, on cre de nouvelles fonctions la vole. Comment crer une fonction qui prend un nombre, et le compare 100 ? Comme a : compareWithHundred :: (Num a, Ord a) => a -> Ordering compareWithHundred x = compare 100 x

compareWithHundred x = compare 100 x

Si on lappelle avec 99 , elle retourne GT . Simple. Remarquez-vous le x tout droite des deux cts de lquation ? Rflchissons prsent ce que compare 100 retourne. Cela retourne une fonction qui prend un nombre, et le compare 100. Ouah ! Est-ce que ce ntait pas la fonction quon voulait ? On devrait rcrire cela : reWithHundred :: (Num a, Ord a) => a -> Ordering compareWithHundred = compare 100

La dclaration de type est inchange, car compare 100 retourne bien une fonction. compare a pour type (Ord a) => a -> (a -> Ordering) et lappeler avec la valeur 100 retourne un (Num a, Ord a) => a -> Ordering) . Une classe de contrainte additionnelle sest insinue ici parce que 100 est un lment de la classe Num .

Yo ! Soyez certain de bien comprendre les fonctions curryfies et lapplication partielle, cest trs important !

Les fonctions infixes peuvent aussi tre appliques partiellement laide de sections. Sectionner une fonction infixe revient lentourer de parenthses et lui fournir un paramtre sur lun de ses cts. Cela cre une fonction qui attend lautre paramtre et lapplique du ct o il lui manquait une oprande. Une fonction insultante de trivialit : divideByTen :: (Floating a) => a -> a divideByTen = (/10)

Appeler, disons, divideByTen 200 est quivalent faire 200 / 10 , ce qui est aussi quivalent (/10) 200 . Maintenant, une fonction qui vrifie si un caractre est en majuscule : isUpperAlphanum :: Char -> Bool isUpperAlphanum = (`elem` ['A'..'Z'])

La seule chose spciale propos des sections, cest avec - . Par dfinition des sections, (-4) devrait tre la fonction qui attend un nombre, et lui soustrait 4. Cependant, par habitude, (-4) signifie la valeur moins quatre. Pour crer une fonction qui soustrait 4 du nombre en paramtre, appliquez plutt partiellement la fonction subtract ainsi : (subtract 4) . Que se passe-t-il si on tape multThree 3 4 directement dans GHCi sans la lier avec un let ou la passer une autre fonction ? ghci> multThree 3 4 <interactive>:1:0: No instance for (Show (t -> t)) arising from a use of `print' at <interactive>:1:0-12 Possible fix: add an instance declaration for (Show (t -> t)) In the expression: print it In a 'do' expression: print it

GHCi nous dit que lexpression produite est une fonction de type a -> a mais quil ne sait pas afficher cela lcran. Les fonctions ne sont pas des instances de la classe Show , donc elles nont pas de reprsentation littrale. Quand on entre 1 + 1 dans GHCi, il calcule dabord la valeur 2 puis appelle show sur 2 pour obtenir une reprsentation textuelle de ce nombre. La reprsentation de 2 est "2" , et cest ce quil nous affiche.

lordre du jour : de lordre suprieur


Les fonctions peuvent prendre des fonctions en paramtres et retourner des fonctions. Pour illustrer cela, nous allons crer une fonction qui prend une fonction en paramtre, et lapplique deux fois quelque chose ! applyTwice :: (a -> a) -> a -> a applyTwice f x = f (f x)

Dabord, remarquez la dclaration de type. Avant, on navait pas besoin de parenthses, parce que -> est naturellement associatif droite. Ici, au contraire, elles sont obligatoires. Elles indiquent que le premier paramtre est une fonction qui prend quelque chose et retourne une chose du mme type. Le second paramtre est quelque chose galement de ce type, et la valeur retourne est elle aussi de ce type. On pourrait lire cette dclaration de type de manire curryfie, mais pour ne pas se prendre trop la tte, disons juste quelle prend deux paramtres, et retourne une chose. Le premier paramtre est une fonction (de type a -> a ) et le second est du mme type a . La fonction peut trs bien tre de type Int -> Int ou String -> String ou quoi que ce soit. Mais alors, le

second paramtre doit tre du type correspondant.

Note : partir de maintenant, nous dirons quune fonction prend plusieurs paramtres malgr le fait quelle ne prend en ralit quun paramtre et retourne une fonction applique partiellement jusqu finalement arriver une valeur solide. Donc, pour simplifier, on dira que a -> a -> a prend deux paramtres, bien que lon sache ce qui se trame en ralit sous la couverture.

Le corps de la fonction est plutt simple. On utilise le paramtre f comme une fonction, on applique cette fonction x en les sparant par un espace, puis on applique f au rsultat une nouvelle fois. Jouons un peu avec la fonction : ghci> applyTwice 16 ghci> applyTwice "HEY HAHA HAHA" ghci> applyTwice "HAHA HAHA HEY" ghci> applyTwice 144 ghci> applyTwice [3,3,1] (+3) 10 (++ " HAHA") "HEY" ("HAHA " ++) "HEY" (multThree 2 2) 9 (3:) [1]

Lapplication partielle de fonction est videmment gniale et trs utile. Si notre fonction a besoin dune fonction qui prend un paramtre, on peut lui passer une autre fonction, quon aura partiellement applique jusqu ce quil ne lui manque plus quun paramtre. Maintenant, nous allons profiter de la programmation dordre suprieur pour implmenter une fonction trs utile de la bibliothque standard. Elle sappelle zipWith . Elle prend une fonction, et deux listes, et zippe les deux listes laide de la fonction, en lappliquant aux lments correspondants. Voici une implmentation : zipWith' zipWith' zipWith' zipWith' :: (a -> _ [] _ = _ _ [] = f (x:xs) b -> c) -> [a] -> [b] -> [c] [] [] (y:ys) = f x y : zipWith' f xs ys

Regardez la dclaration de type. Le premier paramtre est une fonction qui attend deux choses pour en produire une troisime. Elles nont pas avoir le mme type, mais elles le peuvent tout de mme. Les deuxime et troisime paramtres sont des listes. Le rsultat est aussi une liste. La premire liste est une liste de a , parce que la fonction de jointure attend un a en premier argument. La deuxime doit tre une liste de b parce que la fonction de jointure attend un b ensuite. Le rsultat est une liste de c . Si la dclaration de type dune fonction dit quelle accepte un a -> b -> c , alors elle peut accepter une fonction de type a -> a -> a , mais pas linverse ! Rappelez-vous que quand vous crivez des fonctions, notamment dordre suprieur, et que vous ntes pas certain du type, vous pouvez juste omettre la dclaration de type et vrifier ce quHaskell a infr avec :t . Laction dans la fonction est assez proche du zip normal. Les conditions de base sont les mmes, seulement il y a un argument de plus, la fonction de jointure, mais puisquon ne va pas sen servir ici, on met juste un _ . Le corps de la fonction pour le dernier motif est galement similaire zip , seulement la place de faire (x, y) , elle fait f x y . Une mme fonction dordre suprieur peut tre utilise pour une multitude de diffrentes tches si elle est assez gnrale. Voici une petite dmonstration de toutes les diffrentes choses que zipWith' peut faire : ghci> zipWith' (+) [4,2,5,6] [2,6,2,3] [6,8,7,9] ghci> zipWith' max [6,3,2,1] [7,3,1,5] [7,3,2,5] ghci> zipWith' (++) ["foo ", "bar ", "baz "] ["fighters", "hoppers", "aldrin"] ["foo fighters","bar hoppers","baz aldrin"] ghci> zipWith' (*) (replicate 5 2) [1..] [2,4,6,8,10] ghci> zipWith' (zipWith' (*)) [[1,2,3],[3,5,6],[2,3,4]] [[3,2,2],[3,4,5],[5,4,3]] [[3,4,6],[9,20,30],[10,12,12]]

Comme vous le voyez, une mme fonction dordre suprieur peut tre utilise trs flexiblement plusieurs usages. La programmation imprative utilise gnralement des trucs comme des boucles for, des boucles while, des affectations de variables, des vrifications de ltat courant, etc. pour arriver un comportement, puis lencapsule dans une interface, comme une fonction. La programmation fonctionnelle utilise des fonctions dordre suprieur pour abstraire des motifs rcurrents, comme examiner deux listes paire paire et faire quelque chose avec ces paires, ou rcuprer un ensemble de solutions et liminer celles qui ne vous intressent pas. Nous allons encore implmenter une fonction de la bibliothque standard, flip . Elle prend une fonction et retourne une fonction qui est comme la fonction initiale, mais dont les deux premiers arguments ont chang leur place. On peut limplmenter ainsi :

flip' :: (a -> b -> c) -> (b -> a -> c) flip' f = g where g x y = f y x

En lisant la dclaration de type, on voit quelle prend une fonction qui prend un a puis un b et retourne une fonction qui prend un b puis un a . Mais puisque les fonctions sont curryfies par dfaut, la deuxime paire de parenthses ne sert en fait rien, vu que -> est associatif droite par dfaut. (a -> b -> c) -> (b -> a -> c) est la mme chose que (a -> b -> c) -> (b -> (a -> c)) , qui est aussi la mme chose que (a -> b -> c) -> b -> a -> c . On crit que g x y = f y x . Si cest le cas, alors f y x = g x y nest-ce pas ? Avec cela en tte, on peut dfinir la fonction encore plus simplement. flip' :: (a -> b -> c) -> b -> a -> c flip' f y x = f x y

Ici, on profite du fait que les fonctions soient curryfies. Quand on appelle flip' f sans les paramtres y et x , elle retourne une f qui prend ces deux paramtres et appelle loriginale avec les arguments dans lordre inverse. Bien que les fonctions retournes ainsi sont gnralement passes dautres fonctions, on peut se servir de la curryfication lorsquon crit des fonctions dordre suprieur en rflchissant ce que leur rsultat serait si elles taient appliques entirement. ghci> flip' zip [1,2,3,4,5] "hello" [('h',1),('e',2),('l',3),('l',4),('o',5)] ghci> zipWith (flip' div) [2,2..] [10,8,6,4,2] [5,4,3,2,1]

Maps et filtres
map prend une fonction et une liste, et applique la fonction tous les lments de la liste, rsultant en une nouvelle liste. Regardons sa signature et sa dfinition. map :: (a -> b) -> [a] -> [b] map _ [] = [] map f (x:xs) = f x : map f xs

La signature dit quelle prend une fonction de a vers b , une liste de a , et retourne une liste de b . Il est intressant de voir que rien quen regardant la signature dune fonction, vous pouvez parfois dire exactement ce quelle fait. map est une de ces fonctions dordre suprieur extrmement utile qui peut tre utilise de milliers de faons diffrentes. La voici en action : ghci> map (+3) [1,5,3,1,6] [4,8,6,4,9] ghci> map (++ "!") ["BIFF", "BANG", "POW"] ["BIFF!","BANG!","POW!"] ghci> map (replicate 3) [3..6] [[3,3,3],[4,4,4],[5,5,5],[6,6,6]] ghci> map (map (^2)) [[1,2],[3,4,5,6],[7,8]] [[1,4],[9,16,25,36],[49,64]] ghci> map fst [(1,2),(3,5),(6,3),(2,6),(2,5)] [1,3,6,2,2]

Vous avez probablement remarqu que chacun de ces exemples aurait pu tre ralis laide de listes en comprhension. map (+3) [1, 5, 3, 1, 6] est quivalent [x+3 | x <- [1, 5, 3, 1, 6]] . Cependant, utiliser map est bien plus lisible dans certains cas o vous ne faites quappliquer une fonction chaque lment de la liste, surtout quand vous mappez un map auquel cas les crochets sentassent de manire dplaisante. filter est une fonction qui prend un prdicat (un prdicat est une fonction qui dit si quelque chose est vrai ou faux, une fonction qui retourne une valeur boolenne) et une liste, et retourne la liste des lments qui satisfont le prdicat. La signature et limplmentation : filter :: (a -> Bool) -> [a] -> [a] filter _ [] = [] filter p (x:xs) | p x = x : filter p xs | otherwise = filter p xs

Plutt simple. Si p x vaut True , llment est inclus dans la nouvelle liste. Sinon, il reste dehors. Quelques exemples dutilisation : ghci> filter (>3) [1,5,3,2,1,6,4,3,2,1] [5,6,4] ghci> filter (==3) [1,2,3,4,5] [3] ghci> filter even [1..10]

[2,4,6,8,10] ghci> let notNull x = not (null x) in filter notNull [[1,2,3],[],[3,4,5],[2,2],[],[],[]] [[1,2,3],[3,4,5],[2,2]] ghci> filter (`elem` ['a'..'z']) "u LaUgH aT mE BeCaUsE I aM diFfeRent" "uagameasadifeent" ghci> filter (`elem` ['A'..'Z']) "i lauGh At You BecAuse u r aLL the Same" "GAYBALLS"

Tout ceci pourrait aussi tre fait base de listes en comprhension avec des prdicats. Il ny a pas de rgle privilgiant lutilisation de map et filter celle des listes en comprhension, vous devez juste dcider de ce qui est plus lisible en fonction du code et du contexte. Lquivalent pour filter de lapplication de plusieurs prdicats dans une liste en comprhension est soit en filtrant plusieurs fois daffile, soit en fusionnant des prdicats laide de && . Vous souvenez-vous de la fonction quicksort du chapitre prcdent ? Nous avions utilis des listes en comprhension pour filtrer les lments plus petits que le pivot. On peut faire de mme avec filter : quicksort :: (Ord a) => [a] -> [a] quicksort [] = [] quicksort (x:xs) = let smallerSorted = quicksort (filter (<=x) xs) biggerSorted = quicksort (filter (>x) xs) in smallerSorted ++ [x] ++ biggerSorted

Mapper et filtrer sont le marteau et le clou de la bote outils de tout programmeur fonctionnel. Bon, il importe peut que vous utilisiez plutt map et filter que des listes en comprhension. Souvenez-vous comment nous avions rsolu le problme de trouver les triangles rectangles avec un certain primtre. En programmation imprative, nous aurions imbriqu trois boucles, dans lequelles nous aurions test si la combinaison courante correspondait un triangle rectangle et avait le bon primtre, auquel cas nous en aurions fait quelque chose comme lafficher lcran. En programmation fonctionnelle, ceci est effectu par mappage et filtrage. Vous crez une fonction qui prend une valeur et produit un rsultat. Vous mappez cette fonction sur une liste de valeurs, puis vous filtrez la liste rsultante pour obtenir les rsultats qui satisfont votre recherche. Et grce la paresse dHaskell, mme si vous mappez et filtrez sur une liste plusieurs fois, la liste ne sera traverse quune seule fois. Trouvons le plus grand entier infrieur 100 000 qui soit divisible par 3 829. Pour cela, filtrons un ensemble de solutions pour lesquelles on sait que le rsultat sy trouve. largestDivisible :: (Integral a) => a largestDivisible = head (filter p [100000,99999..]) where p x = x `mod` 3829 == 0

On cre dabord une liste de tous les entiers infrieurs 100 000 en dcroissant. Puis, on la filtre par un prdicat, et puisque les nombres sont en ordre dcroissant, le plus grand dentre eux sera simplement le premier lment de la liste filtre. On na mme pas eu besoin dune liste finie pour dmarrer. Encore un signe de la paresse en action. Puisque lon nutilise que la tte de la liste filtre, peu importe quelle soit finie ou infinie. Lvaluation sarrte ds que la premire solution est trouve. Maintenant, trouvons la somme de tous les carrs impairs infrieurs 10 000. Mais dabord, puisquon va lutiliser dans la solution, introduisons la fonction takeWhile . Elle prend un prdicat et une liste, et parcourt cette liste depuis le dbut en retournant tous les lments tant que le prdicat reste vrai. Ds quun lment ne satisfait plus le prdicat, elle sarrte. Si lon voulait le premier mot dune chane comme "elephants know how to party" , on pourrait faire takeWhile (/=' ') "elephants know how to party" et cela retournerait "elephants" . Ok. La somme des carrs impairs infrieurs dix mille. Dabord, on va commencer par mapper (^2) sur la liste infinie [1..] . Ensuite, on va filtrer pour garder les nombres impairs. Puis, on prendra des lments de cette liste tant quils restent infrieurs 10 000. Enfin, on va sommer cette liste. On na mme pas besoin dune fonction pour a, une ligne dans GHCi suffit : ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..]))) 166650

Gnial ! On commence avec une donne initiale (la liste infinie de tous les entiers naturels) et on mappe par dessus, puis on filtre et on coupe jusqu avoir ce que lon dsire, et on somme le tout. On aurait pu crire ceci laide de listes en comprhension : ghci> sum (takeWhile (<10000) [n^2 | n <- [1..], odd (n^2)]) 166650

Cest une question de got, vous de choisir la forme que vous prfrez. Encore une fois, la paresse dHaskell rend ceci possible. On peut mapper et filtrer une liste infinie, puisquil ne va pas mapper et filtrer directement, il retardera ces actions. Cest seulement quand on demande Haskell de nous montrer la somme que la fonction sum dit la fonction takeWhile quelle a besoin de cette liste de nombre. takeWhile force le filtrage et le mappage, en demandant des nombres un par un tant quil nen croise pas un plus grand que 10000.

Pour notre prochain problme, on va se confronter aux suites de Collatz. On prend un entier naturel. Si cet entier est pair, on le divise par deux. Sil est impair, on le multiplie par 3 puis on ajoute 1. On prend ce nombre, et on recommence, ce qui produit un nouveau nombre, et ainsi de suite. On obtient donc une chane de nombres. On pense que, quel que soit le nombre de dpart, la chane termine, avec pour valeur 1. Si on prend 13, on obtient la suite : 13, 40, 20, 10, 5, 16, 8, 4, 2, 1. 13 fois 3 plus 1 vaut 40. 40 divis par 2 vaut 20, etc. On voit que la chane contient 10 termes. Maintenant on cherche savoir : pour tout nombre de dpart compris entre 1 et 100, combien de chanes ont une longueur suprieure 15 ? Dabord, on va crire une fonction qui produit ces chanes : chain chain chain | | :: (Integral a) => a -> [a] 1 = [1] n even n = n:chain (n `div` 2) odd n = n:chain (n*3 + 1)

Puisque les chanes terminent 1, cest le cas de base. Cest une fonction rcursive assez simple. ghci> chain 10 [10,5,16,8,4,2,1] ghci> chain 1 [1] ghci> chain 30 [30,15,46,23,70,35,106,53,160,80,40,20,10,5,16,8,4,2,1]

Yay ! a a lair de marcher correctement. prsent, la fonction qui nous donne notre rponse : numLongChains :: Int numLongChains = length (filter isLong (map chain [1..100])) where isLong xs = length xs > 15

On mappe la fonction chain sur la liste [1..100] pour obtenir une liste de toutes les chanes, qui sont elles-mme reprsentes sous forme de listes. Puis, on les filtre avec un prdicat qui vrifie simplement si la longueur est plus grande que 15. Une fois ceci fait, il ne reste plus qu compter le nombre de chanes dans la liste finale.

Note : Cette fonction a pour type numLongChains :: Int parce que length a pour type Int au lieu de Num a , pour des raisons historiques. Si nous voulions retourner Num a , on aurait pu utiliser fromIntegral sur le nombre retourn.

En utilisant map , on peut faire des trucs comme map (*) [0..] , par exemple pour illustrer la curryfication et le fait que les fonctions appliques partiellement sont de vraies valeurs que lon peut manipuler et placer dans des listes (par contre, on ne peut pas les transformer en chanes de caractres). Pour linstant, on a seulement mapp des fonctions qui attendent un paramtre sur des listes, comme map (*2) [0..] pour obtenir des listes de type (Num a) => [a] , mais on peut aussi faire map (*) [0..] sans problme. Ce qui se passe ici, cest que le nombre dans la liste sapplique la fonction * , qui a pour type (Num a) => a -> a -> a . Appliquer une fonction sur un paramtre, alors quelle en attend deux, retourne une fonction qui attend encore un paramtre. Si lon mappe * sur [0..] , on obtient en retour une liste de fonctions qui attendent chacune un paramtre, donc (Num a) [a -> a] . map (*) [0..] produit une liste comme celle quon obtiendrait en crivant [(0*), (1*), (2*), (3*), (4*), (5*), ] . ghci> let listOfFuns = map (*) [0..] ghci> (listOfFuns !! 4) 5 20

Demander llment dindex 4 de notre liste retourne une fonction quivalente (4*) . Puis on applique cette fonction 5 . Donc cest comme crire (4*) 5 ou juste 4 * 5 .

Lambdas
Les lambda expressions sont simplement des fonctions anonymes utilises lorsquon a besoin dune fonction usage unique. Normalement, on cre une lambda uniquement pour la passer une fonction dordre suprieur. Pour crer une lambda, on crit un \ (parce que a ressemble la lettre grecque lambda si vous le fixez assez longtemps des yeux) puis les paramtres, spars par des espaces. Aprs cela vient -> puis le corps de la fonction. On lentoure gnralement de parenthses, autrement elle stend autant que possible vers la droite. Si vous regardez 10cm plus haut, vous verrez quon a utilis une liaison where dans numLongChains pour crer la fonction isLong , avec pour seul but de passer cette fonction filter . Au lieu de faire a, on pourrait utiliser une lambda :

numLongChains :: Int numLongChains = length (filter (\xs -> length xs > 15) (map chain [1..100]))

Les lambdas sont des expressions, cest pourquoi on peut les manipuler comme a. Lexpression (\xs -> length xs > 15) retourne une fonction qui nous dit si la longueur de la liste passe en paramtre est plus grande que 15. Les personnes ntant pas habitues la curryfication et lapplication partielle de fonctions utilisent souvent des lambdas l o ils nen ont en fait pas besoin. Par exemple, les expressions map (+3) [1, 6, 3, 2] et map (\x -> x + 3) [1, 6, 3, 2] sont quivalentes puisque (+3) tout comme (\x -> x + 3) renvoient toutes les deux le nombre pass en paramtre plus 3. Il est inutile de vous prciser que crer une lambda dans ce cas est stupide puisque lapplication partielle est bien plus lisible. Comme les autres fonctions, les lambdas peuvent prendre plusieurs paramtres. ghci> zipWith (\a b -> (a * 30 + 3) / b) [5,4,3,2,1] [1,2,3,4,5] [153.0,61.5,31.0,15.75,6.6]

Et comme les fonctions, vous pouvez filtrer par motif dans les lambdas. La seule diffrence, cest que vous ne pouvez pas dfinir plusieurs motifs pour un paramtre, comme par exemple crer un motif [] et un (x:xs) pour le mme paramtre, et avoir les valeurs parcourir les diffrents motifs. Si un filtrage par motif choue dans une lambda, une erreur dexcution a lieu, donc faites attention en filtrant dans les lambdas. ghci> map (\(a,b) -> a + b) [(1,2),(3,5),(6,3),(2,6),(2,5)] [3,8,9,8,7]

Les lambdas sont habituellement entoures de parenthses moins quon veuille quelles stendent le plus possible droite. Voil quelque chose dintressant : vu la faon dont sont curryfies les fonctions par dfaut, ces deux codes sont quivalents : addThree :: (Num a) => a -> a -> a -> a addThree x y z = x + y + z

addThree :: (Num a) => a -> a -> a -> a addThree = \x -> \y -> \z -> x + y + z

Si on dfinit une fonction ainsi, il devient vident que la signature doit avoir cette forme. Il y a trois -> dans le type et dans lquation. Mais bien sr, la premire faon dcrire est bien plus lisible, la seconde ne sert qu illustrer la curryfication. Cependant, cette notation savre parfois pratique. Je trouve la fonction flip plus lisible dfinie ainsi : flip' :: (a -> b -> c) -> b -> a -> c flip' f = \x y -> f y x

Bien que ce soit comme crire flip' f x y = f y x , on fait apparatre clairement que ce sera utilis pour crer une nouvelle fonction la plupart du temps. Lutilisation la plus classique de flip consiste appeler la fonction avec juste une autre fonction, et de passer le rsultat un map ou un filter . Utilisez donc les lambdas lorsque vous voulez rendre explicite le fait que cette fonction est gnralement applique partiellement avant dtre passe une autre comme paramtre.

Plie mais ne rompt pas


lpoque o nous faisions de la rcursivit, nous avions repr un thme rcurrent dans nos fonctions rcursives qui opraient sur des listes. Nous introduisions un motif x:xs et nous faisions quelque chose avec la tte et quelque chose avec la queue. Il savre que cest un motif trs commun, donc il existe quelques fonctions trs utiles qui lencapsulent. Ces fonctions sont appeles des folds (NDT : des plis). Elles sont un peu comme la fonction map , seulement quelles rduisent une liste une valeur unique. Un fold prend une fonction binaire, une valeur de dpart (que jaime appeler laccumulateur) et une liste plier. La fonction binaire prend deux paramtres. Elle est appele avec laccumulateur en premire ou deuxime position, et le premier ou le dernier lment de la liste comme autre paramtre, et produit un nouvel accumulateur. La fonction est appele nouveau avec le nouvel accumulateur et la nouvelle extrmit de la queue, et ainsi de suite. Une fois quon a travers toute la liste, seul laccumulateur reste, cest la valeur laquelle on a rduit la liste. Dabord, regardons la fonction foldl , aussi appele left fold (pli gauche). Elle plie la liste en partant de la gauche. La fonction binaire est appele avec la

valeur de dpart de laccumulateur et la tte de liste. Cela produit un nouvel accumulateur, et la fonction est nouveau appele sur cette valeur et le prochain lment de la liste, etc. Implmentons sum nouveau, mais cette fois, utilisons un fold plutt quune rcursivit explicite. sum' :: (Num a) => [a] -> a sum' xs = foldl (\acc x -> acc + x) 0 xs

Un, deux, trois, test : ghci> sum' [3,5,2,1] 11

Regardons de prs comment ce pli se droule. \acc x -> acc + x est la fonction binaire. 0 est la valeur de dpart et xs la liste plier. Dabord, 0 est utilis pour acc et 3 pour x . 0 + 3 retourne 3 et devient, pour ainsi dire, le nouvel accumulateur. Ensuite, 3 est utilis comme accumulateur, et llment courant, 5 , rsultant en un 8 comme nouvel accumulateur. Encore en avant, 8 est laccumulateur, 2 la valeur courante, le nouvel accumulateur est donc 10 . Utilis avec la valeur courante 1 , il produit 11 . Bravo, vous venez dachever votre premier pli ! Le diagramme professionnel sur la gauche illustre la faon dont le pli se droule, tape par tape (jour aprs jour !). Le nombre vert kaki est laccumulateur. Vous pouvez voir comme la liste est consomme par la gauche par laccumulateur. Om nom nom nom ! (NDT : Miam miam miam !) Si on prend en compte le fait que les fonctions sont curryfies, on peut crire cette implmentation encore plus rapidement : sum' :: (Num a) => [a] -> a sum' = foldl (+) 0

La lambda (\acc x -> acc + x) est quivalente (+) . On peut omettre le xs la fin parce que foldl (+) 0 retourne une fonction qui attend une liste. En gnral, lorsque vous avec une fonction foo a = bar b a , vous pouvez rcrire foo = bar b , grce la curryfication. Bon, implmentons une autre fonction avec un pli gauche avant de passer aux plis droite. Je suis sr que vous savez tous qu elem vrifie si une valeur fait partie dune liste, donc je ne vais pas vous le rappeler (oups, je viens de le faire !). Implmentons-l avec un pli gauche. elem' :: (Eq a) => a -> [a] -> Bool elem' y ys = foldl (\acc x -> if x == y then True else acc) False ys

Bien, bien, bien, quavons-nous l ? La valeur de dpart et laccumulateur sont de type boolen. Le type de laccumulateur et de linitialisateur est toujours le mme quand on plie. Rappelez-vous en quand vous ne savez plus quoi utiliser comme valeur de dpart, a vous mettra sur la piste. Ici, on commence avec False . Il est logique dutiliser False comme valeur initiale. On prsume que llment nest pas l, tant quon na pas de preuve de sa prsence. Notez que si lon appelle fold sur une liste vide, on obtient en retour la valeur initiale. Ensuite, on vrifie le premier lment pour savoir si cest celui que lon cherche. Si cest le cas, on passe laccumulateur True . Sinon, on ne touche pas laccumulateur. Sil tait False , il reste False car on ne vient pas de trouver llment. Sil tait True , on le laisse aussi. Le pli droite, foldr travaille de manire analogue au pli gauche, mais laccumulateur consomme les valeurs en partant de la droite. Aussi, la fonction binaire du pli gauche prend laccumulateur en premier paramtre, et la valeur courante en second ( \acc x -> ), celle du pli droit prend la valeur courante en premier et laccumulateur en second ( \x acc -> ). Cest assez logique que le pli droite ait laccumulateur droite, vu quil plie depuis la droite. La valeur de laccumulateur (et donc le rsultat) dun pli peut tre de nimporte quel type. Un nombre, un boolen, ou mme une liste. Implmentons la fonction map laide dun pli droite. Laccumulateur sera la liste, on va accumuler les valeurs aprs mappage, lment par lment. De fait, il est vident que llment de dpart sera une liste vide. map' :: (a -> b) -> [a] -> [b] map' f xs = foldr (\x acc -> f x : acc) [] xs

Si lon mappe (+3) [1, 2, 3] , on attaque la liste par la droite. On prend le dernier lment, 3 et on applique la fonction, rsultant en un 6 . On le place lavant de laccumulateur, qui tait [] . 6:[] est [6] , et notre nouvel accumulateur. On applique (+3) sur 2 , donnant 5 et on le place devant ( : ) laccumulateur, laccumulateur devient [5, 6] . On applique (+3) 1 et on le place devant laccumulateur, ce qui donne pour valeur finale [4, 5, 6] . Bien sr, nous aurions pu implmenter cette fonction avec un pli gauche aussi. Cela serait map' f xs = foldl (\acc x -> acc ++ [f x]) [] xs , mais le problme, cest que ++ est beaucoup plus coteux que : , donc gnralement, on utilise des plis droite lorsquon construit des nouvelles listes partir dune liste.

Si vous renversez une liste, vous pouvez faire un pli droit sur une liste comme vous auriez fait un pli gauche sur la liste originale, et vice versa. Parfois, vous navez mme pas besoin de a. La fonction sum peut tre implmente aussi bien avec un pli gauche qu droite. Une des grosses diffrences est que les plis droite fonctionnent sur des listes infinies, alors que les plis gauche, non ! Pour mettre cela au clair, si vous prenez un endroit dune liste infinie et que vous vous mettez plier depuis la droite depuis celui-ci, vous finirez par atteindre le dbut de la liste. Par contre, si vous vous placez un endroit dune liste infinie, et que vous commencez plier depuis la gauche vers la droite, vous natteindrez jamais la fin ! Les plis peuvent tre utiliss pour implmenter nimporte quelle fonction qui traverse une liste une fois, lment par lment, et retourne quoi que ce soit bas l-dessus. Si jamais vous voulez parcourir une liste pour retourner quelque chose, vous aurez besoin dun pli. Cest pourquoi les plis sont, avec les maps et les filtres, un des types les plus utiles en programmation fonctionnelle. Les fonctions foldl1 et foldr1 fonctionnent quasiment comme foldl et foldr , mais nont pas besoin dune valeur de dpart. Elles considrent que le premier (ou le dernier respectivement) lment de la liste est la valeur de dpart, et commencent plier partir de llment suivant. Avec cela en tte, la fonction sum peut tre implmente : sum = foldl1 (+) . Puisquelles dpendent du fait que les listes quelles essaient de plier aient au moins un lment, elles peuvent provoquer des erreurs lexcution si on les appelle sur des listes vides. foldl et foldr , dun autre ct, fonctionnent bien sur des listes vides. Quand vous faites un pli, pensez donc la faon dont il se comporte sur une liste vide. Si la fonction na aucun sens pour une liste vide, vous pouvez probablement utiliser foldl1 et foldr1 pour limplmenter. Histoire de vous montrer la puissance des plis, on va implmenter un paquet de fonctions de la bibliothque standard avec eux : maximum' :: (Ord a) => [a] -> a maximum' = foldr1 (\x acc -> if x > acc then x else acc) reverse' :: [a] -> [a] reverse' = foldl (\acc x -> x : acc) [] product' :: (Num a) => [a] -> a product' = foldr1 (*) filter' :: (a -> Bool) -> [a] -> [a] filter' p = foldr (\x acc -> if p x then x : acc else acc) [] head' :: [a] -> a head' = foldr1 (\x _ -> x) last' :: [a] -> a last' = foldl1 (\_ x -> x)

head est tout de mme mieux implment par filtrage par motif, mais ctait juste pour lexemple, que lon peut y arriver avec des plis. Notre fonction reverse' est ce titre plutt astucieuse. On prend pour valeur initiale une liste vide, et on attaque la liste par la gauche en positionnant les lments rencontrs lavant de notre accumulateur. Au final, on a construit la liste renverse. \acc x -> x : acc ressemble assez la fonction : , mais avec ses paramtres dans lautre sens. Cest pourquoi, on aurait pu crire reverse comme foldl (flip (:)) [] . Une autre faon de se reprsenter les plis droite et gauche consiste se dire : mettons quon a un pli droite et une fonction binaire f , et une valeur initiale z . Si lon plie droite la liste [3, 4, 5, 6] , on fait en gros cela : f 3 (f 4 (f 5 (f 6 z))) . f est appel avec le dernier lment de la liste et laccumulateur, cette valeur est donne comme accumulateur la prochaine valeur, etc. Si on prend pour f la fonction + et pour accumulateur de dpart 0 , a donne 3 + (4 + (5 + (6 + 0))) . Ou, avec un + prfixe, (+) 3 ((+) 4 ((+) 5 ((+) 6 0))) . De faon similaire, un pli gauche avec la fonction binaire g et laccumulateur z est quivalent p g (g (g (g z 3) 4) 5) 6 . Si on utilise flip (:) comme fonction binaire, et [] comme accumulateur (de manire renverser la liste), cest quivalent flip (:) (flip (:) (flip (:) (flip (:) [] 3) 4) 5) 6 . Et pour sr, valuer cette expression renvoie [6, 5, 4, 3] . scanl et scanr sont comme foldl et foldr , mais rapportent tous les rsultats intermdiaires de laccumulateur sous forme dune liste. Il existe aussi scanl1 et scanr1 , analogues foldl1 et foldr1 . ghci> scanl (+) 0 [3,5,2,1] [0,3,8,10,11] ghci> scanr (+) 0 [3,5,2,1] [11,8,3,1,0] ghci> scanl1 (\acc x -> if x > acc then x else acc) [3,4,5,3,7,9,2,1] [3,4,5,5,7,9,9,9] ghci> scanl (flip (:)) [] [3,2,1] [[],[3],[2,3],[1,2,3]]

En utilisant un scanl , le rsultat final sera dans le dernier lment de la liste retourne, alors que pour un scanr , il sera en tte.

Les scans sont utiliss pour surveiller la progression dune fonction implmentable comme un pli. Rpondons cette question : Combien dentiers naturels croissants faut-il pour que la somme de leurs racines carres dpasse 1000 ? Pour rcuprer les racines carres de tous les entiers naturels, on fait map sqrt [1..] . Maintenant, pour obtenir leur somme, on pourrait faire un pli, mais puisquon sintresse la progression de la somme, on va plutt faire un scan. Une fois le scan fait, on compte juste le nombre de sommes qui sont infrieures 1000. La premire somme sera normalement gale 1. La deuxime, 1 plus racine de 2. La troisime cela plus racine de 3. Si X de ces sommes sont infrieures 1000, alors il faut X+1 lments pour dpasser 1000. sqrtSums :: Int sqrtSums = length (takeWhile (<1000) (scanl1 (+) (map sqrt [1..]))) + 1

ghci> sqrtSums 131 ghci> sum (map sqrt [1..131]) 1005.0942035344083 ghci> sum (map sqrt [1..130]) 993.6486803921487

On utilise takeWhile plutt que filter parce que filter ne peut pas travailler sur des listes infinies. Bien que nous sachions que cette liste est croissante, filter ne le sait pas, donc on utilise takeWhile pour arrter de scanner ds quune somme est plus grande que 1000.

Appliquer des fonctions avec $


Bien, maintenant, dcouvrons la fonction $ , aussi appele application de fonction. Voyons sa dfinition : ($) :: (a -> b) -> a -> b f $ x = f x

De quoi ? Quest-ce que cest que cet oprateur inutile ? Cest juste une application de fonction ! Eh bien, presque, mais pas compltement ! Alors que lapplication de fonction habituelle (avec un espace entre deux choses) a une prcdence trs leve, la fonction $ a la plus faible prcdence. Une application de fonction avec un espace est associative gauche ( f a b c est quivalent ((f a) b) c ), alors quavec $ elle est associative droite. Cest tout, mais en quoi cela nous aide-t-il ? La plupart du temps, cest une fonction pratique pour viter dcrire des tas de parenthses. Considrez lexpression sum (map sqrt [1..130]) . Puisque $ a une prcdence aussi faible, on peut rcrire cette expression sum $ map sqrt [1..130] , et viter de prcieuses frappes de clavier ! Quand on rencontre un $ , la fonction sur sa gauche sapplique lexpression sur sa droite. Quen est-il de sqrt 3 + 4 + 9 ? Ceci ajoute 9, 4, et la racine de 3. Mais si lon veut la racine carre de 3 + 4 + 9, il faut crire sqrt (3 + 4 + 9) , ou avec $ , sqrt $ 3 + 4 + 9 , car $ a la plus faible prcdence de tous les oprateurs. On peut donc voir $ comme le fait dcrire une parenthse ouvrante en lieu et place, et daller mettre une parenthse fermante le plus possible droite de lexpression. Et sum (filter (> 10) (map (*2) [2..10])) ? Et bien, puisque $ est associatif droite, f (g (z x)) est gal f $ g $ z x . On peut donc rcrire lexpression sum $ filter (> 10) $ map (*2) [2..10] . part pour se dbarasser des parenthses, $ implique aussi que lon peut traiter lapplication de fonction comme nimporte quelle fonction. Ainsi, on peut par exemple mapper lapplication de fonction sur une liste de fonctions. ghci> map ($ 3) [(4+), (10*), (^2), sqrt] [7.0,30.0,9.0,1.7320508075688772]

Composition de fonctions
En mathmatique, la composition de fonctions est dfinie ainsi : , ce qui signifie que deux fonctions produisent une nouvelle fonction

qui, lorsquelle est appele avec un paramtre, disons x, est quivalent lappel de g sur x, puis lappel de f sur le rsultat. En Haskell, la composition de fonction est plutt identique. On utilise la fonction . , dfinie ainsi : (.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (g x)

Prtez attention la dclaration de type. f doit prendre pour paramtre une valeur qui a le mme type que la valeur retourne par g . Ainsi, la fonction rsultante peut prendre un paramtre du mme type que celui attendu par g , et retourner une valeur du mme type que celui retourn par f . Lexpression negate . (* 3) retourne une fonction qui prend un nombre, le multiplie par 3, et retourne loppos.

Une utilisation de la composition de fonctions est de crer des fonctions la vole pour les passer dautre fonctions. Bien sr, on peut utiliser des lambdas cet effet, mais souvent, la composition de fonctions est plus claire et concise. Disons quon a une liste de nombres, et quon veut tous les rendre ngatifs. Un moyen de procder serait de prendre la valeur absolue de chacun dentre eux, et de renvoyer son oppos : ghci> map (\x -> negate (abs x)) [5,-3,-6,7,-3,2,-19,24] [-5,-3,-6,-7,-3,-2,-19,-24]

Remarquez la lambda-expression, comme elle ressemble une composition de fonctions. Avec la composition, on peut crire : ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24] [-5,-3,-6,-7,-3,-2,-19,-24]

Fabuleux ! La composition est associative droite, donc on peut composer plusieurs fonctions la fois. Lexpression f (g (z x)) est quivalente (f . g . z) x . Avec a en tte, on peut transformer : ghci> map (\xs -> negate (sum (tail xs))) [[1..5],[3..6],[1..7]] [-14,-15,-27]

en : ghci> map (negate . sum . tail) [[1..5],[3..6],[1..7]] [-14,-15,-27]

Mais quen est-il des fonctions plusieurs paramtres ? Pour les utiliser dans de la composition de fonctions, il faut les avoir partiellement appliques jusqu ce quelles ne prennent plus quun paramtre. sum (replicate 5 (max 6.7 8.9)) peut tre rcrit (sum . replicate 5 . max 6.7) 8.9 ou bien sum . replicate 5 . max 6.7 $ 8.9 . Ce qui se passe ici : une fonction qui prend la mme chose que max 6.7 et applique replicate 5 celle-ci est cre. Ensuite, une fonction qui prend a et applique sum est cre. Finalement, cette fonction est appele avec 8.9 . Vous liriez normalement comme cela : prend 8.9 et applique max 6.7 , puis applique replicate 5 a, et applique sum a. Si vous voulez rcrire une expression pleine de parenthses avec de la composition de fonctions, vous pouvez commencer par mettre le dernier paramtre de la dernire fonction la suite dun $ , et ensuite composer les appels successifs, en les crivant sans leur dernier paramtre et en les sparant par des points. Si vous avez replicate 100 (product (map (*3) (zipWith max [1, 2, 3, 4, 5] [4, 5, 6, 7, 8]))) , vous pouvez lcrire replicate 100 . product . map (*3) . zipWith max [1, 2, 3, 4, 5] $ [4, 5, 6, 7, 8] . Si lexpression se terminait par trois parenthses, la rcriture contiendra probablement trois oprateurs de composition. Une autre utilisation courante de la composition de fonctions consiste dfinir des fonctions avec un style dit sans point (certains disent mme sans but !). Prenez par exemple la fonction dfinie auparavant : sum' :: (Num a) => [a] -> a sum' xs = foldl (+) 0 xs

Le xs est expos gauche comme droite. cause de la curryfication, on peut lomettre des deux cts, puisquappeler foldl (+) 0 renverra une fonction qui attend une liste. crire une fonction comme sum' = foldl (+) 0 est dit dans le style sans point. Comment crire ceci en style sans point ? fn x = ceiling (negate (tan (cos (max 50 x))))

On ne peut pas se dbarasser du x des deux cts. Le x du ct du corps de la fonction a des parenthses aprs lui. cos (max 50) ne voudrait rien dire. On ne peut pas prendre le cosinus dune fonction. On peut cependant exprimer fn comme une composition de fonctions : fn = ceiling . negate . tan . cos . max 50

Excellent ! Souvent, un style sans point est plus lisible et concis, car il vous fait rflchir en termes de fonctions et de composition de leur rsultat, plutt qu la faon dont les donnes sont transportes dun endroit lautre. Vous pouvez prendre de simples fonctions et utiliser la composition pour les coller et former des fonctions plus complexes. Cependant, souvent, crire une fonction en style sans point peut tre moins lisible parce que la fonction est trop complexe. Cest pourquoi lon dcourage les trop grandes chanes de composition, bien que je sois coupable dtre fan de composition. Le style prfr consiste utiliser des liaisons let pour donner des noms aux rsultats intermdiaires ou dcouper le problme en sous-problmes et ensuite mettre tout en place pour que les fonctions aient du sens pour le lecteur, plutt que de voir une grosse chane de compositions. Dans la section sur les maps et les filtres, nous avions rsolu le problme de trouver la somme des carrs impairs infrieurs 10000. Voici la solution sous la forme dune fonction.

oddSquareSum :: Integer oddSquareSum = sum (takeWhile (<10000) (filter odd (map (^2) [1..])))

tant un fan de composition, jaurais probablement crit : oddSquareSum :: Integer oddSquareSum = sum . takeWhile (<10000) . filter odd . map (^2) $ [1..]

Cependant, sil tait possible que quelquun doive lire ce code, jaurais plutt crit : oddSquareSum :: Integer oddSquareSum = let oddSquares = filter odd $ map (^2) [1..] belowLimit = takeWhile (<10000) oddSquares in sum belowLimit

Ce code ne gagnerait pas une comptition de code golf, mais si quelquun devrait le lire, il trouverait certainement cela plus lisible quune chane de compositions.

Rcursivit

Table des matires

Modules

Modules
Fonctions dordre suprieur Table des matires Crer nos propres types et classes de types

Charger des modules


Un module Haskell est une collection de fonctions, types et classes de types en rapport les uns avec les autres. Un programme Haskell est une collection de modules o le module principal charge les autres modules et utilise les fonctions quils dfinissent. Sparer son code dans plusieurs modules a plusieurs avantages. Si un module est assez gnrique, les fonctions quil exporte peuvent tre utilises dans une multitude de programmes. Si votre propre code est spar dans des modules assez indpendants (on dit quils ont un couplage faible), vous pourrez les rutiliser plus tard. Cela rend la programmation plus facile grer en sparant des entits avec des buts prcis. La bibliothque standard Haskell est dcoupe en modules, chacun deux contenant des fonctions et des types qui sont relis et servent un but partag. Il y a un module de manipulation de listes, un module de programmation concurrente, un module pour les nombres complexes, etc. Toutes les fonctions, types et classes de types que nous avons utiliss jusqu prsent faisaient partie du module Prelude , qui est import par dfaut. Dans ce chapitre, on va explorer quelques modules utiles et les fonctions quils contiennent. Mais dabord, voyons comment importer un module. La syntaxe dimport de modules dans un script Haskell est import <module name> . Cela doit tre fait avant de dfinir des fonctions, donc gnralement, les imports sont faits en dbut de fichier. Un script peut bien sr importer plusieurs modules. crivez simplement une ligne dimport par module. Importons le module Data.List , qui a un paquet de fonctions utiles pour travailler sur des listes et utilisons une des fonctions que le module exporte afin de savoir combien dlments uniques une liste comporte. import Data.List numUniques :: (Eq a) => [a] -> Int numUniques = length . nub

Quand vous crivez import Data.List , toutes les fonctions que Data.List exporte deviennent disponibles dans votre espace de nommage global, donc vous pouvez les appeler de nimporte o dans le script. nub est une fonction dfinie dans Data.List qui prend une liste et supprime les lments en double. Composer length et nub en faisant length . nub produit une fonction quivalente \xs -> length (nub xs) . Vous pouvez aussi importer des fonctions dun module dans lespace de nommage global de GHCi. Si vous tes dans GHCi et que vous dsirez appeler des fonctions exportes par Data.List , faites : ghci> :m + Data.List

Pour charger plusieurs modules, pas besoin de taper :m + plusieurs fois, vous pouvez en charger plusieurs la fois. ghci> :m + Data.List Data.Map Data.Set

Cependant, en chargeant un script qui importe des modules, vous navez mme pas besoin dutiliser :m + pour accder ces modules. Si vous navez besoin que de quelques fonctions dun module, vous pouvez importer slectivement juste ces fonctions. Si nous ne voulions que nub et sort de Data.List , on ferait : import Data.List (nub, sort)

Vous pouvez aussi choisir dimporter toutes les fonctions dun module, sauf certaines. Cest souvent utile quand plusieurs modules exportent des fonctions qui ont le mme nom et que vous voulez vous dbarrasser de celles qui ne vous concernent pas. Mettons quon ait dfini une fonction nub et quon veuille importer toutes les fonctions de Data.List part nub : import Data.List hiding (nub)

Un autre moyen de grer les collisions de noms est dutiliser les imports qualifis. Le module Data.Map , qui offre une structure de donne pour trouver des valeurs grce une cl, exporte un paquet de fonctions qui ont les mmes noms que celles du Prelude , comme filter ou null . Donc, quand on importe

Data.Map et quon appelle filter , Haskell ne sait pas de laquelle on parle. Voici comment on rsout cela : import qualified Data.Map

Cela nous force crire Data.Map.filter pour dsigner la fonction filter de Data.Map , alors que filter tout court correspond la fonction du Prelude quon connat et quon aime tous. Mais crire Data.Map devant chaque fonction du module est un peu fastidieux. Cest pourquoi on peut renommer les imports qualifis : import qualified Data.Map as M

Maintenant, pour parler de la fonction filter de Data.Map , on peut utiliser M.filter . Utilisez cette rfrence trs pratique pour savoir quels modules font partie de la bibliothque standard. Un bon moyen dapprendre des choses sur Haskell est de se balader dans cette rfrence et dexplorer des modules et leurs fonctions. Pour chaque module, le code source Haskell est disponible. Lire le code source de certains modules est un trs bon moyen dapprendre Haskell et de se faire une ide solide de ce dont il sagit. Pour trouver des fonctions et savoir dans quel module elles rsident, utilisez Hoogle. Cest un moteur de recherche pour Haskell gnial, vous pouvez chercher quelque chose par son nom, par le nom de son module ou mme par son type !

Data.List
Le module Data.List soccupe des listes, videmment. Il contient des fonctions trs pratiques. Nous en avons dj croises quelques unes (comme map et filter ) parce que le module Prelude exporte quelques fonctions de Data.List par commodit. Vous navez pas besoin dimporter Data.List de manire qualifie, il na de collision avec aucun nom du Prelude , part videmment les fonctions que le Prelude lui avait empruntes. Intressons-nous dautres fonctions que nous navions pas encore vues. intersperse prend un lment et une liste et place cet lment entre chaque paire dlments de la liste. Dmonstration : ghci> intersperse '.' "MONKEY" "M.O.N.K.E.Y" ghci> intersperse 0 [1,2,3,4,5,6] [1,0,2,0,3,0,4,0,5,0,6]

intercalate prend une liste de listes et une liste. Elle insre cette dernire entre toutes les listes de la premire et aplatit le rsultat. ghci> intercalate " " ["hey","there","guys"] "hey there guys" ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]] [1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]

transpose transpose une liste de listes. Si vous pensez une liste de listes comme une matrice bidimensionnelle, les colonnes deviennent des lignes et vice versa. ghci> transpose [[1,2,3],[4,5,6],[7,8,9]] [[1,4,7],[2,5,8],[3,6,9]] ghci> transpose ["hey","there","guys"] ["htg","ehu","yey","rs","e"]

Mettons quon ait les polynmes 3x + 5x + 9, 10x + 9 et 8x + 5x + x - 1 et que lon souhaite les additionner. On peut utiliser les listes [0, 3, 5, 9] , [10, 0, 0, 9] et [8, 5, 1, -1] pour les reprsenter en Haskell. Maintenant, pour les sommer, il suffit de faire : ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]] [18,8,6,17]

Lorsquon transpose ces trois listes, les coefficients de degr 3 se retrouvent sur la premire ligne, les coefficients de degr 2 sur la deuxime et ainsi de suite. Mapper sum sur cette liste produit le rsultat dsir. foldl' et foldl1' sont des versions strictes de leur quivalent paresseux. Si vous faites des plis paresseux sur des listes trs grandes, vous pouvez souvent avoir des erreurs de dbordement de pile. Le coupable est la nature paresseuse des plis, la valeur de laccumulateur ntant pas rellement mise jour pendant la phase de pli. Ce qui se passe en ralit, cest que laccumulateur fait en quelque sorte la promesse quil se rappellera comment calculer sa valeur quand on la lui demandera (NDT : pour cela, ce quon appelle un glaon, traduction de thunk , est cr). Cela a lieu pour chaque accumulateur intermdiaire, et tous les glaons sont empils sur votre pile et finissent par la faire dborder. Les plis stricts

ne sont pas de vils paresseux et calculent les valeurs intermdiaires en route au lieu de remplir votre pile de glaons. Donc, si jamais vous rencontrez des problmes de dbordement de pile en faisant des plis paresseux, essayez dutiliser plutt les versions strictes. concat aplatit une liste de listes en une liste simple. ghci> concat ["foo","bar","car"] "foobarcar" ghci> concat [[3,4,5],[2,3,4],[2,1,1]] [3,4,5,2,3,4,2,1,1]

Elle enlvera seulement un niveau dimbrication. Si vous voulez compltement aplatir la liste [[[2,3],[3,4,5],[2]],[[2,3],[3,4]]] , qui est une liste de listes de listes, vous devrez la concatner deux fois. concatMap quivaut dabord mapper une fonction, puis concatner la liste laide de concat . ghci> concatMap (replicate 4) [1..3] [1,1,1,1,2,2,2,2,3,3,3,3]

and prend une liste de valeurs boolennes et retourne True seulement si toutes les valeurs de la liste sont True . ghci> and $ map (>4) [5,6,7,8] True ghci> and $ map (==4) [4,4,4,3,4] False

or est comme and , mais retourne True si nimporte laquelle des valeurs boolennes est True . ghci> or $ map (==4) [2,3,4,5,6,1] True ghci> or $ map (>4) [1,2,3] False

any et all prennent un prdicat et vrifient respectivement si lun ou tous les lments dune liste satisfont ce prdicat. On les utilise gnralement en lieu et place dun mappage du prdicat sur la liste suivi de and ou or . ghci> True ghci> True ghci> False ghci> True any (==4) [2,3,5,6,1,4] all (>4) [6,9,10] all (`elem` ['A'..'Z']) "HEYGUYSwhatsup" any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"

iterate prend une fonction et une valeur initiale. Elle applique la fonction la valeur, puis applique la fonction au rsultat, puis applique la fonction au rsultat nouveau, etc. Elle retourne tous ces rsultats sous la forme dune liste infinie. ghci> take 10 $ iterate (*2) 1 [1,2,4,8,16,32,64,128,256,512] ghci> take 3 $ iterate (++ "haha") "haha" ["haha","hahahaha","hahahahahaha"]

splitAt prend un nombre et une liste. Ensuite, elle coupe la liste au nombre dlments spcifi, retournant chaque bout dans une paire. ghci> splitAt 3 "heyman" ("hey","man") ghci> splitAt 100 "heyman" ("heyman","") ghci> splitAt (-3) "heyman" ("","heyman") ghci> let (a,b) = splitAt 3 "foobar" in b ++ a "barfoo"

takeWhile est trs utile. Elle prend des lments de la liste tant que le prdicat est satisfait et sarrte ds quun lment linvalide. Cest trs utile. ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]

[6,5,4] ghci> takeWhile (/=' ') "This is a sentence" "This"

Si lon cherchait la somme de toutes les puissances de 3 infrieures 10 000, on ne pourrait pas mapper (^3) [1..] , puis appliquer un filtre et sommer le rsultat, parce que filter ne termine pas sur des listes infinies. Vous savez peut-tre que les lments sont croissants, mais Haskell ne le sait pas. Vous pouvez donc faire a : ghci> sum $ takeWhile (<10000) $ map (^3) [1..] 53361

On applique (^3) une liste infinie et on coupe ds que a dpasse 10 000. Il ne reste plus qu sommer aisment. dropWhile est similaire, mais elle jette les lments tant que le prdicat est vrai. Une fois que le prdicat est invalid, elle retourne ce qui reste de la liste. Fonction trs utile et adorable ! ghci> dropWhile (/=' ') "This is a sentence" " is a sentence" ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1] [3,4,5,4,3,2,1]

Imaginons quon ait une liste des valeurs dun stock par date. La liste est compose de tuples dont la premire composante est la valeur du stock, la deuxime est lanne, la troisime le mois, la quatrime le jour. On dsire savoir quand est-ce que la valeur du stock a dpass 1000 $ pour la premire fois ! ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)] ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock) (1001.4,2008,9,4)

span est un peu comme takeWhile , mais retourne une paire de listes. La premire liste contient tout ce que contiendrait la liste retourne par takeWhile appele avec le mme prdicat et la mme liste. La seconde liste correspond ce qui aurait t laiss. ghci> let (fw, rest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ", the rest:" ++ rest "First word: This, the rest: is a sentence"

Alors que span stend sur la liste tant que la prdicat est vrai, break la coupe ds quil devient vrai. Ainsi, break p est quivalent span (not . p) . ghci> break (==4) [1,2,3,4,5,6,7] ([1,2,3],[4,5,6,7]) ghci> span (/=4) [1,2,3,4,5,6,7] ([1,2,3],[4,5,6,7])

Dans le tuple retourn par break , la seconde liste dbute avec le premier lment ayant satisfait le prdicat. sort trie une liste. Le type des lments de la liste doit tre membre de la classe de types Ord , parce que si lon ne peut pas ordonner les lments, eh bien on ne peut pas les trier. ghci> sort [8,5,3,2,1,6,4,2] [1,2,2,3,4,5,6,8] ghci> sort "This will be sorted soon" " Tbdeehiillnooorssstw"

group prend une liste et groupe les lments adjacents en sous-listes sils sont gaux. ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7] [[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]

Si lon trie une liste avant de grouper ses lments, on peut savoir combien de fois chaque lment apparat dans la liste. ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7] [(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]

inits et tails sont comme init et tail , sauf quelles appliquent ces fonctions rcursivement une liste jusqu ce quil ne reste plus rien. Observez :

ghci> inits "w00t" ["","w","w0","w00","w00t"] ghci> tails "w00t" ["w00t","00t","0t","t",""] ghci> let w = "w00t" in zip (inits w) (tails w) [("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]

Utilisons un pli pour implmenter la recherche dune liste dans une sous-liste. search :: (Eq a) => [a] -> [a] -> Bool search needle haystack = let nlen = length needle in foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)

Dabord, on appelle tails avec la liste quon cherche. Puis on parcourt chaque queue retourne pour voir si elle commence par ce quon cherche. On vient de faire une fonction qui se comporte comme isInfixOf . isInfixOf cherche une sous-liste dans une liste et retourne True si la sous-liste quon cherche apparat quelque part dans la liste cible. ghci> "cat" `isInfixOf` "im a cat burglar" True ghci> "Cat" `isInfixOf` "im a cat burglar" False ghci> "cats" `isInfixOf` "im a cat burglar" False

isPrefixOf et isSuffixOf cherchent une sous-liste respectivement au dbut et la fin dune liste. ghci> True ghci> False ghci> True ghci> False "hey" `isPrefixOf` "hey there!" "hey" `isPrefixOf` "oh hey there!" "there!" `isSuffixOf` "oh hey there!" "there!" `isSuffixOf` "oh hey there"

elem et notElem cherchent si un lment appartient ou nappartient pas une liste. partition prend une liste et un prdicat, et retourne une paire de listes. La premire liste contient tous les lments qui satisfont le prdicat, la seconde tous les autres. ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy" ("BOBMORGAN","sidneyeddy") ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7] ([5,6,7],[1,3,3,2,1,0,3])

Il est important de saisir la diffrence avec span et break : ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy" ("BOB","sidneyMORGANeddy")

Alors que span et break sarrtent ds quils rencontrent le premier lment qui ne satisfait pas ou qui satisfait le prdicat, partition traverse toute la liste et la dcoupe conformment au prdicat. find prend une liste et un prdicat et retourne le premier lment de la liste satisfaisant le prdicat. Mais il retourne ce prdicat encapsul dans une valeur de type Maybe . Nous couvrirons les types de donnes algbriques plus en profondeur dans le prochain chapitre, mais pour linstant, voil ce que vous avez besoin de savoir : une valeur Maybe peut soit tre Just something , soit Nothing . Tout comme une liste peut tre soit la liste vide, soit une liste avec des lments, une valeur Maybe peut contenir soit aucun, soit un lement. Et comme le type dune liste dentiers est par exemple [Int] , le type dun ventuel entier est Maybe Int . Bon, faisons un tour de find : ghci> find (>4) [1,2,3,4,5,6] Just 5 ghci> find (>9) [1,2,3,4,5,6] Nothing ghci> :t find find :: (a -> Bool) -> [a] -> Maybe a

Prtez attention au type de find . Elle retourne un Maybe a . Cest un peu comme sil y avait un [a] , seulement quune valeur de type Maybe ne peut contenir quun ou zro lment, alors quune liste peut en contenir zro, un ou plus. Vous vous souvenez quand nous cherchions la premire fois que notre stock dpassait 1000 $ ? On avait fait head (dropWhile (\(val,y,m,d) -> val < 1000) stock) . Rappelez-vous que head nest pas sre. Que se serait-il pass si le stock navait jamais dpass 1000 $ ? Notre application de dropWhile aurait retourn la liste vide, et essayer de prendre sa tte aurait t une erreur. Cependant, si lon rcrivait cela find (\(val,y,m,d) -> val > 1000) stock , on serait beaucoup plus tranquille. Si notre stock ne dpassait jamais 1000 $ (donc, aucun lment ne satisfait le prdicat), on rcuprerait Nothing . Mais sil y avait une rponse correcte dans la liste, on rcuprerait, mettons, Just (1001.4, 2008, 9, 4) . elemIndex est un peu comme elem , mais ne retourne pas une valeur boolenne. Elle retourne ventuellement lindice de llment quon cherche. Si cet lment nest pas dans la liste, elle retourne Nothing . ghci> :t elemIndex elemIndex :: (Eq a) => a -> [a] -> Maybe Int ghci> 4 `elemIndex` [1,2,3,4,5,6] Just 3 ghci> 10 `elemIndex` [1,2,3,4,5,6] Nothing

elemIndices est comme elemIndex , seulement quelle retourne une liste dindices dans le cas o llment que lon cherche apparatrait plusieurs fois dans la liste. Puisquon utilise des listes pour reprsenter les indices, on na pas besoin dun type Maybe , il suffit de reprsenter un chec comme une liste vide, qui sera donc trs synonyme de Nothing . ghci> ' ' `elemIndices` "Where are the spaces?" [5,9,13]

findIndex est comme find , mais retourne ventuellement lindice du premier lment qui satisfait le prdicat. findIndices retourne les indices de tous les lments qui satisfont le prdicat sous forme dune liste. ghci> findIndex (==4) [5,3,2,1,6,4] Just 5 ghci> findIndex (==7) [5,3,2,1,6,4] Nothing ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?" [0,6,10,14]

Nous avons dj couvert zip et zipWith . Nous avions vu quelles zippent ensemble deux listes, soit sous la forme dun tuple, soit en appliquant une fonction binaire (cest--dire qui prend deux paramtres). Mais comment zipper ensemble trois listes ? Ou zipper trois listes laide dune fonction qui attend trois paramtres ? Pour cela, il existe zip3 , zip4 , etc. et zipWith3 , zipWith4 , etc. Ces variantes existent jusqu 7. Bien que a ait lair un peu arbitraire et ad hoc, a savre plutt suffisant, car vous ne voudrez srement jamais zipper ensemble 8 listes. Il existe un moyen astucieux de zipper ensemble une infinit de listes, mais nous ne sommes pas assez avanc pour couvrir a maintenant. ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3] [7,9,8] ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2] [(2,2,5,2),(3,2,5,2),(3,2,3,2)]

Comme un zip normal, des listes plus longues que la plus courte des listes zippes seront coupes. lines est une fonction trs utile pour traiter des fichiers ou une entre. Elle prend une chane de caractres et retourne une liste des diffrentes lignes. ghci> lines "first line\nsecond line\nthird line" ["first line","second line","third line"]

'\n' est le caractre UNIX pour aller la ligne. Lantislash a une signification spciale dans les caractres et les chanes de caractres. unlines est la fonction rciproque de lines . Elle prend une liste de chanes de caractres et les joint ensemble avec des '\n' . ghci> unlines ["first line", "second line", "third line"] "first line\nsecond line\nthird line\n"

words et unwords servent sparer une ligne de texte en plusieurs mots ou joindre une liste de mots en un texte. Trs pratique.

ghci> words "hey these are the words in this sentence" ["hey","these","are","the","words","in","this","sentence"] ghci> words "hey these are the words in this\nsentence" ["hey","these","are","the","words","in","this","sentence"] ghci> unwords ["hey","there","mate"] "hey there mate"

Nous avons dj mentionn nub . Elle prend une liste et retire les lments en double, retournant une liste o chaque lment est aussi unique quun flocon de neige ! Le nom de la fonction est un peu bizarre. Il savre que nub dsigne lessence, le cur de quelque chose. mon avis, ils devraient plutt utiliser des vrais noms pour les fonctions, pas des mots de vieilles personnes. ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1] [1,2,3,4] ghci> nub "Lots of words and stuff" "Lots fwrdanu"

delete prend un lment, une liste et supprime la premire occurrence de cet lment dans la liste. ghci> delete 'h' "hey there ghang!" "ey there ghang!" ghci> delete 'h' . delete 'h' $ "hey there ghang!" "ey tere ghang!" ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!" "ey tere gang!"

\\ est la fonction de diffrence sur les listes. Elle se comporte comme la diffrence ensembliste. Pour chaque lment de la liste de droite, elle supprime un lment correspondant dans la liste de gauche. ghci> [1..10] \\ [2,5,9] [1,3,4,6,7,8,10] ghci> "Im a big baby" \\ "big" "Im a baby"

Faire [1..10] \\ [2, 5, 9] est comme faire delete 2 . delete 5 . delete 9 $ [1..10] . union se comporte aussi comme une fonction sur les ensembles. Elle retourne lunion de deux listes. En gros, elle parcourt toute la liste de droite et ajoute les lments en queue de celle de gauche sils ny sont pas dj. Attention, elle supprime les doublons dans la liste de droite ! ghci> "hey man" `union` "man what's up" "hey manwt'sup" ghci> [1..7] `union` [5..10] [1,2,3,4,5,6,7,8,9,10]

intersect fonctionne comme lintersection ensembliste. Elle ne retourne que les lments apparaissant dans les deux listes. ghci> [1..7] `intersect` [5..10] [5,6,7]

insert prend un lment et une liste dlments qui peuvent tre tris et insre llment la dernire position o il reste infrieur au prochain lment. insert va parcourir la liste jusqu trouver un lment plus grand que celui pass en paramtre et va alors insrer ce dernier devant lautre. ghci> insert 4 [3,5,1,2,8,2] [3,4,5,1,2,8,2] ghci> insert 4 [1,3,4,4,1] [1,3,4,4,4,1]

Le 4 est insr juste aprs le 3 et juste avant le 5 dans le premier exemple et entre le 3 et le 4 dans le second. Proprit intressante : si lon utilise insert pour insrer dans une liste trie, la liste reste trie. ghci> insert 4 [1,2,3,5,6,7] [1,2,3,4,5,6,7] ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z'] "abcdefghijklmnopqrstuvwxyz" ghci> insert 3 [1,2,4,3,2,1] [1,2,3,4,3,2,1]

Ce que length , take , drop , splitAt , !! et replicate ont de commun, cest quelles prennent ou retournent un Int , alors quelles pourraient tre plus gnrales en utilisant plutt un type appartenant aux classes Integral ou Num (en fonction de la fonction). Elles ne le font pas pour des raisons historiques. Rparer cela dtruirait certainement beaucoup de code existant. Cest pourquoi Data.List contient les quivalents plus gnriques, nomms genericLength , genericTake , genericDrop , genericSplitAt , genericIndex et genericReplicate . Par exemple, length a pour signature length :: [a] -> Int . Si lon essaie de rcuprer la moyenne dune liste en faisant let xs = [1..6] in sum xs / length xs , on obtient une erreur de type, parce que / ne fonctionne pas sur les Int . genericLength , au contraire, a pour signature genericLength :: (Num a) => [b] -> a . Puisquun Num peut se faire passer pour un nombre virgule flottante, trouver la moyenne en faisant let xs = [1..6] in sum xs / genericLength xs marchera. Les fonctions nub , delete , union , intersect et group ont toute un quivalent plus gnral, respectivement nubBy , deleteBy , unionBy , intersectBy et groupBy . La diffrence entre les deux, cest que les premires utilisent == pour tester lgalit, alors que les versions en By prennent en paramtre une fonction utilise pour tester lgalit. group est donc quivalente groupBy (==) . Par exemple, mettons quon a une liste qui dcrit les valeurs dune fonction chaque seconde, et on voudrait la segmenter entre les valeurs positives et les valeurs ngatives. Un group normal ne regrouperait que les valeurs adjacentes gales entre elles. On voudrait les grouper en fonction de leur signe. Cest l que groupBy entre en jeu ! La fonction dgalit des fonctions en By doit prendre deux lments de mme type et retourne True si elle les considre gales selon ses propres critres. ghci> let values = [-4.3, -2.4, -1.2, 0.4, 2.3, 5.9, 10.5, 29.1, 5.3, -2.4, -14.5, 2.9, 2.3] ghci> groupBy (\x y -> (x > 0) == (y > 0)) values [[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

On voit ici clairement quelles sections sont positives et ngatives. La fonction dgalit fournie prend deux lments et retourne True seulement sils sont tous les deux positifs ou tous les deux ngatifs. Elle pourrait aussi tre crite \x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0) , bien que je trouve la premire version plus lisible. Une manire encore plus claire dcrire les fonctions dgalit pour les fonctions en By consiste importer on depuis Data.Function . on est dfinie ainsi : on :: (b -> b -> c) -> (a -> b) -> a -> a -> c f `on` g = \x y -> f (g x) (g y)

Ainsi, faire (==) `on` (> 0) retourne une fonction dgalit qui ressemble \x y -> (x > 0) == (y > 0) . on est trs utilise avec les fonctions By parce quavec elle, on peut faire : ghci> groupBy ((==) `on` (> 0)) values [[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]

Super lisible, non ? Vous pouvez le lire tout haut : groupe ceci par galit sur le fait que les lments soient plus grands que zro. De faon similaire, les fonctions sort , insert , maximum et minimum ont aussi un quivalent plus gnral. Les fonctions comme groupBy prenaient une fonction pour dterminer si deux lments taient gaux. sortBy , insertBy , maximumBy et minimumBy prennent une fonction pour dterminer si un lment est plus petit, plus grand ou gal lautre. La signature de sortBy est sortBy :: (a -> a -> Ordering) -> [a] -> [a] . Si vous vous souvenez bien, le type Ordering peut prendre pour valeurs LT , EQ ou GT . sort est quivalent sortBy compare , car compare prend juste deux lments dont le type est membre de la classe Ord et retourne leur relation dordre. Les listes peuvent tre compares, mais lorsquelles le sont, elles sont compares lexicographiquement. Et si lon avait une liste de listes et que lon souhaitait la trier non pas par rapport au contenu des listes internes, mais plutt en fonction de leur longueur ? Vous lavez peut-tre dj devin, on utilisera la fonction sortBy . ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]] ghci> sortBy (compare `on` length) xs [[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]

Gnial ! compare `on` length man, on dirait presque de langlais naturel ! Si vous ntes pas certain de ce que fait on ici, compare `on` length est quivalent y -> length x `compare` length y . Quand vous utilisez des fonctions en By qui attendent une fonction dgalit, vous ferez souvent (==) `on` something , et quand vous utilisez des fonctions en By qui attendent une fonction de comparaison, vous ferez souvent compare `on` length .

Data.Char
Le module Data.Char fait ce que son nom indique. Il exporte des fonctions qui agissent sur des caractres. Et qui servent aussi pour filtrer ou mapper sur des chanes de caractres, puisque ce ne sont que des listes de caractres. Data.Char exporte tout un tas de prdicats sur les caractres. Cest--dire, des fonctions qui prennent un caractre et nous indiquent sil satisfait une condition. Les voici :

isControl vrifie si cest un caractre de contrle (NDT : caractres non affichables du sous-ensemble Latin-1 dUnicode). isSpace vrifie si cest un caractre despacement. Cela inclue les espaces, les tabulations, les nouvelles lignes, etc. isLower vrifie si le caractre est minuscule. isUpper vrifie si le caractre est majuscule. isAlpha vrifie si le caractre est une lettre. isAlphaNum vrifie si le caractre est une lettre ou un chiffre. isPrint vrifie si le caractre est affichable. Les caractres de contrle, par exemple, ne le sont pas. isDigit vrifie si le caractre est un chiffre. isOctDigit vrifie si le caractre est un chiffre octal. isHexDigit vrifie si le caractre est un chiffre hexadcimal. isLetter vrifie si le caractre est une lettre. isMark vrifie les caractres Unicode de marque. Ce sont des caractres qui se combinent au prcdent pour crer des caractres accentus. Utile pour nous franais. isNumber vrifie si un caractre est numrique. isPunctuation vrifie si le caractre est un caractre de ponctuation. isSymbol vrifie si le caractre est un symbole mathmatique ou montaire. isSeparator vrifie les espaces et sparateurs Unicode. isAscii vrifie si un caractre fait partie des 128 premiers caractres Unicode. isLatin1 vrifie si le code fait partie des 256 premiers caractres Unicode. isAsciiUpper vrifie si cest un caractre ASCII majuscule. isAsciiLower vrifie si cest un caractre ASCII minuscule. Tous ces prdicats ont pour signature Char -> Bool . La plupart du temps, vous les utiliserez pour filtrer des chanes de caractres. Par exemple, disons quon crive un programme qui prend un nom dutilisateur, et que ce nom doit tre compos seulement de caractres alphanumriques. On peut utiliser la fonction all de Data.List en combinaison avec les prdicats de Data.Char pour vrifier la validit du nom de lutilisateur. ghci> all isAlphaNum "bobby283" True ghci> all isAlphaNum "eddy the fish!" False

Cool ! Au cas o vous auriez oubli, all prend un prdicat et retourne True seulement si tous les lments de la liste valident ce prdicat. On peut aussi utiliser isSpace pour simuler la fonction words de Data.List . ghci> words "hey guys its me" ["hey","guys","its","me"] ghci> groupBy ((==) `on` isSpace) "hey guys its me" ["hey"," ","guys"," ","its"," ","me"] ghci>

Hmmm a marche peu prs comme words , mais il nous reste les lments qui ne contiennent que des espaces. Que devrions-nous faire ? Je sais, filtrons ces importuns ! ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey guys its me" ["hey","guys","its","me"]

Ah !

Le module Data.Char exporte galement un type de donne similaire Ordering . Ordering peut prendre pour valeur LT , EQ ou GT . Cest une sorte dnumration. Cela dcrit les ventualits du rsultat dune comparaison. Le type GeneralCategory est aussi une numration. Il dcrit les diffrentes catgories auxquelles un caractre peut appartenir. La fonction principale retournant la catgorie dun caractre est generalCategory . Son type est generalCategory :: Char -> GeneralCategory . Il y a environ 31 catgories, donc on ne va pas les lister ici, mais jouons un peu avec la fonction. ghci> generalCategory ' ' Space ghci> generalCategory 'A' UppercaseLetter ghci> generalCategory 'a' LowercaseLetter ghci> generalCategory '.' OtherPunctuation ghci> generalCategory '9' DecimalNumber ghci> map generalCategory " \t\nA9?|" [Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]

Puisque le type GeneralCategory est membre de la classe Eq , on peut aussi crire des choses comme generalCategory c == Space . toUpper convertit un caractre minuscule en majuscule. Les espaces, nombres et autres restent inchangs. toLower convertit un caractre majuscule en minuscule. toTitle convertit un caractre en casse de titre. Pour la plupart des caractres, la casse de titre est majuscule. digitToInt convertit un caractre en un Int . Pour russir, le caractre doit tre dans les intervalles '0'..'9' , 'a'..'f' ou 'A'..'F' . ghci> map digitToInt "34538" [3,4,5,3,8] ghci> map digitToInt "FF85AB" [15,15,8,5,10,11]

intToDigit est la fonction inverse de digitToInt . Elle prend un Int compris dans 0..15 et le convertit en caractre minuscule. ghci> intToDigit 15 'f' ghci> intToDigit 5 '5'

Les fonctions ord et chr convertissent un caractre vers sa valeur numrique et vice versa. ghci> ord 'a' 97 ghci> chr 97 'a' ghci> map ord "abcdefgh" [97,98,99,100,101,102,103,104]

La diffrence entre les valeurs ord de deux caractres correspond leur espacement dans la table Unicode. Le chiffre de Csar est une mthode primitive dencodage de messages base de dcalage de chaque caractre dun nombre fix de positions, par rapport lalphabet. On peut facilement crer un chiffre de Csar nous-mmes, sans se restreindre lalphabet. encode :: Int -> String -> String encode shift msg = let ords = map ord msg shifted = map (+ shift) ords in map chr shifted

Ici, on convertit dabord la chane de caractres en une liste de nombres. Puis, on ajoute le dcalage chacun des nombres, avant de reconvertir cette liste de nombres en caractres. Si vous tes un cowboy de la composition, vous pouvez crire le corps de cette fonction comme map (chr . (+ shift) . ord) msg . Essayons dencoder quelques messages. ghci> encode 3 "Heeeeey" "Khhhhh|" ghci> encode 4 "Heeeeey" "Liiiii}" ghci> encode 1 "abcd"

"bcde" ghci> encode 5 "Marry Christmas! Ho ho ho!" "Rfww~%Hmwnxyrfx&%Mt%mt%mt&"

Cest effectivement encod. Dcoder ces messages consiste simplement les dcaler dans le sens oppos et du mme nombre de places que le dcalage initial. decode :: Int -> String -> String decode shift msg = encode (negate shift) msg

ghci> encode 3 "Im a little teapot" "Lp#d#olwwoh#whdsrw" ghci> decode 3 "Lp#d#olwwoh#whdsrw" "Im a little teapot" ghci> decode 5 . encode 5 $ "This is a sentence" "This is a sentence"

Data.Map
Les listes associatives (ou dictionnaires) sont des listes utilises pour stocker des paires cl-valeur, o lordre nest pas important. Par exemple, on peut utiliser une liste associative pour stocker des numros de tlphone, o les numros de tlphone seraient les valeurs, et les noms des personnes les cls. On se fiche de lordre dans lequel cest rang, on souhaite seulement pouvoir rcuprer le bon numro de tlphone pour une personne donne. La faon la plus vidente de reprsenter des listes associatives en Haskell est sous forme de listes de paires. La premire composante de la paire serait la cl, la seconde serait la valeur. Voici un exemple de liste associative de numros de tlphone : phoneBook = [("betty","555-2938") ,("bonnie","452-2928") ,("patsy","493-2928") ,("lucille","205-2928") ,("wendy","939-8282") ,("penny","853-2492") ]

Malgr cette indentation bizarre, cest bien une liste de paires de chanes de caractres. Lopration la plus courante associe aux listes associatives est la recherche dune valeur par sa cl. Crons une fonction qui trouve une valeur partir dune cl. findKey :: (Eq k) => k -> [(k,v)] -> v findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs

Plutt simple. La fonction prend une cl, une liste, filtre la liste de faon ce que seules les cls correspondantes ne restent, prend la premire paire cl-valeur et en retourne la valeur. Mais que se passe-t-il si la cl que lon cherche nest pas dans la liste ? Hmmm. Ici, si la cl nest pas dans la liste, on va essayer de prendre la tte dune liste vide, ce qui gnre une erreur lexcution. On ne devrait pas laisser notre programme planter si facilement, utilisons donc un type de donnes Maybe . Si lon ne trouve pas la cl, on retourne Nothing . Si lon trouve quelque chose, on retourne Just something , o something sera la valeur correspondant la cl. findKey :: (Eq k) => k -> [(k,v)] -> Maybe v findKey key [] = Nothing findKey key ((k,v):xs) = if key == k then Just v else findKey key xs

Regardez la dclaration de type. Elle prend une cl qui dispose dun test dgalit, une liste associative et retourne ventuellement une valeur. a a lair correct. Ceci est un cas dcole de fonction rcursive oprant sur une liste. Cas de base, dcoupage de la liste en queue et tte, appels rcursifs, cest tout ce quil se passe. Cest le motif classique du pli, implmentons-le donc comme un pli. findKey :: (Eq k) => k -> [(k,v)] -> Maybe v findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing

Note : il est gnralement mieux dutiliser des plis pour effectuer de la rcursivit standard sur les listes plutt que dcrire explicitement la rcursivit, car cest plus simple lire et identifier. Tout le monde connat les plis et sait en identifier un lorsquil voit un appel foldr , mais cela prend plus de temps de dchiffrer une rcursivit explicite.

ghci> findKey "penny" phoneBook Just "853-2492" ghci> findKey "betty" phoneBook Just "555-2938" ghci> findKey "wilma" phoneBook Nothing

a fonctionne comme un charme ! Si on a le numro de tlphone de cette fille, on rcupre Just le numro, sinon, on rcupre Nothing . On vient juste dimplmenter la fonction lookup de Data.List . Si lon veut trouver la valeur correspondant une cl, on doit traverser la liste jusqu ce quon la trouve. Le module Data.Map offre des listes associatives qui sont beaucoup plus rapides (car elles sont implmentes laide darbres) et fournit galement un paquet de fonctions utiles. partir de maintenant, nous dirons quon travaille sur des maps plutt que des listes associatives. Data.Map exporte des fonctions qui collisionnent avec le Prelude et Data.List , donc on limporte qualifi. import qualified Data.Map as Map

Placez cette ligne dans un script et chargez-le dans GHCi. Allons prsent voir ce que Data.Map a en magasin pour nous ! Voil la liste des fonctions. fromList prend une liste associative (sous forme de liste) et retourne une map avec les mmes associations. ghci> Map.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")] fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")] ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)] fromList [(1,2),(3,2),(5,5)]

Sil y a des cls en doublon dans la liste originale, les doublons sont ignors. Voici la signature de type de fromList : Map.fromList :: (Ord k) => [(k, v)] -> Map.Map k v

Elle indique quelle prend une liste de paires k et v et retourne une map qui mappe des cls de type k sur des valeurs de type v . Remarquez que lorsquon faisait des listes associatives sous forme de liste, on avait juste besoin de lgalit sur les cls (leur type appartenait Eq ), mais prsent elles doivent tre ordonnables. Cest une contrainte essentielle pour le module Data.Map . Il a besoin de pouvoir ordonner les cls afin de les arranger en arbre. Vous devriez toujours utiliser Data.Map pour associer des cls-valeurs, moins que vous ayez des cls qui ne sont pas membres d Ord . empty reprsente une map vide. Elle ne prend pas dargument et retourne une map vide. ghci> Map.empty fromList []

insert prend une cl, une valeur et une map, et retourne une nouvelle map identique lancienne, avec en plus la nouvelle cl-valeur insre. ghci> Map.empty fromList [] ghci> Map.insert 3 100 Map.empty fromList [(3,100)] ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100 Map.empty)) fromList [(3,100),(4,200),(5,600)] ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty fromList [(3,100),(4,200),(5,600)]

On peut implmenter notre propre fromList en utilisant la map vide, insert et un pli. Observez : fromList' :: (Ord k) => [(k,v)] -> Map.Map k v fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty

Cest un pli plutt simple. On commence avec la map vide et on la plie depuis la droite, en insrant les paires cl-valeur dans laccumulateur tout du long. null vrifie si une map est vide.

ghci> Map.null Map.empty True ghci> Map.null $ Map.fromList [(2,3),(5,5)] False

size retourne la taille dune map. ghci> Map.size Map.empty 0 ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)] 5

singleton prend une cl et une valeur et cre une map qui contient uniquement cette association. ghci> Map.singleton 3 9 fromList [(3,9)] ghci> Map.insert 5 9 $ Map.singleton 3 9 fromList [(3,9),(5,9)]

lookup fonctionne comme lookup de Data.List , mais opre sur des maps. Elle retourne Just something si elle trouve quelque chose, Nothing sinon. member est un prdicat qui prend une cl, une map et indique si la cl est dans la map. ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)] True ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)] False

map et filter fonctionnent comme leur quivalent. ghci> Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)] fromList [(1,100),(2,400),(3,900)] ghci> Map.filter isUpper $ Map.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')] fromList [(2,'A'),(4,'B')]

toList est linverse de fromList . ghci> Map.toList . Map.insert 9 2 $ Map.singleton 4 3 [(4,3),(9,2)]

keys et elems retournent des listes de cls et de valeurs respectivement. keys est lquivalent de map fst . Map.toList , et elems lquivalent de map snd . Map.toList . fromListWith est une petite fonction assez cool. Elle agit un peu comme fromList , mais elle ne jette pas les cls en double et utilise une fonction passe en paramtre pour dcider quoi faire delles. Disons quune fille peut avoir plusieurs numros de tlphone, et quon a une liste associative comme celle-ci : phoneBook = [("betty","555-2938") ,("betty","342-2492") ,("bonnie","452-2928") ,("patsy","493-2928") ,("patsy","943-2929") ,("patsy","827-9162") ,("lucille","205-2928") ,("wendy","939-8282") ,("penny","853-2492") ,("penny","555-2111") ]

Maintenant, si on utilise seulement fromList pour transformer cela en map, on va perdre quelques numros ! Voil ce quon va faire : phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs

ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook "827-9162, 943-2929, 493-2928"

ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook "939-8282" ghci> Map.lookup "betty" $ phoneBookToMap phoneBook "342-2492, 555-2938"

Si une cl en double est trouve, la fonction quon passe est utilise pour combiner les valeurs en une nouvelle valeur. On aurait aussi pu transformer toutes les valeurs de la liste en listes singleton, puis utiliser (++) comme combinateur. phoneBookToMap :: (Ord k) => [(k, a)] -> Map.Map k [a] phoneBookToMap xs = Map.fromListWith (++) $ map (\(k,v) -> (k,[v])) xs

ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook ["827-9162","943-2929","493-2928"]

Plutt chic ! Un autre cas dutilisation est celui o lon veut crer une map partir dune liste dassociation, et lors dun doublon, lon souhaite conserver la plus grande des valeurs par exemple. ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)] fromList [(2,100),(3,29),(4,22)]

On pourrait tout aussi bien choisir dadditionner des valeurs de mme cl. ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)] fromList [(2,108),(3,62),(4,37)]

insertWith est insert ce que fromListWith est fromList . Elle insre une paire cl-valeur dans la map, et si la map contient dj cette cl, utilise la fonction passe afin de dterminer quoi faire. ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)] fromList [(3,104),(5,103),(6,339)]

Ce ntait quun aperu de Data.Map . Vous pouvez voir la liste complte des fonctions dans la documentation.

Data.Set
Le module Data.Set nous offre des ensembles. Comme les ensembles en mathmatiques. Les ensembles sont un peu comme un mlange de listes et de maps. Tous les lments dun ensemble sont uniques. Et comme ils sont implments en interne laide darbres (comme les maps de Data.Map ), ils sont ordonns. Vrifier lappartenance, insrer, supprimer, etc. sont des oprations bien plus rapides sur les ensembles que sur les listes. Les oprations les plus courantes sur les ensembles sont linsertion, le test dappartenance et la conversion en liste. Les noms dans Data.Set collisionnent beaucoup avec le Prelude et Data.List , on fait donc un import qualifi. Placez cette dclaration dans un script : import qualified Data.Set as Set

Puis chargez ce script via GHCi. Supposons quon ait deux bouts de texte. On souhaite trouver les caractres utiliss dans les deux chanes. text1 = "I just had an anime dream. Anime... Reality... Are they so different?" text2 = "The old man left his garbage can out and now his trash is all over my lawn!"

La fonction fromList fonctionne comme on peut sy attendre. Elle prend une liste et la convertit en un ensemble. ghci> let set1 = Set.fromList text1 ghci> let set2 = Set.fromList text2 ghci> set1 fromList " .?AIRadefhijlmnorstuy" ghci> set2 fromList " !Tabcdefghilmnorstuvwy"

Comme vous pouvez le voir, les lments sont ordonns et chaque lment est unique. Maintenant, utilisons la fonction intersection pour voir les lments quils partagent. ghci> Set.intersection set1 set2 fromList " adefhilmnorstuy"

On peut utiliser la fonction difference pour voir quelles lettres sont dans le premier ensemble mais pas le second, et vice versa. ghci> Set.difference set1 set2 fromList ".?AIRj" ghci> Set.difference set2 set1 fromList "!Tbcgvw"

Ou bien, on peut voir les lettres uniques chaque ensemble en utilisant union . ghci> Set.union set1 set2 fromList " !.?AIRTabcdefghijlmnorstuvwy"

Les fonctions null , size , member , empty , singleton , insert et delete fonctionnent toutes comme on peut sy attendre. ghci> Set.null Set.empty True ghci> Set.null $ Set.fromList [3,4,5,5,4,3] False ghci> Set.size $ Set.fromList [3,4,5,3,4,5] 3 ghci> Set.singleton 9 fromList [9] ghci> Set.insert 4 $ Set.fromList [9,3,8,1] fromList [1,3,4,8,9] ghci> Set.insert 8 $ Set.fromList [5..10] fromList [5,6,7,8,9,10] ghci> Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5] fromList [3,5]

On peut aussi tester si un ensemble est un sous-ensemble (ou sous-ensemble strict) dun autre. Un ensemble A est sous-ensemble de B si B contient tous les lments de A. A est un sous-ensemble strict de B si B contient tous les lments de A, mais a strictement plus dlments. ghci> True ghci> True ghci> False ghci> False Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5] Set.fromList [1,2,3,4,5] `Set.isSubsetOf` Set.fromList [1,2,3,4,5] Set.fromList [1,2,3,4,5] `Set.isProperSubsetOf` Set.fromList [1,2,3,4,5] Set.fromList [2,3,4,8] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]

On peut aussi map et filter les ensembles. ghci> Set.filter odd $ Set.fromList [3,4,5,6,7,2,3,4] fromList [3,5,7] ghci> Set.map (+1) $ Set.fromList [3,4,5,6,7,2,3,4] fromList [3,4,5,6,7,8]

Les ensembles sont souvent utiliss pour supprimer les doublons dune liste en la transformant avec fromList en ensemble, puis en la reconvertissant en liste laide de toList . La fonction nub de Data.List fait aussi a, mais supprimer les doublons dune liste est beaucoup plus rapide en convertissant la liste en ensemble puis en liste plutt quen utilisant nub . Mais nub ne ncessite quune contrainte de classe Eq sur le type des lments, alors que pour la transformer en ensemble, le type doit tre membre de Ord . ghci> let setNub xs = Set.toList $ Set.fromList xs ghci> setNub "HEY WHATS CRACKALACKIN" " ACEHIKLNRSTWY" ghci> nub "HEY WHATS CRACKALACKIN" "HEY WATSCRKLIN"

setNub est gnralement plus rapide que nub sur des grosses listes, mais comme vous pouvez le voir, nub prserve lordre des lments alors que setNub les mlange.

Crer nos propres modules


On vient de regarder des modules plutt cools, mais comment cre-t-on nos propres modules ? Presque tous les langages de programmation vous permettent de dcouper votre code en plusieurs fichiers et Haskell galement. Lorsquon programme, il est de bonne pratique de prendre des fonctions et des types qui partagent un but similaire et de les placer dans un module. Ainsi, vous pouvez facilement rutiliser ces fonctions plus tard dans dautres programmes juste en important le module. Voyons comment faire nos propres modules en crant un petit module fournissant des fonctions de calcul de volume et daire dobjets gomtriques. Commenons par crer un fichier Geometry.hs . On dit quun module exporte des fonctions. Cela signifie que quand jimporte un module, je peux utiliser les fonctions que celui-ci exporte. Il peut dfinir des fonctions que ses propres fonctions appellent en interne, mais on peut seulement voir celles quil a exportes. Au dbut dun module, on spcifie le nom du module. On a cr un fichier Geometry.hs , nous devrions donc nommer notre module Geometry . Puis, nous spcifions les fonctions quil exporte, et aprs cela, on peut commencer crire nos fonctions. Dmarrons. module Geometry ( sphereVolume , sphereArea , cubeVolume , cubeArea , cuboidArea , cuboidVolume ) where

Comme vous pouvez le voir, nous allons faire des aires et des volumes de sphres, de cubes et de pavs droits. Dfinissons nos fonctions : module Geometry ( sphereVolume , sphereArea , cubeVolume , cubeArea , cuboidArea , cuboidVolume ) where sphereVolume :: Float -> Float sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3) sphereArea :: Float -> Float sphereArea radius = 4 * pi * (radius ^ 2) cubeVolume :: Float -> Float cubeVolume side = cuboidVolume side side side cubeArea :: Float -> Float cubeArea side = cuboidArea side side side cuboidVolume :: Float -> Float -> Float -> Float cuboidVolume a b c = rectangleArea a b * c cuboidArea :: Float -> Float -> Float -> Float cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2 rectangleArea :: Float -> Float -> Float rectangleArea a b = a * b

De la gomtrie lmentaire. Quelques choses noter tout de mme. Puisquun cube est un cas spcial de pav droit, on a dfini son aire et son volume comme ceux dun pav dont les cts ont tous la mme longueur. On a galement dfini la fonction auxiliaire rectangleArea , qui calcule laire dun rectangle partir des longueurs de ses cts. Cest plutt trivial puisquil sagit dune simple multiplication. Remarquez comme on lutilise dans nos fonctions de ce module (dans cuboidArea et cuboidVolume ), mais on ne lexporte pas ! On souhaite que notre module prsente des fonctions de calcul sur des objets en trois dimensions, donc on nexporte pas rectangleArea . Quand on cre un module, on nexporte en gnral que les fonctions qui agissent en rapport avec linterface de notre module, de manire cacher limplmentation. Si quelquun utilise notre module Geometry , il na pas se soucier des fonctions que lon na pas exportes. On peut dcider de changer ces fonctions compltement ou de les effacer dans une nouvelle version (on pourrait supprimer rectangleArea et utiliser * la place) et personne ne sen souciera parce quon ne les avait pas exportes.

Pour utiliser notre module, on fait juste : import Geometry

Geometry.hs doit tout de mme tre dans le mme dossier que le programme qui souhaite limporter. Les modules peuvent aussi tre organiss hirarchiquement. Chaque module peut avoir un nombre de sous-modules et eux-mmes peuvent avoir leurs sousmodules. Dcoupons ces fonctions de manire ce que Geometry soit un module avec trois sous-modules, un pour chaque type dobjet. Dabord, crons un dossier Geometry . Attention la majuscule G. Dans ce dossier, placez trois dossiers : Sphere.hs , Cuboid.hs et Cube.hs (NDT : Cuboid signifie pav droit). Voici ce que les fichiers contiennent : Sphere.hs module Geometry.Sphere ( volume , area ) where volume :: Float -> Float volume radius = (4.0 / 3.0) * pi * (radius ^ 3) area :: Float -> Float area radius = 4 * pi * (radius ^ 2)

Cuboid.hs module Geometry.Cuboid ( volume , area ) where volume :: Float -> Float -> Float -> Float volume a b c = rectangleArea a b * c area :: Float -> Float -> Float -> Float area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2 rectangleArea :: Float -> Float -> Float rectangleArea a b = a * b

Cube.hs module Geometry.Cube ( volume , area ) where import qualified Geometry.Cuboid as Cuboid volume :: Float -> Float volume side = Cuboid.volume side side side area :: Float -> Float area side = Cuboid.area side side side

Parfait ! Tout dabord, nous avons Geometry.Sphere . Remarquez comme on la plac dans le dossier Geometry puis nomm Geometry.Sphere . Idem pour le pav. Remarquez aussi comme dans chaque sous-module, nous avons dfini des fonctions avec le mme nom. On peut le faire car les modules sont spars. On veut utiliser des fonctions de Geometry.Cuboid dans Geometry.Cube , mais on ne peut pas simplement import Geometry.Cuboid parce que ce module exporte des fonctions ayant le mme nom que celles de Geometry.Cube . Cest pourquoi limport est qualifi, et tout va bien. Donc maintenant, si lon se trouve dans un fichier qui se trouve au mme niveau que le dossier Geometry , on peut par exemple : import Geometry.Sphere

Et maintenant, on peut utiliser area et volume , qui nous donneront laire et le volume dune sphre. Et si lon souhaite jongler avec deux ou plus de ces modules, on doit utiliser des imports qualifis car ils exportent des fonctions avec des noms identiques. Tout simplement : import qualified Geometry.Sphere as Sphere

import qualified Geometry.Cuboid as Cuboid import qualified Geometry.Cube as Cube

Et maintenant, on peut appeler Sphere.area , Sphere.volume , Cuboid.area , etc. et chacune calculera laire ou le volume de lobjet correspondant. La prochaine fois que vous vous retrouvez en train dcrire un fichier trs gros avec plein de fonctions, essayez de voir lesquelles partagent un but commun et si vous pouvez les regrouper dans un module. Vous naurez plus qu importer ce module si vous souhaitez rutiliser ces fonctionnalits dans un autre programme.

Fonctions dordre suprieur

Table des matires

Crer nos propres types et classes de types

Crer nos propres types et classes de types


Modules Table des matires Entres et sorties Dans les chapitres prcdents, nous avons vu quelques types et classes de types qui existent en Haskell. Dans ce chapitre, nous verrons comment crer les ntres et les mettre en pratique !

Introduction aux types de donnes algbriques


Jusquici, nous avons crois beaucoup de types de donnes. Bool , Int , Char , Maybe , etc. Mais comment crer les ntres ? Eh bien, un des moyens consiste utiliser le mot-cl data pour dfinir un type. Voyons comment le type Bool est dfini dans la bibliothque standard. data Bool = False | True

data signifie quon cre un nouveau type de donnes. La partie avant le = dnote le type, ici Bool . Les choses aprs le = sont des constructeurs de valeurs. Ils spcifient les diffrentes valeurs que peut prendre ce type. Le | se lit comme un ou. On peut donc lire cette dclaration : le type Bool peut avoir pour valeur True ou False . Le nom du type tout comme les noms des constructeurs de valeurs doivent commencer par une majuscule. De manire similaire, on peut imaginer Int dfini ainsi : data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647

Le premier et le dernier constructeurs sont les valeurs minimales et maximales dun Int . En vrai le type nest pas dfini ainsi, les points de suspension cachent lomission dun paquet norme de nombres, donc ceci est juste titre illustratif. Maintenant, imaginons comment lon reprsenterait une forme en Haskell. Une possibilit serait dutiliser des tuples. Un cercle pourrait tre (43.1, 55.0, 10.4) o les deux premires composantes seraient les coordonnes du centre, et la troisime le rayon. a a lair raisonnable, mais a pourrait tout aussi bien reprsenter un vecteur 3D ou je ne sais quoi. Une meilleure solution consiste crer notre type pour reprsenter les donnes. Disons quune forme puisse tre un cercle ou un rectangle. Voil : data Shape = Circle Float Float Float | Rectangle Float Float Float Float

Quest-ce que cest que a ? Pensez-y ainsi. Le constructeur de valeurs Circle a trois champs, qui sont tous des Float . Ainsi, lorsquon crit un constructeur de valeurs, on peut optionnellement ajouter des types la suite qui dfinissent les valeurs quil peut contenir. Ici, les deux premiers champs sont les coordonnes du centre, le troisime est son rayon. Le constructeur de valeurs Rectangle a quatre champs qui acceptent des Float . Les deux premiers sont les coordonnes du coin suprieur gauche, et les deux autres celles du coin infrieur droit. Quand je dis champ, il sagit en fait de paramtres. Les constructeurs de valeurs sont ainsi des fonctions qui retournent ultimement un type de donnes. Regardons les signatures de type de ces deux constructeurs de valeurs. ghci> :t Circle Circle :: Float -> Float -> Float -> Shape ghci> :t Rectangle Rectangle :: Float -> Float -> Float -> Float -> Shape

Cool, donc les constructeurs de valeurs sont des fonctions comme les autres. Qui let cru ? Crons une fonction qui prend une forme et retourne sa surface. surface :: Shape -> Float surface (Circle _ _ r) = pi * r ^ 2 surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)

La premire chose notable, cest la dclaration de type. Elle dit que la fonction prend un Shape et retourne un Float . On naurait pas pu crire une dclaration de type Circle -> Float parce que Circle nest pas un type, alors que Shape en est un. Tout comme on ne peut pas crire une fonction qui a pour

dclaration de type True -> Int . La prochaine chose remarquable, cest le filtrage par motif sur les constructeurs de valeurs. Nous lavons en fait dj fait (tout le temps vrai dire), lorsque lon filtrait des valeurs contre [] ou False ou 5 , seulement ces valeurs navaient pas de champs. On crit seulement le constructeur, puis on lie ses champs des noms. Puisquon sintresse au rayon, on se fiche des autres champs, qui nindiquent que la position du cercle. ghci> surface $ Circle 10 20 10 314.15927 ghci> surface $ Rectangle 0 0 100 100 10000.0

Yay, a marche ! Mais si on essaie dafficher Circle 10 20 5 dans linvite, on obtient une erreur. Cest parce quHaskell ne sait pas (encore) comment afficher ce type de donnes sous une forme littrale. Rappelez-vous, quand on essaie dafficher une valeur dans linvite, Haskell excute la fonction show sur cette valeur pour obtenir une reprsentation en chane de caractres, puis affiche celle-ci dans le terminal. Pour rendre Shape membre de la classe de types Show , on peut le modifier ainsi : data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)

On ne sintressera pas trop la drivation pour linstant. Disons seulement quen ajoutant deriving (Show) la fin dune dclaration data, Haskell rend magiquement ce type membre de la classe de types Show . Ainsi, on peut prsent faire : ghci> Circle 10 20 5 Circle 10.0 20.0 5.0 ghci> Rectangle 50 230 60 90 Rectangle 50.0 230.0 60.0 90.0

Les constructeurs de valeurs sont des fonctions, on peut donc les mapper, les appliquer partiellement, etc. Si lon veut crer une liste de cercles concentriques de diffrents rayons, on peut faire : ghci> map (Circle 10 20) [4,5,6,6] [Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]

Notre type de donnes est satisfaisant, bien quil pourrait tre amlior. Crons un type de donnes intermdiaire qui dfinit un point dans lespace bidimensionnel. On pourra lutiliser pour rendre nos formes plus comprhensibles. data Point = Point Float Float deriving (Show) data Shape = Circle Point Float | Rectangle Point Point deriving (Show)

Remarquez quen dfinissant un point, on a utilis le mme nom pour le type de donnes et pour le constructeur de valeurs. a na pas de sens particulier, mais il est assez usuel de donner le mme nom lorsquun type na quun seul constructeur de valeurs. prsent, le Circle a deux champs, un de type Point et un autre de type Float . Cela simplifie la comprhension de ce quils reprsentent. Idem pour le rectangle. On doit maintenant ajuster surface pour reflter ces changements. surface :: Shape -> Float surface (Circle _ r) = pi * r ^ 2 surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)

On a seulement chang les motifs. Dans le motif du cercle, on ignore compltement le point. Dans celui du rectangle, on utilise des motifs imbriqus pour rcuprer les champs des points. Si lon voulait rfrencer le point lui-mme pour une raison quelconque, on aurait pu utiliser des motifs nomms. ghci> surface (Rectangle (Point 0 0) (Point 100 100)) 10000.0 ghci> surface (Circle (Point 0 0) 24) 1809.5574

Et pourquoi pas une fonction qui bouge une forme ? Elle prend une forme, une quantit sur laxe x et sur laxe y, et retourne une forme aux mmes dimensions, mais dplace selon ces quantits. nudge :: Shape -> Float -> Float -> Shape nudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b))

Plutt simple. On ajoute simplement les dplacements aux coordonnes correspondantes.

ghci> nudge (Circle (Point 34 34) 10) 5 10 Circle (Point 39.0 44.0) 10.0

Si on ne veut pas manipuler directement les points, on peut crer des fonctions auxiliaires qui crent des formes dune taille donne lorigine, puis les dplacer. baseCircle :: Float -> Shape baseCircle r = Circle (Point 0 0) r

baseRect :: Float -> Float -> Shape baseRect width height = Rectangle (Point 0 0) (Point width height) ghci> nudge (baseRect 40 100) 60 23 Rectangle (Point 60.0 23.0) (Point 100.0 123.0)

Vous pouvez bien sr exporter vos types de donnes de vos modules. Pour ce faire, ajoutez simplement les types que vous souhaitez exporter au mme endroit que les fonctions exporter, puis ouvrez des parenthses et spcifiez les constructeurs de valeurs que vous voulez exporter, spars par des virgules. Si vous voulez exporter tous les constructeurs, vous pouvez crire .. . Si on voulait exporter les fonctions et types dfinis ici dans un module, on pourrait commencer ainsi : module Shapes ( Point(..) , Shape(..) , surface , nudge , baseCircle , baseRect ) where

En faisant Shape(..) , on exporte tous les constructeurs de valeurs de Shape , donc quiconque importe le module peut crer des formes laide de Rectangle et Circle . Cest quivalent Shape (Rectangle, Circle) . On pourrait aussi choisir de ne pas exporter de constructeurs de valeurs pour Shape en crivant simplement Shape dans la dclaration dexport. Ainsi, quelquun qui voudrait crer une forme serait oblig de le faire en passant par les fonctions baseCircle et baseRect . Data.Map utilise cette approche. Vous ne pouvez pas crer de map en faisant Map.Map [(1,2),(3,4)] parce quil nexporte pas ce constructeur de valeurs. Cependant, vous pouvez en crer laide de fonctions auxiliaires comme Map.fromList . Souvenez-vous, les constructeurs de valeurs sont juste des fonctions qui prennent des champs en paramtres et retournent une valeur dun certain type (comme Shape ). Donc quand on choisit de ne pas les exporter, on empche seulement les personnes qui importent notre module dutiliser ces fonctions, mais si dautres fonctions exportes retournent un type, on peut les utiliser pour crer des valeurs de ce type de donnes. Ne pas exporter les constructeurs de valeurs dun type de donnes le rend plus abstrait en cachant son implmentation. galement, quiconque utilise ce module ne peut pas filtrer par motif sur les constructeurs de valeurs.

Syntaxe des enregistrements


OK, on vient de nous donner la tche de crer un type de donnes pour dcrire une personne. Les informations stocker pour dcrire une personne sont : prnom, nom de famille, ge, poids, numro de tlphone, et parfum de glace prfr. Je ne sais pas pour vous, mais cest tout ce que je souhaite savoir propos dune personne. Allons-y ! data Person = Person String String Int Float String String deriving (Show)

Ok. Le premier champ est le prnom, le deuxime le nom, etc. Crons une personne. ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate" ghci> guy Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"

Plutt cool, bien quun peu illisible. Comment faire une fonction pour rcuprer juste une information sur la personne ? Une pour son prnom, une pour son nom, etc. Nous devrions les dfinir ainsi : firstName :: Person -> String firstName (Person firstname _ _ _ _ _) = firstname lastName :: Person -> String lastName (Person _ lastname _ _ _ _) = lastname age :: Person -> Int age (Person _ _ age _ _ _) = age

height :: Person -> Float height (Person _ _ _ height _ _) = height phoneNumber :: Person -> String phoneNumber (Person _ _ _ _ number _) = number flavor :: Person -> String flavor (Person _ _ _ _ _ flavor) = flavor

Wow ! Personnellement, je nai pas trop apprci crire a ! En dpit dtre trs encombrante et ENNUYEUSE crire, cette mthode fonctionne. ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate" ghci> firstName guy "Buddy" ghci> height guy 184.2 ghci> flavor guy "Chocolate"

Il doit y avoir un meilleur moyen, vous vous dites ! Eh bien non, il ny en a pas, dsol. Non, je blague, il y en a un. Hahaha ! Les crateurs dHaskell taient trs intelligents et ont anticip ce scnario. Ils ont inclus une version alternative de lcriture des types de donnes. Voici comment lon pourrait obtenir les mmes fonctionnalits avec une syntaxe denregistrements. data Person = Person { , , , , , } firstName :: String lastName :: String age :: Int height :: Float phoneNumber :: String flavor :: String deriving (Show)

Donc, plutt que de seulement nommer les types des champs les uns aprs les autres et de les sparer par des espaces, on peut utiliser des accolades. On crit dabord le nom du champ, par exemple firstName , puis un :: (aussi appel Paamayim Nekudotayim, haha) puis on spcifie son type. Le type de donnes rsultant est exactement le mme. Le principal avantage est que cela cre les fonctions pour examiner chaque champ du type de donnes. En utilisant la syntaxe des enregistrements, Haskell a automatiquement cr ces fonctions : firstName , lastName , age , height , phoneNumber et flavor . ghci> :t flavor flavor :: Person -> String ghci> :t firstName firstName :: Person -> String

Il y a galement un autre avantage utiliser la syntaxe des enregistrements. Lorsquon drive Show pour le type, il est affich diffremment lorsquon utilise la syntaxe des enregistrements pour dfinir et instancier le type. Mettons quon a un type qui reprsente des voitures. On souhaite garder une trace de la compagnie qui la cre, le nom du modle et lanne de production. Regardez. data Car = Car String String Int deriving (Show)

ghci> Car "Ford" "Mustang" 1967 Car "Ford" "Mustang" 1967

Si on le dfinit laide de la syntaxe des enregistrements, on peut crer une nouvelle voiture comme suit. data Car = Car {company :: String, model :: String, year :: Int} deriving (Show)

ghci> Car {company="Ford", model="Mustang", year=1967} Car {company = "Ford", model = "Mustang", year = 1967}

la cration de la voiture, on na pas ncessairement mettre les champs dans le bon ordre, du moment quon les liste tous. Alors que sans la syntaxe des enregistrements, lordre importe. Utilisez cette syntaxe lorsquun constructeur a plusieurs champs et quil nest pas vident de prdire lequel correspond quoi. Si lon cre un type de donnes vecteur 3D en faisant data Vector = Vector Int Int Int , il est plutt vident que les champs sont les trois composantes du vecteur. Par contre, pour Person ou Car , ctait plus compliqu et on a gagn utiliser la syntaxe des enregistrements.

Paramtres de types
Un constructeur de valeurs peut prendre plusieurs valeurs comme paramtres et retourner une nouvelle valeur. Par exemple, le constructeur Car prend trois valeurs et produit une valeur de type Car . De manire similaire, les constructeurs de types peuvent prendre des types en paramtres pour crer de nouveaux types. a peut sembler un peu trop mta au premier abord, mais ce nest pas si compliqu. Si vous tes familier avec les templates C++, vous verrez des parallles. Pour se faire une bonne image de lutilisation des paramtres de types en action, regardons comment un des types que nous avons dj rencontr est implment. data Maybe a = Nothing | Just a

Le a est un paramtre de type. Et puisquil y a un paramtre de type impliqu, on dit que Maybe est un constructeur de type. En fonction de ce que lon veut que ce type de donnes contienne lorsquil ne vaut pas Nothing , ce constructeur de types peut ventuellement construire un type Maybe Int , Maybe Car , Maybe String , etc. Aucune valeur ne peut avoir pour type Maybe , car ce nest pas un type per se, mais un constructeur de types. Pour pouvoir tre un type rel qui peut avoir une valeur, il doit avoir tous ses paramtres remplis. Donc, si lon passe Char en paramtre de type Maybe , on obtient un type Maybe Char . La valeur Just 'a' a pour type Maybe Char par exemple. Vous ne le savez peut-tre pas, mais on a utilis un type qui a un paramtre de type avant mme dutiliser Maybe . Ce type est le type des listes. Bien quil y ait un peu de sucre syntaxique en jeu, le type liste prend un paramtre et produit un type concret. Des valeurs peuvent avoir pour type [Int] , [Char] , [[String]] , mais aucune valeur ne peut avoir pour type [] . Jouons un peu avec le type Maybe . ghci> Just "Haha" Just "Haha" ghci> Just 84 Just 84 ghci> :t Just "Haha" Just "Haha" :: Maybe [Char] ghci> :t Just 84 Just 84 :: (Num t) => Maybe t ghci> :t Nothing Nothing :: Maybe a ghci> Just 10 :: Maybe Double Just 10.0

Les paramtres de types sont utiles parce quon peut crer diffrents types en fonction de la sorte de types quon souhaite que notre type de donnes contienne. Quand on fait :t Just "Haha" , le moteur dinfrence de types se rend compte que le type doit tre Maybe [Char] , parce que le a dans le Just a est une chane de caractres, donc le a de Maybe a doit aussi tre une chane de caractres. Remarquez que le type de Nothing est Maybe a . Son type est polymorphique. Si une fonction ncessite un Maybe Int en paramtre, on peut lui donner Nothing , parce que Nothing ne contient pas de valeur de toute faon, donc peu importe. Le type Maybe a peut se comporter comme Maybe Int sil le faut, tout comme 5 peut se comporter comme un Int ou un Double . De faon similaire, le type de la liste vide est [a] . Une liste vide peut tre une liste de quoi que ce soit. Cest pourquoi on peut faire [1, 2, 3] ++ [] et ["ha", "ha", "ha"] ++ [] . Utiliser les paramtres de types est trs bnfique, mais seulement quand les utiliser a un sens. Gnralement, on les utilise quand notre type de donnes fonctionne sans se soucier du type de ce quil contient en lui, comme pour notre type Maybe a . Si notre type se comporte comme une sorte de bote, il est bien de les utiliser. On pourrait changer notre type de donnes Car de ceci : data Car = Car { , , } company :: String model :: String year :: Int deriving (Show)

en cela : data Car a b c = Car { , , } company :: a model :: b year :: c deriving (Show)

Mais y gagnerait-on vraiment ? La rponse est : probablement pas, parce quon finirait par dfinir des fonctions qui ne fonctionnent que sur le type

Car String String Int . Par exemple, vu notre premire dfinition de Car , on pourrait crire une fonction qui affiche les proprits de la voiture avec un joli petit texte. tellCar :: Car -> String tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y

ghci> let stang = Car {company="Ford", model="Mustang", year=1967} ghci> tellCar stang "This Ford Mustang was made in 1967"

Quelle jolie petite fonction ! La dclaration de type est mignonne et fonctionne bien. Maintenant, si Car tait Car a b c ? tellCar :: (Show a) => Car String String a -> String tellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y

Nous devrions forcer cette fonction prendre un type Car tel que (Show a) => Car String String a . Vous pouvez constater que la signature de type est plus complique, et le seul avantage quon en tire serait quon pourrait utiliser nimporte quel type instance de la classe de types Show pour c . ghci> tellCar (Car "Ford" "Mustang" 1967) "This Ford Mustang was made in 1967" ghci> tellCar (Car "Ford" "Mustang" "nineteen sixty seven") "This Ford Mustang was made in \"nineteen sixty seven\"" ghci> :t Car "Ford" "Mustang" 1967 Car "Ford" "Mustang" 1967 :: (Num t) => Car [Char] [Char] t ghci> :t Car "Ford" "Mustang" "nineteen sixty seven" Car "Ford" "Mustang" "nineteen sixty seven" :: Car [Char] [Char] [Char]

Dans la vie relle cependant, on finirait par utiliser Car String String Int la plupart du temps, et il semblerait que paramtrer le type Car ne vaudrait pas le coup. On utilise gnralement les paramtres de types lorsque le type contenu dans les divers constructeurs de valeurs du type de donnes nest pas vraiment important pour que le type fonctionne. Une liste de choses est une liste de choses, peu importe ce que les choses sont, a marche. Si on souhaite sommer une liste de nombres, on peut spcifier au dernier moment que la fonction de sommage attend une liste de nombres. De mme pour Maybe . Maybe reprsente une option qui est soit de navoir rien, soit davoir quelque chose. Peu importe ce que le type de cette chose est. Un autre exemple de type paramtr que nous avons dj rencontr est Map k v de Data.Map . Le k est le type des cls, le v le type des valeurs. Cest un bon exemple dendroit o les types paramtrs sont trs utiles. Avoir des maps paramtres nous permet de crer des maps de nimporte quel type vers nimporte quel autre type, du moment que le type de la cl soit membre de la classe de types Ord . Si nous souhaitions dfinir un type de map, on pourrait ajouter la contrainte de classe dans la dclaration data : data (Ord k) => Map k v = ...

Cependant, il existe une trs forte convention en Haskell qui est de ne jamais ajouter de contraintes de classe une dclaration de donnes. Pourquoi ? Eh bien, parce que le bnfice est minimal, mais on se retrouve crire plus de contraintes de classes, mme lorsquelles ne sont pas ncessaires. Que lon mette ou non, la contrainte Ord k dans la dclaration data de Map k v , on aura crire la contrainte dans les fonctions qui supposent un ordre sur les cls de toute faon. Mais, si lon ne met pas la contrainte dans la dclaration data, alors on naura pas mettre (Ord k) => dans les dclarations de types des fonctions qui nont pas besoin de cette contrainte pour fonctionner. Un exemple dune telle fonction est toList , qui prend un mapping et le convertit en liste associative. Sa signature de type est toList :: Map k a -> [(k, a)] . Si Map k v avait une contrainte de classe dans sa dclaration data, le type de ToList devrait tre toList :: (Ord k) => Map k a -> [(k, a)] , alors que cette fonction ne fait aucune comparaison de cls selon leur ordre. Conclusion : ne mettez pas de contraintes de classe dans les dclarations data mme lorsquelles ont lair senses, parce que de toute manire, vous devrez les crire dans les fonctions qui en dpendent. Implmentons un type de vecteur 3D et ajoutons-y quelques oprations. Nous utiliserons un type paramtr afin quil supporte plusieurs types numriques, bien quen gnral on nen utilise quun seul. data Vector a = Vector a a a deriving (Show) vplus :: (Num t) => Vector t -> Vector t -> Vector t (Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n) vectMult :: (Num t) => Vector t -> t -> Vector t (Vector i j k) `vectMult` m = Vector (i*m) (j*m) (k*m) scalarMult :: (Num t) => Vector t -> Vector t -> t (Vector i j k) `scalarMult` (Vector l m n) = i*l + j*m + k*n

vplus somme deux vecteurs. Deux vecteurs sont somms en sommant leurs composantes deux deux. scalarMult est le produit scalaire de deux vecteurs et vectMult permet de multiplier un vecteur par un scalaire. Ces fonctions peuvent oprer sur des types comme Vector Int , Vector Integer , Vector Float , condition que le a de Vector a soit de la classe Num . galement, si vous examinez les dclarations de type des fonctions, vous verrez quelles noprent que sur des vecteurs de mme type et que les scalaires doivent galement tre du type contenu dans les vecteurs. Remarquez quon na pas mis de contrainte Num dans la dclaration data, puisquon a eu lcrire dans chaque fonction qui en dpendait de toute faon. Une fois de plus, il est trs important de distinguer le constructeur de types du constructeur de valeurs. Lorsquon dclare un type, le nom gauche du = est le constructeur de types, et les constructeurs situs aprs (spars par des | ) sont des constructeurs de valeurs. Donner une fonction le type Vector t t t -> Vector t t t -> t serait faux, parce que lon doit donner des types dans les dclarations de types, le constructeur de types vecteurs ne prend quun paramtre, alors que le constructeur de valeurs en prend trois. Jouons avec nos vecteurs. ghci> Vector 3 5 8 Vector 12 7 16 ghci> Vector 3 5 8 Vector 12 9 19 ghci> Vector 3 9 7 Vector 30 90 70 ghci> Vector 4 9 5 74.0 ghci> Vector 2 9 3 Vector 148 666 222 `vplus` Vector 9 2 8 `vplus` Vector 9 2 8 `vplus` Vector 0 2 3 `vectMult` 10 `scalarMult` Vector 9.0 2.0 4.0 `vectMult` (Vector 4 9 5 `scalarMult` Vector 9 2 4)

Instances drives
Dans la section Classes de types 101, nous avons vu les bases des classes de types. Nous avons dit quune classe de types est une sorte dinterface qui dfinit un comportement. Un type peut devenir une instance dune classe de types sil supporte ce comportement. Par exemple : le type Int est une instance d Eq parce que cette classe dfinit le comportement de ce qui peut tre test pour lgalit. Et puisquon peut tester lgalit de deux entiers, Int est membre de la classe Eq . La vraie utilit vient des fonctions qui agissent comme linterface d Eq , savoir == et /= . Si un type est membre d Eq , on peut utiliser les fonctions == et /= avec des valeurs de ce type. Cest pourquoi des expressions comme 4 == 4 et "foo" /= "bar" sont correctement types. Nous avons aussi mentionn quelles sont souvent confondues avec les classes de langages comme Java, Python, C++ et autres, ce qui sme la confusion dans lesprit de beaucoup de gens. Dans ces langages, les classes sont des patrons partir desquels sont crs des objets qui contiennent un tat et peuvent effectuer des actions. Les classes de types sont plutt comme des interfaces. On ne cre pas de donnes partir de classes de types. Plutt, on cre dabord notre type de donnes, puis on se demande pour quoi il peut se faire passer. Sil peut tre compar pour lgalit, on le rend instance de la classe Eq . Sil peut tre ordonn, on le rend instance de la classe Ord . Dans la prochaine section, nous verrons comment crer manuellement nos instances dune classe de types en implmentant les fonctions dfinies par cette classe. Pour linstant, voyons comment Haskell peut magiquement faire de nos types des instances de nimporte laquelle des classes de types suivantes : Eq , Ord , Enum , Bounded , Show et Read . Haskell peut driver le comportement de nos types dans ces contextes si lon utilise le mot-cl deriving lors de la cration du type de donnes. Considrez ce type de donnes : data Person = Person { firstName :: String , lastName :: String , age :: Int }

Il dcrit une personne. Posons comme hypothse que deux personnes nont jamais les mmes nom, prnom et ge. Maintenant, si lon a un enregistrement pour deux personnes, est-ce que cela a un sens de vrifier sil sagit de la mme personne ? Bien sr. On peut essayer de voir si les enregistrements sont les mmes ou non. Cest pourquoi il serait sens que ce type soit membre de la classe Eq . Nous allons driver cette instance. data Person = Person { , , } firstName :: String lastName :: String age :: Int deriving (Eq)

Lorsquon drive linstance Eq pour un type, puis quon essaie de comparer deux valeurs de ce type avec == ou /= , Haskell va regarder si les deux constructeurs sont gaux (ici il ny en a quun possible cependant), puis va tester si les donnes contenues dans les valeurs sont gales deux deux en testant chaque paire de champ avec == . Ainsi, il y a un dtail important, qui est que les types des champs doivent aussi tres membres de la classe Eq . Puisque String et Int le sont, tout va bien. Testons notre instance d Eq .

ghci> ghci> ghci> ghci> False ghci> False ghci> True ghci> True

let let let mca

mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43} adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41} mca = Person {firstName = "Adam", lastName = "Yauch", age = 44} == adRock

mikeD == adRock mikeD == mikeD mikeD == Person {firstName = "Michael", lastName = "Diamond", age = 43}

Bien sr, puisque Person est maintenant dans Eq , on peut utiliser comme a pour toute fonction ayant une contrainte de classe Eq a dans sa signature, comme elem . ghci> let beastieBoys = [mca, adRock, mikeD] ghci> mikeD `elem` beastieBoys True

Les classes de types Show et Read sont pour les choses qui peuvent tre converties respectivement vers et depuis une chane de caractres. Comme avec Eq , si un constructeur de types a des champs, leur type doit aussi tre membre de Show ou Read si on veut faire de notre type une instance de lune de ces classes. Faisons de notre type de donnes Person un membre de Show et de Read . data Person = Person { , , } firstName :: String lastName :: String age :: Int deriving (Eq, Show, Read)

Maintenant, on peut afficher une personne dans le terminal. ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43} ghci> mikeD Person {firstName = "Michael", lastName = "Diamond", age = 43} ghci> "mikeD is: " ++ show mikeD "mikeD is: Person {firstName = \"Michael\", lastName = \"Diamond\", age = 43}"

Si nous avions voulu afficher une personne dans le terminal avant de rendre Person membre de Show , Haskell se serait plaint du fait quil ne sache pas reprsenter une personne comme une chane de caractres. Mais maintenant quon a driv Show , il sait comment faire. Read est en gros linverse de Show . Show convertit des valeurs de notre type en des chanes de caractres, Read convertit des chane de caractres en valeurs de notre type. Souvenez-vous cependant, lorsquon avait utilis la fonction read , on avait d annoter explicitement le type quon dsirait. Sans cela, Haskell ne sait pas vers quel type on veut convertir. ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" :: Person Person {firstName = "Michael", lastName = "Diamond", age = 43}

Si on utilise le rsultat de read dans un calcul plus labor, Haskell peut infrer le type quon attend, et lon na alors pas besoin dannoter le type. ghci> read "Person {firstName =\"Michael\", lastName =\"Diamond\", age = 43}" == mikeD True

On peut aussi lire des types paramtrs, et il faut alors annoter le type avec les paramtres complts. Ainsi, on ne peut pas faire read "Just 't'" :: Maybe a , mais on peut faire read "Just 't'" :: Maybe Char . On peut driver des instances la classe de types Ord , qui est pour les types dont les valeurs peuvent tre ordonnes. Si lon compare deux valeurs du mme type ayant t construites par deux constructeurs diffrents, la valeur construite par le constructeur dfini en premier sera considre plus petite. Par exemple, considrez le type Bool , qui peut avoir pour valeur False ou True . Pour comprendre comment il fonctionne lorsquil est compar, on peut limaginer dfini comme : data Bool = False | True deriving (Ord)

Puisque le constructeur de valeurs False est spcifi avant le constructeur de valeurs True , on peut considrer que True est plus grand que False . ghci> True `compare` False GT

ghci> True > False True ghci> True < False False

Dans le type de donnes Maybe a , le constructeur de valeurs Nothing est spcifi avant le constructeur de valeurs Just , donc une valeur Nothing est toujours plus petite quune valeur Just something , mme si ce something est moins un milliard de milliards. Mais si lon compare deux valeurs Just , alors Haskell compare ce quelles contiennent. ghci> True ghci> False ghci> GT ghci> True Nothing < Just 100 Nothing > Just (-49999) Just 3 `compare` Just 2 Just 100 > Just 50

Mais on ne peut pas faire Just (*3) > Just (*2) , parce que (*3) et (*2) sont des fonctions, qui ne sont pas des instances d Ord . On peut facilement utiliser des types de donnes algbriques pour crer des numrations, et les classes de types Enum et Bounded nous aident dans la tche. Considrez les types de donnes suivants : data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday

Puisque tous les constructeurs de valeurs sont nullaires (ils ne prennent pas de paramtres, ou champs), on peut rendre le type membre de la classe Enum . La classe de types Enum est pour les choses qui ont des prdcesseurs et des successeurs. On peut aussi le rendre membre de la classe Bounded , qui est pour les choses avec une plus petite valeur et une plus grande valeur. Tant quon y est, faisons-en aussi une instance des autres classes quon a vues. data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum)

Puisque le type est membre des classes Show et Read , on peut le convertir vers et depuis des chanes de caractres. ghci> Wednesday Wednesday ghci> show Wednesday "Wednesday" ghci> read "Saturday" :: Day Saturday

Puisquil est membre des classes Eq et Ord , on peut comparer et tester lgalit de deux jours. ghci> False ghci> True ghci> True ghci> LT Saturday == Sunday Saturday == Saturday Saturday > Friday Monday `compare` Wednesday

Il est aussi membre de Bounded , donc on peut demander le plus petit jour et le plus grand jour. ghci> minBound :: Day Monday ghci> maxBound :: Day Sunday

Cest aussi une instance d Enum . On peut obtenir le prdcesseur et le successeur dun jour, et crer une progression de jours ! ghci> succ Monday Tuesday ghci> pred Saturday Friday ghci> [Thursday .. Sunday] [Thursday,Friday,Saturday,Sunday]

ghci> [minBound .. maxBound] :: [Day] [Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday]

Cest plutt gnial.

Synonymes de types
Prcdemment, on a mentionn quen crivant des types, les types [Char] et String taient quivalents et interchangeables. Ceci est implment laide des synonymes de types. Les synonymes de types ne font rien per se, il sagit simplement de donner des noms diffrents au mme type afin que la lecture du code ou de la documentation ait plus de sens pour le lecteur. Voici comment la bibliothque standard dfinit String comme un synonyme de [Char] . type String = [Char]

On a introduit le mot-cl type. Le mot-cl peut tre droutant pour certains, puisquon ne cre en fait rien de nouveau (on faisait cela avec le mot-cl data), mais on cre seulement un synonyme pour un type dj existant. Si lon cre une fonction qui convertit une chane de caractres en majuscules et quon lappelle toUpperString , on peut lui donner comme dclaration de type toUpperString :: [Char] -> [Char] ou toUpperString :: String -> String . Ces deux dclarations sont en effet identiques, mais la dernire est plus agrable lire. Quand on utilisait le module Data.Map , on a commenc par reprsenter un carnet tlphonique comme une liste associative avant de le reprsenter comme une map. Comme nous lavions alors vu, une liste associative est une liste de paires cl-valeur. Regardons le carnet que nous avions alors. phoneBook :: [(String,String)] phoneBook = [("betty","555-2938") ,("bonnie","452-2928") ,("patsy","493-2928") ,("lucille","205-2928") ,("wendy","939-8282") ,("penny","853-2492") ]

On voit que le type de phoneBook est [(String, String)] . Cela nous indique que cest une liste associative qui mappe des chanes de caractres vers des chanes de caractres, mais pas grand chose de plus. Crons un synonyme de types pour communiquer plus dinformations dans la dclaration de type. type PhoneBook = [(String,String)]

prsent, la dclaration de notre carnet peut tre phoneBook :: PhoneBook . Crons aussi un synonyme pour String . type PhoneNumber = String type Name = String type PhoneBook = [(Name,PhoneNumber)]

Donner des synonymes de types au type String est pratique courante chez les programmeurs en Haskell lorsquils souhaitent indiquer plus dinformation propos des chanes de caractres quils utilisent dans leur programme et ce quelles reprsentent. prsent, lorsquon implmente une fonction qui prend un nom et un nombre, et cherche si cette combinaison de nom et de numro est dans notre carnet tlphonique, on peut lui donner une description de type trs jolie et descriptive. inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool inPhoneBook name pnumber pbook = (name,pnumber) `elem` pbook

Si nous navions pas utilis de synonymes de types, notre fonction aurait pour type String -> String -> [(String,String)] -> Bool . Ici, la dclaration tirant parti des synonymes de types est plus facile lire. Cependant, nen faites pas trop. On introduit les synonymes de types pour soit dcrire ce que des types existants reprsentent dans nos fonctions (et ainsi les dclarations de types de nos fonctions deviennent de meilleures documentations), soit lorsque quelque chose a un type assez long et rpt souvent (comme [(String, String)] ) et reprsente quelque chose de spcifique dans le contexte de nos fonctions. Les synonymes de types peuvent aussi tre paramtrs. Si on veut un type qui reprsente le type des listes associatives de faon assez gnrale pour tre utilis quelque que soit le type des cls et des valeurs, on peut faire : type AssocList k v = [(k,v)]

Maintenant, une fonction qui rcupre la valeur associe une cl dans une liste associative peut avoir pour type (Eq k) => k -> AssocList k v -> Maybe v . AssocList est un constructeur de types qui prend deux types et produit un type concret, comme AssocList Int String par exemple.

Fonzie dit : Hey ! Quand je parle de types concrets, je veux dire genre des types appliqus compltement comme Map Int String , ou si on se frotte une de ces fonctions polymorphes, [a] ou (Ord a) => Maybe a et tout a. Et tu vois, parfois moi et mes potes on dit que Maybe cest un type, mais bon, cest pas ce quon veut dire, parce que mme les idiots savent que Maybe est un constructeur de types, tas vu. Quand japplique Maybe un type, comme dans Maybe String , alors jai un type concret. Tu sais, les valeurs, elles ne peuvent avoir que des types concrets ! En gros, vis vite, aime fort, et ne prte ton peigne personne !

Tout comme lon peut appliquer partiellement des fonctions pour obtenir de nouvelles fonctions, on peut appliquer partiellement des constructeurs de types pour obtenir de nouveaux constructeurs de types. Tout comme on appelle une fonctions avec trop peu de paramtres, on peut spcifier trop peu de paramtres de types un constructeur de types et obtenir un constructeur de types partiellement appliqu. Si lon voulait un type qui reprsente une map (de Data.Map ) qui va des entiers vers quelque chose, on pourrait faire soit : type IntMap v = Map Int v

Ou bien : type IntMap = Map Int

Dune manire ou de lautre, le constructeur de types IntMap prend un seul paramtre qui est le type de ce vers quoi les entiers doivent pointer.

Ah oui. Si vous comptez essayer dimplmenter ceci, vous allez probablement importer Data.Map qualifi. Quand vous faites un import qualifi, les constructeurs de type doivent aussi tre prcds du nom du module. Donc vous cririez type IntMap = Map.Map Int .

Soyez certains davoir bien saisi la distinction entre constructeurs de types et constructeurs de valeurs. Juste parce quon a cr un synonyme de types IntMap ou AssocList ne signifie pas que lon peut faire quelque chose comme AssocList [(1, 2), (4, 5), (7, 9)] . Tout ce que cela signifie, cest quon peut parler de ce type en utilisant des noms diffrents. On peut faire [(1, 2), (3, 5), (8, 9)] :: AssocList Int Int , ce qui va faire que les nombres lintrieur auront pour type Int , mais on peut continuer utiliser cette liste comme une liste normale qui contient des paires dentiers. Les synonymes de types (et plus gnralement les types) ne peuvent tre utiliss que dans la partie types dHaskell. On se situe dans cette partie lorsquon dfinit de nouveaux types (donc, dans une dclaration data ou type), ou lorsquon se situe aprs un :: . Le :: peut tre dans des dclarations de types ou pour des annotations de types. Un autre type de donnes cool qui prend deux types en paramtres est Either a b . Voici grossirement sa dfinition : data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

Il a deux constructeurs de valeurs. Si le constructeur Left est utilis, alors le contenu a pour type a , si Right est utilis, le contenu a pour type b . Ainsi, on peut utiliser ce type pour encapsuler une valeur qui peut avoir un type ou un autre, et lorsquon rcupre une valeur qui a pour type Either a b , on filtre gnralement par motif sur Left et Right pour obtenir diffrentes choses en fonction. ghci> Right 20 Right 20 ghci> Left "w00t" Left "w00t" ghci> :t Right 'a' Right 'a' :: Either a Char ghci> :t Left True Left True :: Either Bool b

Jusquici, nous avons vu que Maybe a tait principalement utilis pour reprsenter des rsultats de calculs qui pouvaient avoir soit chou, soit russi. Mais parfois, Maybe a nest pas assez bien, parce que Nothing nindique pas dinformation autre que le fait que quelque chose a chou. Cest bien pour les fonctions qui peuvent chouer seulement dune faon, ou lorsquon se fiche de savoir comment elles ont chou. Une recherche de cl dans Data.Map ne peut chouer que lorsquune cl ntait pas prsente, donc on sait ce qui sest pass. Cependant, lorsquon sintresse comment une fonction a chou ou pourquoi, on utilise gnralement le type Either a b , o a est un type qui peut dune certaine faon indiquer les raisons dun ventuel chec, et b est le type du rsultat dun calcul russi. Ainsi, les erreurs utilisent le constructeur de valeurs Left , alors que les rsultats utilisent le constructeur de valeurs Right .

Exemple : un lyce a des casiers dans lesquels les tudiants peuvent ranger leurs posters de GunsnRoses. Chaque casier a une combinaison. Quand un tudiant veut un nouveau casier, il indique au superviseur des casiers le numro de casier quil voudrait, et celui-ci lui donne le code. Cependant, si quelquun utilise dj ce casier, il ne peut pas lui donner le casier, et ltudiant doit en choisir un autre. On va utiliser une map de Data.Map pour reprsenter les casiers. Elle associera chaque numro de casier une paire indiquant si le casier est utilis ou non, et le code du casier. import qualified Data.Map as Map data LockerState = Taken | Free deriving (Show, Eq) type Code = String type LockerMap = Map.Map Int (LockerState, Code)

Simple. On introduit un nouveau type de donnes pour reprsenter un casier occup ou libre, et on cre un synonyme de types pour le code du casier. On cre aussi un synonyme de types pour les maps qui prennent un entier et renvoient une paire dtat et de code de casier. prsent, on va crer une fonction qui cherche le code dans une map de casiers. On va utiliser un type Either String Code pour reprsenter le rsultat, parce que la recherche peut chouer de deux faons - le casier peut tre dj pris, auquel cas on ne peut pas dvoiler le code, ou bien le numro de casier peut tout simplement ne pas exister. Si la recherche choue, on va utiliser une String pour dcrire ce qui sest pass. lockerLookup :: Int -> LockerMap -> Either String Code lockerLookup lockerNumber map = case Map.lookup lockerNumber map of Nothing -> Left $ "Locker number " ++ show lockerNumber ++ " doesn't exist!" Just (state, code) -> if state /= Taken then Right code else Left $ "Locker " ++ show lockerNumber ++ " is already taken!"

On fait une recherche normale dans la map. Si lon obtient Nothing , on retourne une valeur de type Left String , qui dit que le casier nexiste pas. Si on trouve le casier, on effectue une vrification supplmentaire pour voir sil est utilis. Si cest le cas, on retourne Left en indiquant quil est dj pris. Autrement, on retourne une valeur de type Right Code , dans laquelle on donne le code dudit casier. En fait, cest un Right String , mais on a introduit un synonyme de types pour introduire un peu de documentation dans la dclaration de types. Voici une map titre dexemple : lockers :: LockerMap lockers = Map.fromList [(100,(Taken,"ZD39I")) ,(101,(Free,"JAH3I")) ,(103,(Free,"IQSA9")) ,(105,(Free,"QOTSA")) ,(109,(Taken,"893JJ")) ,(110,(Taken,"99292")) ]

Maintenant, cherchons le code de quelques casiers. ghci> lockerLookup 101 lockers Right "JAH3I" ghci> lockerLookup 100 lockers Left "Locker 100 is already taken!" ghci> lockerLookup 102 lockers Left "Locker number 102 doesn't exist!" ghci> lockerLookup 110 lockers Left "Locker 110 is already taken!" ghci> lockerLookup 105 lockers Right "QOTSA"

On aurait pu utiliser un Maybe a pour reprsenter le rsultat, mais alors on ne saurait pas pour quelle raison on na pas pu obtenir le code. Ici, linformation en cas dchec est dans le type de retour.

Structures de donnes rcursives


Comme on la vu, un constructeur de type de donnes algbrique peut avoir plusieurs (ou aucun) champs et chaque champ doit avoir un type concret. Avec cela en tte, on peut crer des types dont les constructeurs ont des champs qui sont de ce mme type ! Ainsi, on peut crer des types de donnes rcursifs, o une valeur dun type peut contenir des valeurs de ce mme type, qui leur tour peuvent contenir encore plus de valeurs de ce type, et ainsi de suite. Pensez cette liste : [5] . Cest juste un sucre syntaxique pour 5:[] . gauche de : , il y a une valeur, et droite, il y a une liste. Dans ce cas, cest une liste vide. Maintenant, quen est-il de [4, 5] ? Eh bien, a se dsucre en 4:(5:[]) . En considrant le

premier : , on voit quil prend aussi un lment gauche et une liste (ici 5:[] ) droite. Il en va de mme pour 3:(4:(5:(6:[]))) , qui peut aussi tre crit 3:4:5:6:[] (parce que : est associatif droite) ou [3, 4, 5, 6] . On peut dire quune liste peut tre la liste vide ou compose dun lment adjoint via : une autre liste (qui peut tre vide ou non). Utilisons un type de donnes algbrique pour implmenter nos propres listes dans ce cas ! data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)

On peut lire a comme la dfinition des listes donne dans un paragraphe ci-dessus. Une liste est soit vide, soit une combinaison dune tte qui a une valeur et dune autre liste. Si cela vous laisse perplexe, peut-tre que vous serez plus laise avec une syntaxe denregistrements. data List a = Empty | Cons { listHead :: a, listTail :: List a} deriving (Show, Read, Eq, Ord)

Vous serez peut-tre aussi surpris par le nom du constructeur Cons ici. cons est un autre nom de : . Vous voyez, pour les listes, : est en ralit un constructeur qui prend une valeur et une autre liste, pour retourner une liste. On peut dores et dj utiliser notre nouveau type de listes ! Il a deux champs. Le premier de type a et le second de type List a . ghci> Empty Empty ghci> 5 `Cons` Cons 5 Empty ghci> 4 `Cons` Cons 4 (Cons 5 ghci> 3 `Cons` Cons 3 (Cons 4

Empty (5 `Cons` Empty) Empty) (4 `Cons` (5 `Cons` Empty)) (Cons 5 Empty))

On a appel notre constructeur Cons de faon infixe pour souligner sa similarit avec : . Empty est similaire [] et 4 `Cons` (5 `Cons` Empty) est similaire 4:(5:[]) . On peut dfinir des fonctions comme automatiquement infixes en ne les nommant quavec des caractres spciaux. On peut aussi faire de mme avec les constructeurs, puisque ce sont des fonctions qui retournent un type de donnes. Regardez a ! infixr 5 :-: data List a = Empty | a :-: (List a) deriving (Show, Read, Eq, Ord)

Tout dabord, on remarque une nouvelle construction syntaxique, la dclaration de fixit. Lorsquon dfinit des fonctions comme oprateurs, on peut leur donner une fixit (ce nest pas ncessaire). Une fixit indique avec quelle force un oprateur lie, et sil est associatif droite ou gauche. Par exemple, la fixit de * est infixl 7 * , et la fixit de + est infixl 6 . Cela signifie quils sont tous deux associatifs gauche ( 4 * 3 * 2 est quivalent (4 * 3) * 2) ), mais * lie plus fortement que + , car il a une plus grande fixit, et ainsi 5 * 4 + 3 est quivalent (5 * 4) + 3 . part ce dtail, on a juste crit a :-: (List a) la place de Cons a (List a) . Maintenant, on peut crire des listes qui ont notre type de listes de la sorte : ghci> (:-:) ghci> ghci> (:-:) 3 :-: 4 :-: 5 :-: Empty 3 ((:-:) 4 ((:-:) 5 Empty)) let a = 3 :-: 4 :-: 5 :-: Empty 100 :-: a 100 ((:-:) 3 ((:-:) 4 ((:-:) 5 Empty)))

Lorsquon drive Show pour notre type, Haskell va toujours afficher le constructeur comme une fonction prfixe, do le parenthsage autour de loprateur (rappelez-vous, 4 + 3 est juste (+) 4 3 ). Crons une fonction qui somme deux de nos listes ensemble. ++ est dfini ainsi pour des listes normales : infixr 5 ++ (++) :: [a] -> [a] -> [a] [] ++ ys = ys (x:xs) ++ ys = x : (xs ++ ys)

On va juste voler cette dfinition pour nos listes. On nommera la fonction .++ . infixr 5 .++ (.++) :: List a -> List a -> List a Empty .++ ys = ys (x :-: xs) .++ ys = x :-: (xs .++ ys)

Voyons si a a march ghci> ghci> ghci> (:-:) let a = 3 :-: 4 :-: 5 :-: Empty let b = 6 :-: 7 :-: Empty a .++ b 3 ((:-:) 4 ((:-:) 5 ((:-:) 6 ((:-:) 7 Empty))))

Bien. Trs bien. Si lon voulait, on pourrait implmenter toutes les fonctions qui oprent sur des listes pour notre propre type liste. Notez comme on a filtr sur le motif (x :-: xs) . Cela fonctionne car le filtrage par motif nest en fait bas que sur la correspondance des constructeurs. On peut filtrer sur :-: parce que cest un constructeur de notre type de liste, tout comme on pouvait filtrer sur : pour des listes normales car ctait un de leurs constructeurs. Pareil pour [] . Puisque le filtrage par motif marche (uniquement) sur les constructeurs, on peut filtrer sur des choses comme des constructeurs normaux prfixes, ou bien comme 8 ou 'a' , qui sont simplement des constructeurs de types numriques ou de caractres respectivement. prsent, implmentons un arbre binaire de recherche . Si vous ne connaissez pas les arbres binaires de recherche, voici en quoi ils consistent : chaque lment pointe vers deux lments, son fils gauche et son fils droit. Le fils gauche a une valeur infrieure celle de llment, le fils droit a une valeur suprieure. Chacun des fils peut son tour pointer vers zro, un ou deux lments. Chaque lment a ainsi au plus deux sous-arbres. Et la proprit intressante de ces arbres est que tous les nuds du sous-arbre gauche dun nud donn, mettons 5, ont une valeur infrieure 5. Quant aux nuds du sous-arbre droit, ils sont tous suprieurs 5. Ainsi, si lon cherche 8 dans un arbre dont la racine vaut 5, on va chercher seulement dans le sous-arbre droit, puisque 8 est plus grand que 5. Si le nud suivant est 7, on cherche encore droite car 8 est plus grand que 7. Et voil ! On a trouv notre lment en seulement 3 coups ! Si lon cherchait dans une liste, ou dans un arbre mal construit, cela pourrait nous prendre jursqu 8 coups pour savoir si 8 est l ou pas. Les ensembles de Data.Set et les maps de Data.Map sont implments laide darbres binaires de recherche quilibrs, qui sont toujours bien quilibrs. Mais pour linstant, nous allons simplement implmenter des arbres de recherches binaires standards. Voil ce quon va dire : un arbre est soit vide, soit un lment contenant une valeur et deux arbres. a sent le type de donnes algbrique plein nez ! data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

Ok, bien, cest trs bien. Plutt que de construire des arbres la main, on va crer une fonction qui prend un arbre, un lment, et insre cet lment. Cela se fait en comparant la valeur de llment que lon souhaite insrer avec le nud racine, sil est plus petit on part gauche, sil est plus grand on part droite. On fait de mme avec tous les nuds suivants jusqu ce quon arrive un arbre vide. Cest ici quon doit insrer notre nud qui va simplement remplacer ce vide. Dans des langages comme C, on fait ceci en modifiant des pointeurs et des valeurs dans larbre. En Haskell, on ne peut pas vraiment modifier larbre, donc on recre un nouveau sous-arbre chaque fois quon dcide daller gauche ou droite et au final, la fonction dinsertion construit un tout nouvel arbre, puisquHaskell na pas de concept de pointeurs mais seulement de valeurs. Ainsi, le type de la fonction dinsertion sera de la forme a -> Tree a -> Tree a . Elle prend un lment et un arbre et retourne un nouvel arbre qui contient cet lment. a peut paratre inefficace, mais la paresse se charge de ce problme. Voici donc deux fonctions. Lune est une fonction auxiliaire pour crer un arbre singleton (qui ne contient quun nud) et lautre est une fonction dinsertion. singleton :: a -> Tree a singleton x = Node x EmptyTree EmptyTree treeInsert :: (Ord a) => a -> Tree a -> Tree a treeInsert x EmptyTree = singleton x treeInsert x (Node a left right) | x == a = Node x left right | x < a = Node a (treeInsert x left) right | x > a = Node a left (treeInsert x right)

La fonction singleton est juste un raccourci pour crer un nud contenant une valeur et deux sous-arbres vides. Dans la fonction dinsertion, on a dabord notre cas de base sous forme dun motif. Si lon atteint un arbre vide et quon veut y insrer notre valeur, cela veut dire quil faut renvoyer larbre singleton qui contient cette valeur. Si lon insre dans un arbre non vide, il faut vrifier quelques choses. Si llment quon veut insrer est gal la racine, alors on retourne le mme arbre. Sil est plus petit, on retourne un arbre qui a la mme racine, le mme sous-arbre droit, mais qui a pour sous-arbre gauche le mme quavant auquel on a ajout llment ajouter. Le raisonnement est symtrique si llment ajouter est plus grand que llment la racine. Ensuite, on va crer une fonction qui vrifie si un lment est dans larbre. Dfinissons dabord le cas de base. Si on cherche un lment dans larbre vide, il nest certainement pas l. Ok. Voyez comme le cas de base est similaire au cas de base lorsquon cherche un lment dans une liste. Si on cherche un lment dans une liste vide, il nest srement pas l. Enfin bon, si larbre nest pas vide, quelques vrifications. Si llment la racine est celui quon cherche, trouv ! Sinon, eh bien ? Puisquon sait que les lments gauche sont plus petits que lui, si on cherche un lment plus petit, on va le chercher gauche. Symtriquement, on

cherchera droite un lment plus grand que celui la racine. treeElem :: (Ord a) => a -> Tree a -> Bool treeElem x EmptyTree = False treeElem x (Node a left right) | x == a = True | x < a = treeElem x left | x > a = treeElem x right

Tout ce quon a eu faire, cest rcrire le paragraphe prcdent en code. Amusons-nous avec nos arbres ! Plutt que den crer un la main (bien que ce soit possible), crons-en un laide dun pli sur une liste. Souvenez-vous, peu prs tout ce qui traverse une liste et retourne une valeur peut tre implment comme un pli ! On va commencer avec un arbre vide, puis approcher une liste par la droite en insrant les lments un un dans larbre accumulateur. ghci> let nums = [8,6,4,1,7,3,5] ghci> let numsTree = foldr treeInsert EmptyTree nums ghci> numsTree Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree)) (Node 7 (Node 6 EmptyTree EmptyTree) (Node 8 EmptyTree

Dans ce foldr , treeInsert tait la fonction de pliage (elle prend un arbre, un lment dune liste et produit un nouvel arbre) et EmptyTree tait laccumulateur initial. nums , videmment, tait la liste quon pliait. Lorsquon affiche notre arbre la console, il nest pas trs lisible, mais en essayant, on peut voir sa structure. On peut voir que le nud racine est 5 et quil a deux sous-arbres, un qui a pour nud racine 3, lautre qui a pour nud racine 7, etc. ghci> True ghci> False ghci> True ghci> False 8 `treeElem` numsTree 100 `treeElem` numsTree 1 `treeElem` numsTree 10 `treeElem` numsTree

Tester lappartenance fonctionne aussi correctement. Cool. Comme vous le voyez, les structures de donnes algbriques sont des concepts cool et puissants en Haskell. On peut les utiliser pour tout faire, des boolens et numrations des jours de la semaine jusquaux arbres binaires de recherche, et encore plus !

Classes de types 102


Jusquici, on a dcouvert des classes de types standard dHaskell et on a vu quels types les habitaient. On a aussi appris crer automatiquement des instances de ces classes en demandant Haskell de les driver pour nous. Dans cette section, on va crer nos propres classes de types, et nos propres instances, le tout la main. Bref rcapitulatif sur les classes de types : elles sont comme des interfaces. Une classe de types dfinit un comportement (tester lgalit, comparer lordre, numrer), puis les types qui peuvent se comporter de la sorte sont fait instances de ces classes. Le comportement des classes de types est dfini en dfinissant des fonctions ou juste des dclarations de types que les instances doivent implmenter. Ainsi, lorsquon dit quun type est une instance dune classe de types, on veut dire quon peut utiliser les fonctions que cette classe dfinit sur des lments de ce type. Les classes de types nont presque rien voir avec les types dans des langages comme Java ou Python. Cela dsoriente beaucoup de gens, aussi je veux que vous oubliiez tout ce que vous savez des classes dans les langages impratifs partir de maintenant. Par exemple, la classe Eq est pour les choses qui peuvent tre testes gales. Elle dfinit les fonctions == et /= . Si on a un type (mettons, Car ) et que comparer deux voitures avec == a un sens, alors il est sens de rendre Car instance d Eq . Voici comment Eq est dfinie dans le prlude : class Eq (==) (/=) x == x /= a where :: a -> :: a -> y = not y = not

a -> Bool a -> Bool (x /= y) (x == y)

Wow, wow, wow ! Quels nouveaux syntaxe et mots-cls tranges ! Pas dinquitude, tout cela va sclaircir bientt. Tout dabord, quand on crit

class Eq a where , cela signifie quon dfinit une nouvelle classe Eq . Le a est une variable de types et indique que a jouera le rle du type quon souhaitera bientt rendre instance d Eq . Cette variable na pas sappeler a , ou mme tre une lettre, elle doit juste tre un mot en minuscules. Ensuite, on dfinit plusieurs fonctions. Il nest pas obligatoire dimplmenter le corps de ces fonctions, on doit juste spcifier les dclarations de type des fonctions.

Certaines personnes comprendraient peut-tre mieux si lon avait crit class Eq equatable where et ensuite spcifi les dclarations de type comme (==) :: equatable -> equatable -> Bool .

Toutefois, on a implment le corps des fonctions que Eq dfinit, mais on les a dfinies en termes de rcursion mutuelle. On a dit que deux instances d Eq sont gales si elles ne sont pas diffrentes, et diffrentes si elles ne sont pas gales. Ce ntait pas ncessaire, mais on la fait, et on va bientt voir en quoi cela nous aide.

Si nous avons, disons, class Eq a where et quon dfinit une dclaration de type dans cette classe comme (==) :: a -> a -> Bool , alors si on examine le type de cette fonction, celui-ci sera (Eq a) => a -> a -> Bool .

Maintenant quon a une classe, que faire avec ? Eh bien, pas grand chose, vraiment. Mais une fois quon a des instances, on commence bnficier de fonctionnalits sympathiques. Regardez donc ce type : data TrafficLight = Red | Yellow | Green

Il dfinit les tats dun feu de signalisation. Voyez comme on na pas driv dinstances de classes. Cest parce quon va les crire la main, bien quon aurait pu driver celles-ci pour des classes comme Eq et Show . Voici comment on fait de notre type une instance d Eq . instance Eq TrafficLight where Red == Red = True Green == Green = True Yellow == Yellow = True _ == _ = False

On la fait en utilisant le mot-cl instance. class dfinit de nouvelles classes de types, et instance dfinit de nouvelles instances dune classe de types. Quand nous dfinissions Eq , on a crit class Eq a where et on a dit que a jouait le rle du type quon voudra rendre instance de la classe plus tard. On le voit clairement ici, puisque quand on cre une instance, on crit instance Eq TrafficLight where . On a remplac le a par le type rel de cette instance particulire. Puisque == tait dfini en termes de /= et vice versa dans la dclaration de classe, il suffit den dfinir un dans linstance pour obtenir lautre automatiquement. On dit que cest une dfinition complte minimale dune classe de types - un des plus petits ensembles de fonctions implmenter pour que le type puisse se comporter comme une instance de la classe. Pour remplir la dfinition complte minimale d Eq , il faut redfinir soit == , soit /= . Cependant, si Eq tait dfinie seulement ainsi : class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool

alors nous aurions d implmenter ces deux fonctions lors de la cration dune instance, car Haskell ne saurait pas comment elles sont relies. La dfinition complte minimale serait alors : == et /= . Vous pouvez voir quon a implment == par un simple filtrage par motif. Puisquil y a beaucoup de cas o deux lumires sont diffrentes, on a spcifi les cas dgalit, et utilis un motif attrape-tout pour dire que dans tous les autres cas, les lumires sont diffrentes. Crons une instance de Show la main galement. Pour satisfaire une dfinition complte minimale de Show , il suffit dimplmenter show , qui prend une valeur et retourne une chane de caractres. instance show show show Show TrafficLight where Red = "Red light" Yellow = "Yellow light" Green = "Green light"

Encore une fois, nous avons utilis le filtrage par motif pour arriver nos fins. Voyons cela en action : ghci> Red == Red True ghci> Red == Yellow

False ghci> Red `elem` [Red, Yellow, Green] True ghci> [Red, Yellow, Green] [Red light,Yellow light,Green light]

Joli. On aurait pu simplement driver Eq et obtenir la mme chose (on ne la pas fait, dans un but ducatif). Cependant, driver Show aurait simplement traduit les constructeurs en chanes de caractres. Si on veut afficher "Reg light" , il faut faire la dclaration dinstance la main. Vous pouvez galement crer des classes de types qui sont des sous-classes dautres classes de types. La dclaration de classe de Num est un peu longue, mais elle dbute ainsi : class (Eq a) => Num a where ...

Comme on la mentionn prcdemment, il y a beaucoup dendroits o lon peut glisser des contraintes de classe. Ici cest comme crire class Num a where , mais on dclare que a doit tre une instance d Eq au pralable. Ainsi, un type doit tre une instance d eq avant de pouvoir prtendre tre une instance de Num . Il est logique quavant quun type soit considr comme numrique, on puisse attendre de lui quil soit testable pour lgalit. Cest tout pour le sous-typage, cest seulement une contrainte de classes sur une dclaration de classe ! partir de l, quand on dfinit le corps des fonctions, que ce soit dans la dclaration de classe ou bien dans une dclaration dinstance, on peut toujours prsumer que a est membre d Eq et ainsi utiliser == sur des valeurs de ce type. Mais comment est-ce que Maybe , ou le type des listes, sont rendus instances de classes de types ? Ce qui rend Maybe diffrent de, par exemple, TrafficLight , cest que Maybe tout seul nest pas un type concret, mais un constructeur de types qui prend un type en paramtre (comme Char ou un autre) pour produire un type concret (comme Maybe Char ). Regardons nouveau la classe de types Eq : class Eq (==) (/=) x == x /= a where :: a -> :: a -> y = not y = not

a -> Bool a -> Bool (x /= y) (x == y)

De ces dclarations de types, on voit que a est utilis comme un type concret car tous les types entre les flches dune fonction doivent tre concrets (souvenezvous, on ne peut avoir de fonction de type a -> Maybe , mais on peut avoir des fonctions a -> Maybe a ou Maybe Int -> Maybe String ). Cest pour cela quon ne peut pas faire : instance Eq Maybe where ...

Parce que, comme on la vu, a doit tre un type concret, et Maybe ne lest pas. Cest un constructeur de types qui prend un paramtre pour produire un type concret. Il serait galement trs fastidieux dcrire des instances instance Eq (Maybe Int) where , instance Eq (Maybe Char) where , etc. pour chaque type quon utilise. Ainsi, on peut crire : instance Eq (Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False

Cest--dire quon dclare tous les types de la forme Maybe something comme instances d Eq . On aurait vrai dire pu crire (Maybe something) , mais on opte gnralement pour des identifiants en une lettre pour rester proche du style Haskell. Le (Maybe m) ici joue le rle du a de class Eq a where . Alors que Maybe ntait pas un type concret, Maybe m lest. En spcifiant un paramtre de type ( m , qui est en minuscule), on dit quon souhaite parler de tous les types de la forme Maybe m , o m est nimporte quel type, afin den faire une instance d Eq . Il y a un problme cela tout de mme. Le voyez-vous ? On utilise == sur le contenu de Maybe , mais on na pas de garantie que ce que contient Maybe est membre d Eq ! Cest pourquoi nous devons modifier la dclaration dinstance ainsi : instance (Eq m) => Eq (Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False

On a d ajouter une contrainte de classe ! Avec cette dclaration dinstance, on dit ceci : nous voulons que tous les types de la forme Maybe m soient membres de la classe Eq , condition que le type m (celui dans le Maybe ) soit lui-mme membre d Eq . Cest ce quHaskell driverait dailleurs. La plupart du temps, les contraintes de classe dans les dclarations de classes sont utilises pour faire dune classe de types une sous-classe dune autre classe

de types, alors que les contraintes de classe dans les dclarations dinstance sont utilises pour exprimer des pr-requis sur le contenu de certains types. Par exemple, ici nous avons requis que le contenu de Maybe soit membre de la classe Eq . Quand vous faites des instances, si vous voyez quun type est utilis comme un type concret dans les dclarations de type de la classe (comme le a dans a -> a -> Bool ), alors il faut fournir linstance un type concret en fournissant si besoin est des paramtres de type et en parenthsant le tout, de manire obtenir un type concret.

Prenez en compte le fait que le type dont vous essayez de faire une instance remplacera le paramtre dans la dclaration de classe. Le a de class Eq a where sera remplac par un type rel lorsque vous crirez une instance, donc essayez de mettre mentalement le type dans les dclarations de types des fonctions. (==) :: Maybe -> Maybe -> Bool ne veut ainsi par dire grand chose, alors que (==) :: (Eq m) => Maybe m -> Maybe m -> Bool est sens. Cest juste pour mieux y voir dans votre tte, en ralit, == conservera toujours son type (==) :: (Eq a) => a -> a -> Bool , peu importe le nombre dinstances que lon cre.

Oh, encore un truc, regardez a ! Si vous voulez connatre les instances dune classe de types, faites juste :info YourTypeClass dans GHCi. En tapant :info Num , vous verrez toutes les fonctions que dfinit cette classe de types, suivies dune liste de tous les types qui habitent cette classe. :info marche aussi sur les types et les constructeurs de types. :info Maybe vous montre toutes les classes dont Maybe est une instance. :info peut aussi montrer la dclaration de type dune fonction. Je trouve a plutt cool.

Une classe de types oui-non


En JavaScript et dans dautres langages typage faible, on peut mettre quasiment ce que lon veut dans une expression if. Par exemple, tout ceci est valide : if (0) alert ("YEAH") else alert("NO!") , if ("") alert ("YEAH") else alert("NO!") , if (false) alert ("YEAH") else alert("NO!") , etc. et tout cela lancera une alerte NO! . Si vous faites if ("WHAT") alert ("YEAH") else alert("NO!") , une alerte "YEAH!" sera lance parce que JavaScript considre toutes les chanes de caractres non vides comme des sortes de valeurs vraies. Bien quutiliser Bool pour une smantique boolenne marche mieux en Haskell, implmentons un fonctionnement la JavaScript pour le fun ! Commenons par une dclaration de classe. class YesNo a where yesno :: a -> Bool

Plutt simple. La classe YesNo dfinit une seule fonction. Cette fonction prend une valeur dun type qui peut reprsenter le concept de vrit, et nous indique si cette valeur est vraie ou fausse. Remarquez que la faon dont on utilise a dans cette dclaration implique que a doit tre un type concret. Ensuite, dfinissons des instances. Pour les nombres, on va dire que tout nombre qui nest pas 0 est vrai, et que 0 est faux (comme en JavaScript). instance YesNo Int where yesno 0 = False yesno _ = True

Les listes vides (et par extension les chanes de caractres vides) sont des valeurs fausses, alors que les listes ou chanes non vides sont des valeurs vraies. instance YesNo [a] where yesno [] = False yesno _ = True

Remarquez quon a ajout un paramtre de type a ici pour faire de la liste un type concret, bien quon nait pas ajout de contrainte sur ce que la liste contient. Quoi dautre, hmmm Je sais, Bool aussi contient la notion de vrit, et cest plutt vident de savoir lequel est quoi. instance YesNo Bool where yesno = id

Hein ? Quest-ce qu id ? Cest juste une fonction de la bibliothque standard qui prend un paramtre et retourne la mme chose, ce qui correspond ce quon crirait ici de toute faon. Faisons de Maybe a une instance. instance YesNo (Maybe a) where yesno (Just _) = True yesno Nothing = False

Pas besoin de contrainte de classe puisquon ne fait aucune supposition sur ce que contient le Maybe . On a juste considr que toute valeur Just est vraie, alors que Nothing est faux. Il a encore fallu crire (Maybe a) plutt que Maybe tout court, parce quen y rflchissant, une fonction Maybe -> Bool ne peut pas exister (car Maybe nest pas un type concret), alors que Maybe a -> Bool est bien et propre. Cependant, cest plutt cool puisqu prsent, tout type de la forme Maybe something est membre de YesNo , peu importe ce que something est. Prcdemment, on a dfini un type Tree a , qui reprsentait un arbre binaire de recherche. On peut dire quun arbre vide est faux, alors quun arbre non vide est vrai. instance YesNo (Tree a) where yesno EmptyTree = False yesno _ = True

Est-ce quun feu tricolore peut tre une valeur vraie ou fausse ? Bien sr. Si cest rouge, on sarrte. Si cest vert, on y va. Si cest jaune ? Eh, personnellement je foncerai pour ladrnaline. instance YesNo TrafficLight where yesno Red = False yesno _ = True

Cool, on a plein dinstances, jouons avec ! ghci> False ghci> True ghci> False ghci> True ghci> True ghci> False ghci> False ghci> True ghci> yesno yesno $ length [] yesno "haha" yesno "" yesno $ Just 0 yesno True yesno EmptyTree yesno [] yesno [0,0,0] :t yesno :: (YesNo a) => a -> Bool

Bien, a marche ! Faisons une fonction qui copie le comportement de if, mais avec des valeurs YesNo . yesnoIf :: (YesNo y) => y -> a -> a -> a yesnoIf yesnoVal yesResult noResult = if yesno yesnoVal then yesResult else noResult

Plutt direct. Elle prend une valeur YesNo et deux choses. Si la valeur est plutt vraie, elle retourne la premire chose, sinon elle retourne la deuxime. ghci> yesnoIf "NO!" ghci> yesnoIf "YEAH!" ghci> yesnoIf "YEAH!" ghci> yesnoIf "YEAH!" ghci> yesnoIf "NO!" [] "YEAH!" "NO!" [2,3,4] "YEAH!" "NO!" True "YEAH!" "NO!" (Just 500) "YEAH!" "NO!" Nothing "YEAH!" "NO!"

La classe de types Functor


Jusquici, nous avons rencontr pas mal de classes de types de la bibliothque standard. On a jou avec Ord , pour les choses quon peut ranger en ordre. On sest familiaris avec Eq , pour les choses dont on peut tester lgalit. On a vu Show , qui prsente une interface pour les choses qui peuvent tre affiches sous une forme de chane de caractres. Notre bon ami Read est l lorsquon veut convertir des chanes de caractres en des valeurs dun certain type. Et maintenant, on va regarder la classe de types Functor , qui est simplement pour les choses sur lesquelles on peut mapper. Vous pensez probablement aux listes linstant, puisque mapper sur des listes est un idiome prdominant en Haskell. Et vous avez raison, le type des listes est bien membre de la classe Functor . Quel meilleur moyen de connatre Functor que de regarder son implmentation ? Jetons un coup dil.

class Functor f where fmap :: (a -> b) -> f a -> f b

Parfait. On voit que la classe dfinit une fonction, fmap , et ne fournit pas dimplmentation par dfaut. Le type de fmap est intressant. Jusquici, dans les dfinitions de classes de types, la variable de type qui jouait le rle du type dans la classe de type tait un type concret, comme le a dans (==) :: (Eq a) => a -> a -> Bool . Mais maintenant, le f nest pas un type concret (un type qui peut contenir des valeurs, comme Int , Bool ou Maybe String ), mais un constructeur de types qui prend un paramtre de type. Petit rafrachissement par lexemple : Maybe Int est un type concret, mais Maybe est un constructeur qui prend un type en paramtre. Bon, on voit que fmap prend une fonction dun type vers un autre type et un foncteur appliqu au premier type, et retourne un foncteur appliqu au second type. Si cela semble un peu confus, ne vous inquitez pas. Tout sera bientt rvl avec des exemples. Hmm, cette dclaration de type de fmap me rappelle quelque chose. Si vous ne connaissez pas la signature de map , cest map :: (a -> b) -> [a] -> [b] . Ah, intressant ! Elle prend une fonction dun type vers un autre type, une liste du premier type, et retourne une liste du second type. Mes amis, je crois que nous avons l un foncteur ! En fait, map est juste le fmap des listes. Voici linstance de Functor pour les listes. instance Functor [] where fmap = map

Cest tout ! Remarquez quon na pas crit instance Functor [a] where , parce quen regardant fmap :: (a -> b) -> f a -> f b , on voit que f doit tre un constructeur de types qui prend un type. [a] est dj un type concret (dune liste contenant nimporte quel type), alors que [] est un constructeur de types qui prend un type et produit des types comme [Int] , [String] ou mme [[String]] . Puisque pour les listes, fmap est juste map , on obtient les mmes rsultats en utilisant lun ou lautre sur des listes. ghci> fmap (*2) [1..3] [2,4,6] ghci> map (*2) [1..3] [2,4,6]

Que se passe-t-il lorsquon map ou fmap sur une liste vide ? Eh bien, bien sr, on obtient une liste vide. On transforme simplement une liste vide de type [a] en une liste vide de type [b] . Tous les types qui peuvent se comporter comme des botes peuvent tre des foncteurs. Vous pouvez imaginer une liste comme une bote ayant une infinit de petits compartiments, et ils peuvent tre soit tous vides, soit partiellement remplis. Donc, quoi dautre quune liste peut tre assimil une bote ? Pour commencer, le type Maybe a . Cest comme une bote qui peut contenir soit rien, auquel cas elle a la valeur Nothing , soit un lment, comme "HAHA" , auquel cas elle a la valeur Just "HAHA" . Voici comment Maybe est fait foncteur. instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing

Encore une fois, remarquez quon a crit instance Functor Maybe where et pas instance Functor (Maybe m) where , contrairement ce quon avait fait avec Maybe dans YesNo . Functor veut un constructeur de types qui prend un paramtre, pas un type concret. Si vous remplacez mentalement les f par des Maybe , fmap agit comme (a -> b) -> Maybe a -> Maybe b pour ce type, ce qui semble correct. Par contre, si vous remplacez mentalement f par (Maybe m) , alors elle devrait agir comme (a -> b) -> Maybe m a -> Maybe m b , ce qui na pas de sens puisque Maybe ne prend quun seul paramtre de type. Bon, limplmentation de fmap est plutt simple. Si cest une valeur vide Nothing , alors on retourne Nothing . Si on mappe sur une bote vide, on obtient une bote vide. Cest sens. Tout comme mapper sur une liste vide retourne une liste vide. Si ce nest pas une valeur vide, mais plutt une valeur emballe dans un Just , alors on applique la fonction sur le contenu du Just . ghci> fmap (++ " HEY GUYS IM INSIDE THE JUST") (Just "Something serious.") Just "Something serious. HEY GUYS IM INSIDE THE JUST" ghci> fmap (++ " HEY GUYS IM INSIDE THE JUST") Nothing Nothing ghci> fmap (*2) (Just 200) Just 400 ghci> fmap (*2) Nothing Nothing

Une autre chose sur laquelle on peut mapper, et donc en faire une instance de Functor, cest notre type Tree a . On peut limaginer comme une bote qui peut contenir zro ou plusieurs valeurs, et le constructeur de type Tree prend exactement un paramtre de type. Si vous regardez fmap comme une fonction sur les Tree , sa signature serait (a -> b) -> Tree a -> Tree b . On va utiliser de la rcursivit pour celui-l. Mapper sur un arbre vide produira un arbre vide. Mapper sur un arbre non vide donnera un arbre o notre fonction sera applique sur le nud racine, et les deux sous-arbres correspondront aux sous-arbres du nud de dpart sur lesquels on aura mapp la mme fonction. instance Functor Tree where fmap f EmptyTree = EmptyTree fmap f (Node x leftsub rightsub) = Node (f x) (fmap f leftsub) (fmap f rightsub)

ghci> fmap (*2) EmptyTree EmptyTree ghci> fmap (*4) (foldr treeInsert EmptyTree [5,7,3,2,1,7]) Node 28 (Node 4 EmptyTree (Node 8 EmptyTree (Node 12 EmptyTree (Node 20 EmptyTree EmptyTree)))) EmptyTree

Joli ! Et pourquoi pas Either a b prsent ? Peut-on en faire un foncteur ? La classe de types Functor veut des constructeurs de types un paramtre de type, mais Either en prend deux. Hmmm ! Je sais, on va partiellement appliquer Either en lui donnant un paramtre de type, de faon ce quil nait plus quun paramtre libre. Voici comment Either a est fait foncteur dans la bibliothque standard : instance Functor (Either a) where fmap f (Right x) = Right (f x) fmap f (Left x) = Left x

Eh bien, quavons-nous fait l ? Vous voyez quon a fait d Either a une instance, au lieu d Either . Cest parce qu Either a est un constructeur de types qui attend un paramtre, alors qu Either en attend deux. Si fmap tait spcifiquement pour Either a , elle aurait pour signature (b -> c) -> Either a b -> Either a c , parce que cest identique (b -> c) -> (Either a) b -> (Either a) c . Dans limplmentation, on a mapp sur le constructeur de valeurs Right , mais pas sur Left . Pourquoi cela ? Eh bien, si on retourne la dfinition d Either a b , cest un peu comme : data Either a b = Left a | Right b

Si lon voulait mapper une fonction sur les deux la fois, a et b devraient avoir le mme type. Ce que je veux dire, cest que si on voulait mapper une fonction qui transforme une chane de caractres en chane de caractres, que b tait une chane de caractres, mais que a tait un nombre, a ne marcherait pas. Aussi, en voyant ce que serait le type de fmap sil noprait que sur des valeurs Either , on voit que le premier paramtre doit rester le mme alors que le second peut changer, et cest le constructeur de valeurs Left qui actualise ce premier paramtre. Cela va bien de pair avec notre analogie de la bote, si lon imagine que Left est une bote vide sur laquelle un message derreur indique pourquoi elle est vide. Les maps de Data.Map peuvent aussi tre faites foncteurs puisquelles contiennent (ou non !) des valeurs. Dans le cas de Map k v , fmap va mapper une fonction de type v -> v' sur une map de type Map k v et retourner une map de type Map k v' .

Notez que le ' na pas de smantique spciale dans les noms des types, de mme quil nen avait pas dans les noms des valeurs. On lutilise gnralement pour dnoter des choses qui sont similaires mais un peu modifies.

Essayez de deviner comment Map k est fait instance de Functor par vous-mme ! Avec la classe Functor , on a vu comment les classes de types peuvent reprsenter des concepts dordre suprieur plutt cool. On a aussi pratiqu un peu plus lapplication partielle sur les types et la cration dinstances. Dans un des chapitres suivants, on verra quelques lois qui sappliquent sur les foncteurs.

Encore une chose ! Les foncteurs doivent suivre certaines lois afin quils bnficient de proprits dont on dpendra sans trop sen soucier. Si on fait fmap (+1) sur la liste [1, 2, 3, 4] , on obtient [2, 3, 4, 5] , et pas la liste renverse [5, 4, 3, 2] . Si on utilise fmap (\a -> a) (la fonction identit qui retourne son paramtre inchang) sur une liste, on sattend retrouver la mme liste en retour. Par exemple, si on donnait une mauvaise instance de foncteur notre type Tree , alors en utilisant fmap sur un arbre o le sous-arbre gauche na que des lments infrieurs au nud racine et le sous-arbre droit na que des lments suprieurs, on risquerait de produire un arbre qui na plus cette proprit. On verra les lois des foncteurs plus en dtail dans un des chapitres suivants.

Sortes et un peu de type-fu

Les constructeurs de types prennent dautres types en paramtres pour finalement produire des types concrets. a me rappelle un peu les fonctions, qui prennent des valeurs en paramtre pour produire des valeurs. On a vu que les constructeurs de types peuvent tre appliqus partiellement ( Either String est un constructeur de types qui prend un type et produit un type concret, comme Either String Int ), tout comme le peuvent les fonctions. Cest effectivement trs intressant. Dans cette section, nous allons voir comment dfinir formellement la manire dont les constructeurs de types sont appliqus aux types, tout comme nous avions dfini formellement comment les fonctions taient appliques des valeurs en utilisant des dclarations de type. Vous navez pas ncessairement besoin de lire cette section pour continuer votre qute magique dHaskell, et si vous ne la comprenez pas, ne vous inquitez pas. Cependant, comprendre ceci vous donnera une comprhension trs pousse du systme de types. Donc, les valeurs comme 3 , "YEAH" , ou takeWhile (les fonctions sont aussi des valeurs, puisquon peut les passer et les retourner) ont chacune un type. Les types sont des petites tiquettes que les valeurs transportent afin quon puisse raisonner sur leur valeur. Mais les types ont eux-mme leurs petites tiquettes, appeles sortes. Une sorte est plus ou moins le type dun type. a peut sembler bizarre et droutant, mais cest en fait un concept trs cool. Que sont les sortes et quoi servent-elles ? Eh bien, examinons la sorte dun type laide de la commande :k dans GHCi. ghci> :k Int Int :: *

Une toile ? Comme cest bizarre. Quest-ce que cela signifie ? Une * signifie que le type est un type concret. Un type concret est un type qui ne prend pas de paramtre de types, et les valeurs ne peuvent avoir que des types concrets. Si je devais lire * tout haut (ce qui na jamais t le cas), je dirais juste toile ou type. Ok, voyons maintenant le type de Maybe . ghci> :k Maybe Maybe :: * -> *

Le constructeur de types Maybe prend un type concret (comme Int ) et retourne un type concret comme Maybe Int . Et cest ce que nous dit cette sorte. Tout comme Int -> Int signifie quune fonction prend un Int et retourne un Int , * -> * signifie quun constructeur de types prend un type concret et retourne un type concret. Appliquons Maybe un paramtre de type et voyons la sorte du rsultat. ghci> :k Maybe Int Maybe Int :: *

Comme prvu ! On a appliqu Maybe un paramtre de type et obtenu un type concret (cest ce que * -> * signifie). Un parallle (bien que non quivalent, les types et les sortes tant des choses diffrentes) cela est, lorsque lon fait :t isUpper et :t isUpper 'A' . isUpper a pour type Char -> Bool et isUpper 'A' a pour type Bool , parce que sa valeur est simplement True . Ces deux types ont tout de mme pour sorte * . On a utilis :k sur un type pour obtenir sa sorte, comme on a utilis :t sur une valeur pour connatre son type. Comme dit prcdemment, les types sont les tiquettes des valeurs, et les sortes les tiquettes des types, et on peut voir des parallles entre les deux. Regardons une autre sorte. ghci> :k Either Either :: * -> * -> *

Aha, cela nous indique qu Either prend deux types concrets en paramtres pour produire un type concret. a ressemble aussi la dclaration de type dune fonction qui prend deux choses et en retourne une troisime. Les constructeurs de types sont curryfis (comme les fonctions), donc on peut les appliquer partiellement. ghci> :k Either String Either String :: * -> * ghci> :k Either String Int Either String Int :: *

Lorsquon voulait faire d Either une instance de Functor , on a d lappliquer partiellement parce que Functor voulait des types qui attendent un paramtre, alors qu Either en prend deux. En dautres mots, Functor veut des types de sorte * -> * et nous avons d appliquer partiellement Either pour obtenir un type de sorte * -> * au lieu de sa sorte originale * -> * -> * . Si on regarde nouveau la dfinition de Functor :

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 quil doit produire un type concret parce que ce type est utilis comme valeur dans une fonction. Et de a, on peut dduire que les types qui peuvent tre amis avec Functor doivent avoir pour sorte * -> * . Maintenant, faisons un peu de type-fu. Regardez cette classe de types que je vais inventer maintenant : class Tofu t where tofu :: j a -> t a j

Wow, a a lair 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 dune valeur que la fonction tofu prend en paramtre, j a doit avoir pour sorte * . En supposant que a a pour sorte * , alors j doit avoir pour sorte * -> * . On voit que t doit aussi produire un type concret et doit prendre deux types. Et sachant que a a pour sorte * et que j a pour sorte * -> * , on infre que t doit avoir pour sorte * -> (* -> *) -> * . Ainsi, il prend un type concret ( a ), un constructeur de types qui prend un type concret ( j ) et produit un type concret. Wow. OK, faisons alors un type qui a pour sorte * -> (* -> *) -> * . Voici une faon dy 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 donnes) doivent contenir des valeurs, donc doivent avoir pour sorte * , videmment. On suppose * pour a , ce qui veut dire que b prend un paramtre de type et donc sa sorte est * -> * . On connat les sortes de a et b , et puisquils sont les paramtres de Frank , on voit que Frank a pour sorte * -> (* -> *) -> * . Le premier * reprsente a et le (* -> *) reprsente b . Crons quelques valeurs de type Frank et vrifions leur type. ghci> Frank ghci> Frank ghci> Frank :t Frank {frankField = Just "HAHA"} {frankField = Just "HAHA"} :: Frank [Char] Maybe :t Frank {frankField = Node 'a' EmptyTree EmptyTree} {frankField = Node 'a' EmptyTree EmptyTree} :: Frank Char Tree :t Frank {frankField = "YES"} {frankField = "YES"} :: Frank Char []

Hmm. Puisque frankField a un type de la forme a b , ses valeurs doivent avoir des types de forme similaire. Ainsi, elles peuvent tre Just "HAHA" , qui a pour type Maybe [Char] ou une valeur ['Y', 'E', 'S'] , qui a pour type [Char] (si on utilisait notre propre type liste, ce serait List Char ). Et on voit que les types des valeurs Frank correspondent bien la sorte de Frank . [Char] a pour sorte * et Maybe a pour sorte * -> * . Puisque pour tre une valeur, elle doit avoir un type concret et donc entirement appliqu, chaque valeur Frank blah blaah a pour sorte * . Faire de Frank une instance de Tofu est plutt facile. On voit que tofu prend un j a (par exemple un Maybe Int ) et retourne un t a j . Si lon remplaait j par Frank , le type rsultant serait Frank Int Maybe . instance Tofu Frank where tofu x = Frank x

ghci> Frank ghci> Frank

tofu (Just 'a') :: Frank Char Maybe {frankField = Just 'a'} tofu ["HELLO"] :: Frank [Char] [] {frankField = ["HELLO"]}

Pas trs utile, mais bon pour lentranement de nos muscles de types. Encore un peu de type-fu. Mettons quon ait ce type de donnes : data Barry t k p = Barry { yabba :: p, dabba :: t k }

Et maintenant, on veut crer une instance de Functor . Functor prend des types de sorte * -> * mais Barry na pas lair de cette sorte. Quelle est la sorte de Barry ? Eh bien, on voit quil prend trois paramtres de types, donc a va tre something -> something -> something -> * . Il est prudent de considrer que p est un type concret et a pour sorte * . Pour k , on suppose * , et par extension, t a pour sorte * -> * . Remplaons les something plus tt par ces sortes, et lon obtient (* -> *) -> * -> * -> * . Vrifions avec GHCi. ghci> :k Barry Barry :: (* -> *) -> * -> * -> *

Ah, on avait raison. Comme cest satisfaisant. Maintenant, pour rendre ce type membre de Functor il nous faut appliquer partiellement les deux premiers paramtres de type de faon ce quil nous reste un * -> * . Cela veut dire que le dbut de la dclaration dinstance sera : instance Functor (Barry a b) where . Si on regarde fmap comme si elle tait faite spcifiquement pour Barry , elle aurait pour type fmap :: (a -> b) -> Barry c d a -> Barry c d b , parce quon remplace juste les f de Functor par Barry c d . Le troisime paramtre de type de Barry devra changer, et on voit quil 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 f sur le premier champ. Dans cette section, on a vu un tour dtaill du fonctionnement des paramtres de types et on les a en quelque sorte formaliss avec les sortes, comme on avait formalis les paramtres de fonctions avec des dclarations de type. On a vu quil existe des parallles intressants entre les fonctions et les constructeurs de types. Cependant, ce sont deux choses compltement diffrentes. En faisant du vrai Haskell, vous naurez gnralement pas travailler avec des sortes et infrer des sortes la main comme on a fait ici. Gnralement, vous devez juste appliquer partiellement * -> * ou * votre type personnalis pour en faire une instance dune des classes de types standard, mais il est bon de savoir comment et pourquoi cela fonctionne. Il est aussi intressant de sapercevoir que les types ont leur propre type. Encore une fois, vous navez pas comprendre tout ce quon vient de faire pour continuer lire, mais si vous comprenez comment les sortes fonctionnent, il y a des chances pour que vous ayez dvelopp une comprhension solide du systme de types dHaskell.

Modules

Table des matires

Entres et sorties

Entres et Sorties
Crer nos propres types et classes de types Table des matires Rsoudre des problmes fonctionnellement Nous avons mentionn quHaskell tait un langage fonctionnel pur. Alors que dans des langages impratifs, on parvient gnralement faire quelque chose en donnant lordinateur une srie dtapes excuter, en programmation fonctionnelle on dfinit plutt ce que les choses sont. En Haskell, une fonction ne peut pas changer un tat, comme par exemple le contenu dune variable (quand une fonction change un tat, on dit quelle a des effets de bord). La seule chose quune fonction peut faire en Haskell, cest renvoyer un rsultat bas sur les paramtres quon lui a donns. Si une fonction est appele deux fois avec les mmes paramtres, elle doit retourner le mme rsultat. Bien que a puisse paratre un peu limitant lorsquon vient dun monde impratif, on a vu que cest en fait plutt sympa. Dans un langage impratif, vous navez aucune garantie quune fonction qui est sense calculer des nombres ne va pas brler votre maison, kidnapper votre chien et rayer votre voiture avec une patate pendant quelle calcule ces nombres. Par exemple, quand on a fait un arbre binaire de recherche, on na pas insr un lment dans un arbre en modifiant larbre sa place. Notre fonction pour insrer dans un arbre binaire retournait en fait un nouvel arbre, parce quelle ne peut pas modifier lancien. Bien quavoir des fonctions incapables de changer dtat soit bien puisque cela nous aide raisonner sur nos programmes, il y a un problme avec a. Si une fonction ne peut rien changer dans le monde, comment est-elle sense nous dire ce quelle a calcul ? Pour nous dire ce quelle a calcul, elle doit pouvoir changer ltat dun matriel de sortie (gnralement, ltat de notre cran), qui va ensuite mettre des photons qui voyageront jusqu notre cerveau pour changer ltat de notre esprit, mec. Ne dsesprez pas, tout nest pas perdu. Il savre quHaskell a en fait un systme trs malin pour grer ces fonctions qui ont des effets de bord, qui spare proprement les parties de notre programme qui sont pures de celles qui sont impures, font tout le sale boulot comme parler au clavier ou lcran. Avec ces deux parties spares, on peut toujours raisonner sur la partie pure du programme, et bnficier de toutes les choses que la puret offre, comme la paresse, la robustesse et la modularit, tout en communiquant efficacement avec le monde extrieur.

Hello, world!
Jusquici, nous avions toujours charg nos fonctions dans GHCi pour les tester et jouer avec elles. On a aussi explor les fonctions de la bibliothque standard de cette faon. Mais prsent, aprs environ huit chapitres, on va finalement crire notre premier vrai programme Haskell ! Yay ! Et pour sr, on va se faire ce bon vieux "hello, world" .

Hey ! Pour ce chapitre, je vais supposer que vous disposez dun environnement la Unix pour apprendre Haskell. Si vous tes sous Windows, je suggre dutiliser Cygwin, un environnement la Linux pour Windows, autrement dit, juste ce quil vous faut.

Pour commencer, tapez ceci dans votre diteur de texte favori : main = putStrLn "hello, world"

On vient juste de dfinir un nom main , qui consiste appeler putStrLn avec le paramtre "hello, world" . a semble plutt banal, mais a ne lest pas, comme on va le voir bientt. Sauvegardez ce fichier comme helloworld.hs . Et maintenant, on va faire quelque chose sans prcdent. On va rellement compiler notre programme ! Je suis tellement mu ! Ouvrez votre terminal et naviguez jusquau rpertoire o helloworld.hs est plac et faites : $ ghc --make helloworld [1 of 1] Compiling Main Linking helloworld ...

( helloworld.hs, helloworld.o )

Ok ! Avec de la chance, vous avez quelque chose comme a et vous pouvez prsent lancer le programme en faisant ./helloworld . $ ./helloworld

hello, world

Et voil, notre premier programme compil qui affichait quelque chose dans le terminal. Comme cest extraordinaire(ment ennuyeux) ! Examinons ce quon vient dcrire. Dabord, regardons le type de la fonction putStrLn . ghci> :t putStrLn ghci> :t putStrLn putStrLn :: String -> IO () putStrLn "hello, world" "hello, world" :: IO ()

On peut lire le type de putStrLn ainsi : putStrLn prend une chane de caractres et retourne une action I/O qui a pour type de retour () (cest--dire le tuple vide, aussi connu comme unit). Une action I/O est quelque chose qui, lorsquelle sera excute, va effectuer une action avec des effets de bord (gnralement, lire en entre ou afficher lcran) et contiendra une valeur de retour. Afficher quelque chose lcran na pas vraiment de valeur de retour significative, alors une valeur factice () est utilise.

Le tuple vide est une valeur () qui a pour type () .

Donc, quand est-ce que cette action sera excute ? Eh bien, cest ici que le main entre en jeu. Une action I/O sera excute lorsquon lui donne le nom main et quon lance le programme ainsi cr. Que votre programme entier ne soit quune action I/O semble un peu limitant. Cest pourquoi on peut utiliser la notation do pour coller ensemble plusieurs actions I/O en une seule. Regardez lexemple suivant : main = do putStrLn "Hello, what's your name?" name <- getLine putStrLn ("Hey " ++ name ++ ", you rock!")

Ah, intressant, une nouvelle syntaxe ! Et celle-ci se lit presque comme un programme impratif. Si vous compilez cela et lessayez, cela se comportera certainement conformment ce que vous attendez. Remarquez quon a dit do, puis on a align une srie dtapes, comme en programmation imprative. Chacune de ces tapes est une action I/O. En les mettant ensemble avec la syntaxe do, on les a colles en une seule action I/O. Laction obtenue a pour type IO () , parce que cest le type de la dernire action lintrieur du collage. cause de a, main a toujours la signature de type main :: IO something , o something est un type concret. Par convention, on ne spcifie gnralement pas la dclaration de type de main . Une chose intressante quon na pas rencontre avant est la troisime ligne, qui dit name <- getLine . On dirait quelle lit une ligne depuis lentre et la stocke dans une variable name . Est-ce le cas ? Examinons le type de getLine . ghci> :t getLine getLine :: IO String

Aha, o-kay. getLine est une action I/O qui contient un rsultat de type String . a tombe sous le sens, parce quelle attendra que lutilisateur tape quelque chose dans son terminal, et ensuite, cette frappe sera reprsente comme une chane de caractres. Mais quest-ce qui se passe dans name <- getLine alors ? Vous pouvez lire ceci ainsi : effectue laction I/O getLine puis lie la valeur rsultante au nom name . getLine a pour type IO String , donc name aura pour type String . Vous pouvez imaginer une action I/O comme une bote avec des petits pieds qui sortirait dans le monde rel et irait faire quelque chose l-bas (comme des graffitis sur les murs) et reviendrait peut-tre avec une valeur. Une fois quelle a attrap une valeur pour vous, le seul moyen douvrir la bote pour en rcuprer le contenu est dutiliser la construction <- . Et si lon sort une valeur dune action I/O, on ne peut le faire qu lintrieur dune autre action I/O. Cest ainsi quHaskell parvient sparer proprement les parties pure et impure du code. getLine est en un sens impure, parce que sa valeur de retour nest pas garantie dtre la mme lorsquon lexcute deux fois. Cest pourquoi elle est en quelque sorte tache par le constructeur de types IO , et on ne peut rcuprer cette donne que dans du code I/O. Et puisque le code I/O est tach aussi, tout calcul qui dpend dune valeur tache I/O renverra un rsultat tach. Quand je dis tach, je ne veux pas dire tach de faon ce que lon ne puisse plus jamais utiliser le rsultat contenu dans laction I/O dans un code pur. Non, on d-tache temporairement la donne dans laction I/O lorsquon la lie un nom. Quand on fait name <- getLine , name est une chane de caractres normale, parce quelle reprsente ce qui est dans la bote. On peut avoir une fonction trs complique qui, mettons, prend votre nom (une chane de caractres normale) et un paramtre, et vous donne votre fortune et votre futur bas sur votre nom. On peut faire cela : main = do

putStrLn "Hello, what's your name?" name <- getLine putStrLn $ "Read this carefully, because this is your future: " ++ tellFortune name

et tellFortune (ou nimporte quelle fonction laquelle on passe name ) na pas besoin de savoir quoi que ce soit propos dI/O, cest une simple fonction String -> String ! Regardez ce bout de code. Est-il valide ? nameTag = "Hello, my name is " ++ getLine

Si vous avez rpondu non, offrez-vous un cookie. Si vous avez dit oui, buvez un verre de lave en fusion. Non, je blague ! La raison pour laquelle a ne marche pas, cest parce que ++ requiert que ses deux paramtres soient des listes du mme type. Le premier paramtre a pour type String (ou [Char] si vous voulez), alors que getLine a pour type IO String . On ne peut pas concatner une chane de caractres et une action I/O. On doit dabord rcuprer le rsultat de laction I/O pour obtenir une valeur de type String , et le seul moyen de faire ceci cest de faire name <- getLine dans une autre action I/O. Si lon veut traiter des donnes impures, il faut le faire dans un environnement impur. Ainsi, la trace de limpuret se propage comme le flau des morts-vivants, et il est dans notre meilleur intrt de restreindre les parties I/O de notre code autant que faire se peut. Chaque action I/O effectue encapsule son rsultat en son sein. Cest pourquoi notre prcdent programme aurait pu scrire ainsi : main = do foo <- putStrLn "Hello, what's your name?" name <- getLine putStrLn ("Hey " ++ name ++ ", you rock!")

Cependant, foo aurait juste pour valeur () , donc crire a serait un peu discutable. Remarquez quon na pas li le dernier putStrLn . Cest parce que, dans un bloc do, la dernire action ne peut pas tre lie un nom contrairement aux prcdentes. Nous verrons pourquoi cest le cas un peu plus tard quand nous nous aventurerons dans le monde des monades. Pour linstant, vous pouvez imaginer que le bloc do extrait automatiquement la valeur de la dernire action et la lie son propre rsultat. part la dernire ligne, toute ligne dun bloc do qui ne lie pas peut tre rcrite avec une liaison. Ainsi, putStrLn "BLAH" peut tre rcrit comme _ <- putStrLn "BLAH" . Mais a ne sert rien, donc on enlve le <- pour les actions I/O dont le rsultat ne nous importe pas, comme putStrLn something . Les dbutants pensent parfois que faire name = getLine

va lire lentre et lier la valeur de cela name . Eh bien non, tout ce que cela fait, cest de donner un autre nom laction I/O getLine , ici, name . Rappelez-vous, pour obtenir une valeur dune action I/O, vous devez le faire de lintrieur dune autre action I/O et la liant un nom via <- . Les actions I/O ne seront excutes que si elles ont pour nom main ou lorsquelles sont dans une grosse action I/O compose par un bloc do en train dtre excute. On peut utiliser les blocs do pour coller des actions I/O, puis utiliser cette action I/O dans un autre bloc do, et ainsi de suite. De toute faon, elles ne seront excutes que si elles finissent par se retrouver dans main . Oh, joubliais, il y a un autre cas o une action I/O est excute. Cest lorsque lon tape cette action I/O dans GHCi et quon presse Entre. ghci> putStrLn "HEEY" HEEY

Mme lorsque lon tape juste des nombres ou quon appelle une fonction dans GHCi et quon tape Entre, il va lvaluer (autant que ncessaire) et appeler show sur le rsultat, puis afficher cette chane de caractres sur le terminal en appelant implicitement putStrLn . Vous vous souvenez des liaisons let ? Si ce nest pas le cas, rafrachissez-vous lesprit en lisant cette section. Elles sont de la forme let bindings in expression , o bindings sont les noms donner aux expressions et expression est lexpression value qui peut voir ces liaisons. On a aussi dit que dans les listes en comprhension, la partie in ntait pas ncessaire. Eh bien, on peut aussi les utiliser dans les blocs do comme on le faisait dans les listes en comprhension. Regardez : import Data.Char main = do putStrLn "What's your first name?" firstName <- getLine putStrLn "What's your last name?" lastName <- getLine let bigFirstName = map toUpper firstName

bigLastName = map toUpper lastName putStrLn $ "hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"

Remarquez comme les actions I/O dans le bloc do sont alignes. Notez galement comme le let est align avec les actions I/O, et les noms du let sont aligns les uns aux autres. Cest important, parce que lindentation importe en Haskell. Ici, on a fait map toUpper firstName , qui transforme quelque chose comme "John" en une chane de caractres bien plus cool comme "JOHN" . On a li cette chane de caractres majuscules un nom, puis utilis ce nom dans une chane de caractres quon a affiche sur le terminal plus tard. Vous vous demandez peut-tre quand utiliser <- et quand utiliser des liaisons let ? Eh bien, souvenez-vous, <- sert (pour linstant) effectuer des actions I/O et lier leur rsultat des noms. map toUpper firstName , cependant, nest pas une action I/O. Cest une expression pure en Haskell. Donc, utilisez <- quand vous voulez lier le rsultat dune action I/O un nom et utiliser une liaison let pour lier une expression pure un nom. Si on avait fait quelque chose comme let firstName = getLine , on aurait juste donn laction I/O getLine un nouveau nom, sans avoir excut cette action. prsent, on va faire un programme qui lit continuellement une ligne et laffiche avec les mots renverss. Le programme sarrtera si on entre une ligne vide. Voici le programme : main = do line <- getLine if null line then return () else do putStrLn $ reverseWords line main reverseWords :: String -> String reverseWords = unwords . map reverse . words

Pour vous rendre compte de ce quil fait, essayez de lexcuter avant quon sintresse au code.

Astuce : Pour lancer un programme, vous pouvez soit le compiler puis lancer lexcutable produit en faisant ghc --make helloworld puis ./helloworld , ou bien vous pouvez utiliser la commande runhaskell ainsi : runhaskell helloworld.hs et votre programme sera excut la vole.

Tout dabord, regardons la fonction reverseWords . Cest une fonction normale qui prend une chane de caractres comme "hey there man" et appelle words pour produire une liste de mots comme ["hey", "there", "man"] . Puis, on mappe reverse sur la liste, rsultant en ["yeh", "ereht", "nam"] , puis on regroupe cette liste en une chane en utilisant unwords et le rsultat final est "yeh ereht nam" . Voyez comme on a utilis la composition de fonctions ici. Sans cela, on aurait d crire reverseWords st = unwords (map reverse (words st)) . Quid de main ? Dabord, on rcupre une ligne du terminal avec getLine et on nomme cette ligne line . Maintenant, on a une expression conditionnelle. Souvenez-vous, en Haskell, tout if doit avoir un else parce que chaque expression est une valeur. On a fait le if de sorte que lorsque la condition est vraie (dans notre cas, la ligne est non vide), on excute une action I/O, et lorsquelle est vide, laction I/O sous le else est excute la place. Cest pour cela que dans des blocs do I/O, les if doivent avoir la forme if condition then I/O action else I/O action . Regardons dabord du ct de la clause else. Puisquon doit avoir une action I/O aprs le else, on cre un bloc do pour coller des actions en une. Vous pouvez aussi crire cela : else (do putStrLn $ reverseWords line main)

Cela rend plus explicite le fait que le bloc do peut tre vu comme une action I/O, mais cest plus moche. Peu importe, dans le bloc do, on appelle reverseWords sur la ligne obtenue de getLine , puis on laffiche au terminal. Aprs cela, on excute main . Cest un appel rcursif, et cest bon, parce que main est bien une action I/O. En un sens, on est de retour au dbut du programme. Maintenant, que se passe-t-il lorsque null line est vrai ? Dans ce cas, ce qui suit le then est excut. Si on regarde, on voit quil y a then return () . Si vous avez utilis des langages impratifs comme C, Java ou Python, vous vous dites certainement que vous savez dj ce que return fait, et il se peut que vous ayez dj saut ce long paragraphe. Eh bien, voil le dtail qui tue : le return de Haskell na vraiment rien voir avec le return de la plupart des autres langages ! Il a le mme nom, ce qui embrouille beaucoup de monde, mais en ralit, il est bien diffrent. Dans les langages impratifs, return termine gnralement lexcution de la mthode ou sous-routine, en rapportant une valeur son appelant. En Haskell (plus spcifiquement, dans les action I/O), il cre une action I/O partir dune valeur pure. Si vous repensez lanalogie de la bote faite prcdemment, il prend une valeur et la met dans une bote. Laction I/O rsultante ne fait en ralit rien, mais encapsule juste cette valeur comme son rsultat. Ainsi, dans un contexte dI/O, return "HAHA" aura pour type IO String . Quel est le but de transformer une valeur pure en une action I/O qui ne fait rien ? Pourquoi salir notre programme d IO plus que ncessaire ? Eh bien, il nous fallait une action I/O excuter dans le cas o la ligne en entre tait vide. Cest pourquoi on a cr une fausse action I/O qui ne fait rien, en crivant

return () . Utiliser return ne cause pas la fin de lexcution du bloc do I/O ou quoi que ce soit du genre. Par exemple, ce programme va gentiment sexcuter jusquau bout de la dernire ligne : main = do return () return "HAHAHA" line <- getLine return "BLAH BLAH BLAH" return 4 putStrLn line

Tout ce que ces return font, cest crer des actions I/O qui ne font rien de spcial part encapsuler un rsultat, et les rsultats sont ici jets puisquon ne les lie pas des noms. On peut utiliser return en combinaison avec <- pour lier des choses des noms. main = do a <- return "hell" b <- return "yeah!" putStrLn $ a ++ " " ++ b

Comme vous voyez, return est un peu loppos de <- . Alors que return prend une valeur et lenveloppe dans une bote, <- prend une bote, lexcute, et en extrait la valeur pour la lier un nom. Faire cela est un peu redondant, puisque lon dispose des liaisons let dans les blocs do, donc on prfre : main = do let a = "hell" b = "yeah" putStrLn $ a ++ " " ++ b

Quand on fait des blocs do I/O, on utilise principalement return soit parce que lon a besoin de crer une action I/O qui ne fait rien, ou bien parce quon ne veut pas que laction cre par le bloc ait la valeur de la dernire action qui la compose, auquel cas on place un return tout la fin avec le rsultat quon veut obtenir de cette action compose.

Un bloc do peut aussi avoir une seule action I/O. Dans ce cas, cest quivalent crire seulement laction. Certains vont prfrer crire then do return () parce que le else avait un do.

Avant de passer aux fichiers, regardons quelques fonctions utiles pour faire des I/O. putStr est un peu comme putStrLn puisquil prend une chane de caractres en paramtre et retourne une action I/O qui affiche cette chane sur le terminal, seulement putStr ne va pas la ligne aprs avoir affich la chane, alors que putStrLn va la ligne. main = do putStr "Hey, " putStr "I'm " putStrLn "Andy!"

$ runhaskell putstr_test.hs Hey, I'm Andy!

Sa signature de type est putStr :: String -> IO () , donc le rsultat encapsul est unit. Une valeur inutile, donc a ne sert rien de la lier. putChar prend un caractre et retourne une action I/O qui laffiche sur le terminal. main = do putChar 't' putChar 'e' putChar 'h'

$ runhaskell putchar_test.hs teh

putStr est en fait dfini rcursivement laide de putChar . Le cas de base de putStr est la chane vide, donc si on affiche une chane vide, elle retourne juste une action I/O qui ne fait rien laide de return () . Si la chane nest pas vide, elle affiche son premier caractre avec putChar , puis affiche le reste de la chane avec putStr .

putStr :: String -> IO () putStr [] = return () putStr (x:xs) = do putChar x putStr xs

Voyez comme on peut utiliser la rcursivit dans les I/O, comme dans du code pur. Comme pour un code pur, on dfinit un cas de base, et on rflchit ce que le rsultat doit tre. Cest une action qui affiche le premire caractre puis affiche le reste de la chane. print prend une valeur de nimporte quel type instance de Show (donc, quon sait reprsenter sous forme de chane de caractres), et appelle show sur cette valeur pour la transformer en chane, puis affiche cette chane sur le terminal. En gros, cest seulement putStrLn . show . Elle lance dabord show sur la valeur, puis nourrit putStrLn du rsultat, qui va alors retourner une action I/O qui affichera notre valeur. main = do print print print print print True 2 "haha" 3.2 [3,4,3]

$ runhaskell print_test.hs True 2 "haha" 3.2 [3,4,3]

Comme vous le voyez, cette fonction est trs pratique. Vous vous souvenez quon a dit que les actions I/O ntaient excutes que lorsquelles sont sous main ou quand on essaie de les valuer dans linvite GHCi ? Quand on tape une valeur (comme 3 ou [1, 2, 3] ) et quon tape Entre, GHCi utilise en fait print sur cette valeur pour lafficher dans notre terminal ! ghci> 3 3 ghci> print 3 3 ghci> map (++"!") ["hey","ho","woo"] ["hey!","ho!","woo!"] ghci> print (map (++"!") ["hey","ho","woo"]) ["hey!","ho!","woo!"]

Quand on veut afficher des chanes de caractres, on utilise gnralement putStrLn parce quon ne veut pas des guillemets autour delles, mais pour afficher toutes les autres valeurs, print est gnralement utilis. getChar est une action I/O qui lit un caractre de lentre. Ainsi, sa signature de type est getChar :: IO Char , parce que le rsultat contenu dans laction I/O a pour type Char . Notez que les caractres sont mis en tampon, ainsi la lecture des caractres naura pas lieu tant que lutilisateur ne tape pas Entre. main = do c <- getChar if c /= ' ' then do putChar c main else return ()

Ce programme semble lire un caractre et vrifier si cest un espace. Si cest le cas, il sarrte, sinon il affiche le caractre et recommence. Cest un peu ce quil fait, mais pas forcment comme on sy attend. Regardez : $ runhaskell getchar_test.hs hello sir hello

La seconde ligne est lentre. On tape hello sir et on appuie sur Entre. cause de la mise en tampon, lexcution du programme ne dmarre que lorsquon tape Entre, et pas chaque caractre tap. Mais une fois quon presse Entre, il agit sur ce quon vient de taper. Essayez de jouer avec ce programme pour vous rendre compte ! La fonction when est dans Control.Monad (pour lutiliser, faites import Control.Monad ). Elle est intressante parce que, dans un bloc do, on dirait quelle contrle le flot du programme, alors quen fait cest une fonction tout fait normale. Elle prend une valeur boolenne et une action I/O. Si la valeur boolenne est

True , elle retourne laction I/O quon lui a passe. Si cest False , elle retourne return () , donc une action I/O qui ne fait rien. Voici comment on pourrait rcrire le code prcdent dans lequel on prsentait getChar , en utilisant when : import Control.Monad main = do c <- getChar when (c /= ' ') $ do putChar c main

Comme vous voyez, cest utile pour encapsuler le motif if something then do some I/O action else return () . sequence prend une liste dactions I/O et retourne une action I/O qui excutera ces actions lune aprs lautre. Le rsultat contenu dans cette action I/O sera la liste de tous les rsultats de toutes les actions I/O excutes. Sa signature de type est sequence :: [IO a] -> IO [a] . Faire ceci : main = do a <- getLine b <- getLine c <- getLine print [a,b,c]

est quivalent faire : main = do rs <- sequence [getLine, getLine, getLine] print rs

Donc sequence [getLine, getLine, getLine] cre une action I/O qui va excuter getLine trois fois. Si on lie cette action un nom, le rsultat sera une liste de tous les rsultats, dans notre cas, une liste de trois choses que lutilisateur a tapes dans linvite. Un motif courant avec sequence consiste mapper des fonctions comme print ou putStrLn sur des listes. Faire map print [1, 2, 3, 4] ne crera pas une action I/O. Cela va crer une liste dactions I/O, car cest quivalent [print 1, print 2, print 3, print 4] . Si on veut transformer une liste dactions I/O en une action I/O, il faut la squencer. ghci> sequence (map print [1,2,3,4,5]) 1 2 3 4 5 [(),(),(),(),()]

Cest quoi ce [(), (), (), (), ()] la fin ? Eh bien, quand on value une action I/O dans GHCi, elle est excute et le rsultat est affich, moins que ce ne soit () , auquel cas il nest pas affich. Cest pourquoi valuer putStrLn "hehe" dans GHCi affiche seulement hehe (parce que le rsultat contenu dans cette action est () ). Mais quand on fait getLine dans GHCi, le rsultat de cette action est affich dans GHCi, parce que getLine a pour type IO String . Puisque mapper une fonction qui retourne une action I/O sur une liste puis squencer cette liste est tellement commun, les fonctions utilitaires mapM et mapM_ ont t introduites. mapM prend une fonction et une liste, mappe la fonction sur la liste et squence le tout. mapM_ fait pareil, mais jette le rsultat final. On utilise gnralement mapM_ lorsquon se fiche du rsultat de nos actions squences. ghci> mapM print [1,2,3] 1 2 3 [(),(),()] ghci> mapM_ print [1,2,3] 1 2 3

forever prend une action I/O et retourne une action I/O qui rpte la premire indfiniment. Elle est dans Control.Monad . Ce programme va demander indfiniment lutilisateur une entre, et va la recracher en MAJUSCULES : import Control.Monad import Data.Char

main = forever $ do putStr "Give me some input: " l <- getLine putStrLn $ map toUpper l

forM (dans Control.Monad ) est comme mapM , mais avec les paramtres dans lordre inverse. Le premier paramtre est la liste, et le second la fonction mapper sur cette liste, qui sera finalement squence. Pourquoi est-ce utile ? Eh bien, en tant un peu cratif sur lutilisation des lambdas et de la notation do, on peut faire des choses comme : import Control.Monad main = do colors <- forM [1,2,3,4] (\a -> do putStrLn $ "Which color do you associate with the number " ++ show a ++ "?" color <- getLine return color) putStrLn "The colors that you associate with 1, 2, 3 and 4 are: " mapM putStrLn colors

Le (\a -> do ...) est une fonction qui prend un nombre et retourne une action I/O. On la mise entre parenthses, sinon le lambda croit que les deux dernires actions I/O sur les deux dernires lignes lui appartiennent galement. Remarquez quon fait return color dans le bloc do. On fait cela de manire ce que laction I/O dfinie par ce bloc do contienne pour rsultat notre couleur. En fait on navait pas besoin de faire a ici, puisque getLine contient dj ce rsultat. Faire color <- getLine suivi de return color consiste seulement sortir le rsultat de getLine et le remettre dans la bote juste aprs, donc il suffit de faire juste getLine . Le forM (appel avec ses deux paramtres) produit une action I/O dont on lie le rsultat colors . colors est une liste tout ce quil y a de plus normale, qui contient des chanes de caractres. la fin, on affiche toutes ces couleurs en faisant mapM putStrLn colors . Vous pouvez imaginer forM comme signifiant : cre une action I/O pour tous les lments de cette liste. Ce que laction I/O fait peut dpendre de la valeur de llment de la liste partir duquelle elle a t cre. Finalement, excute toutes ces actions et lie leur rsultat quelque chose. Notez quon nest pas forc de le lier, on pourrait juste le jeter. $ runhaskell form_test.hs Which color do you associate with the white Which color do you associate with the blue Which color do you associate with the red Which color do you associate with the orange The colors that you associate with 1, white blue red orange

number 1? number 2? number 3? number 4? 2, 3 and 4 are:

On aurait pu le faire sans forM , mais cest plus lisible avec. Gnralement, on utilise forM pour mapper et squencer des actions que lon dfinit au vol avec la notation do. Dans la mme veine, on aurait pu remplacer la dernire ligne par forM colors putStrLn . Dans cette section, on a vu les bases de lentre-sortie. On a aussi vu ce que les actions I/O sont, comment elles nous permettent de faire des entres-sorties, et quand elles sont rellement excutes. Pour en remettre une couche, les actions I/O sont des valeurs presque comme les autres en Haskell. On peut les passer en paramtre des fonctions, et des fonctions peuvent en retourner comme rsultat. Ce qui est spcial leur propos, cest que si elles se retrouvent sous la fonction main (ou dans GHCi), elles sont excutes. Et cest alors quelles se retrouvent crire des choses sur votre cran ou jouer Yakety Sax dans vos haut-parleurs. Chaque action I/O peut encapsuler un rsultat qui vous dira ce quelle a trouv dans le monde rel. Ne pensez pas que putStrLn est une fonction qui prend une chane de caractres et laffiche lcran. Pensez-y comme une fonction qui prend une chane de caractres et retourne une action I/O. Cette action I/O affichera, lorsquelle sera excute, de magnifiques pomes dans votre terminal.

Fichiers et flots
getChar est une action I/O qui lit un caractre du terminal. getLine est une action I/O qui lit une ligne du terminal. Ces deux sont plutt simples, et la plupart des langages de programmation ont des fonctions ou des instructions semblables ces actions I/O. Mais prsent, rencontrons getContents . getContents est une action I/O qui lit toute lentre standard jusqu rencontrer un caractre de fin de fichier. Son type est getContents :: IO String . Ce qui est cool avec getContents , cest quelle effectue une entre paresseuse. Quand on fait foo <- getContents , elle ne va pas lire toute lentre dun coup, la stocker en

mmoire, puis la lier foo . Non, elle est paresseuse ! Elle dira : Ouais ouais, je lirai lentre du terminal plus tard, quand tu en auras besoin !. getContents est trs utile quand on veut connecter la sortie dun programme lentre de notre programme. Si vous ne savez pas comment cette connexion ( base de tubes) fonctionne dans les systmes type Unix, voici un petit aperu. Crons un fichier texte qui contient ce haku : I'm a lil' teapot What's with that airplane food, huh? It's so small, tasteless

Ouais, le haku est pourri, mais bon ? Si quelquun connat un tutoriel de hakus, je suis preneur. Bien, souvenez-vous de ce programme quon a crit en introduisant la fonction forever . Il demandait lutilisateur une ligne, et lui retournait en MAJUSCULES, puis recommenait, indfiniment. Pour vous viter de remonter tout l-haut, je vous la remets ici : import Control.Monad import Data.Char main = forever $ do putStr "Give me some input: " l <- getLine putStrLn $ map toUpper l

Sauvegardons ce programme comme capslocker.hs ou ce que vous voulez, et compilons-le. prsent, on va utiliser un tube Unix pour donner notre fichier texte manger notre petit programme. On va utiliser le programme GNU cat, qui affiche le fichier donn en argument. Regardez-moi a, booyaka ! $ ghc --make capslocker [1 of 1] Compiling Main ( capslocker.hs, capslocker.o ) Linking capslocker ... $ cat haiku.txt I'm a lil' teapot What's with that airplane food, huh? It's so small, tasteless $ cat haiku.txt | ./capslocker I'M A LIL' TEAPOT WHAT'S WITH THAT AIRPLANE FOOD, HUH? IT'S SO SMALL, TASTELESS capslocker <stdin>: hGetLine: end of file

Comme vous voyez, connecter la sortie dun programme (dans notre cas ctait cat) lentre dun autre (ici capslocker) est fait laide du caractre | . Ce quon a fait ici est peu prs quivalent lancer capslocker, puis taper notre haku dans le terminal, avant denvoyer le caractre de fin de fichier (gnralement en tapant Ctrl-D). Cest comme lancer cat haiku.txt et dire : Attends, naffiche pas a dans le terminal, va le dire capslocker plutt !. Donc ce quon fait principalement avec cette utilisation de forever cest prendre lentre et la transformer en une sortie. Cest pourquoi on peut utiliser getContents pour rendre ce programme encore plus court et joli : import Data.Char main = do contents <- getContents putStr (map toUpper contents)

On lance laction I/O getContents et on nomme la chane produite contents . Puis on mappe toUpper sur cette chane, et on laffiche sur le terminal. Gardez en tte que puisque les chanes de caractres sont simplement des listes, qui sont paresseuses, et que getContents est aussi paresseuse en entre-sortie, cela ne va pas essayer de lire tout le contenu et de le stocker en mmoire avant dafficher la version en majuscules. Plutt, cela va afficher la version en majuscule au fur et mesure de la lecture, parce que cela ne lira une ligne de lentre que lorsque ce sera ncessaire. $ cat haiku.txt | ./capslocker I'M A LIL' TEAPOT WHAT'S WITH THAT AIRPLANE FOOD, HUH? IT'S SO SMALL, TASTELESS

Cool, a marche. Et si lon essaie de lancer capslocker et de taper les lignes nous-mme ? $ ./capslocker

hey ho HEY HO lets go LETS GO

On a quitt le programme en tapant Ctrl-D. Plutt joli ! Comme vous voyez, cela affiche nos caractres en majuscules ligne aprs ligne. Quand le rsultat de getContents est li contents , ce nest pas reprsent en mmoire comme une vraie chane de caractres, mais plutt comme une promesse de produire cette chane de caractres en temps voulu. Quand on mappe toUpper sur contents , cest aussi une promesse de mapper la fonction sur le contenu une fois quil sera disponible. Et finalement, quand putStr est excut, il dit la promesse prcdente : H, jai besoin des lignes en majuscules !. Celle-ci na pas encore les lignes, alors elle demande contents : H, pourquoi tu nirais pas chercher ces lignes dans le terminal prsent ?. Cest ce moment que getContents va vraiment lire le terminal et donner une ligne au code qui a demand davoir quelque chose de tangible. Ce code mappe alors toUpper sur la ligne et la donne putStr , qui laffiche. Puis putStr dit : H, jai besoin de la ligne suivante, allez ! et tout ceci se rpte jusqu ce quil ny ait plus dentre, comme lindique le caractre de fin de fichier. Faisons un programme qui prend une entre et affiche seulement les lignes qui font moins de 10 caractres de long. Observez : main = do contents <- getContents putStr (shortLinesOnly contents) shortLinesOnly :: String -> String shortLinesOnly input = let allLines = lines input shortLines = filter (\line -> length line < 10) allLines result = unlines shortLines in result

On a fait la partie I/O du programme la plus courte possible. Puisque notre programme est cens lire une entre, et afficher quelque chose en fonction de lentre, on peut limplmenter en lisant le contenu dentre, puis en excutant une fonction pure sur ce contenu, et en affichant le rsultat que la fonction a renvoy. La fonction shortLinesOnly fonctionne ainsi : elle prend une chane de caractres comme "short\nlooooooooooooooong\nshort again" . Cette chane a trois lignes, dont deux courtes et une au milieu plus longue. La fonction lance lines sur cette chane, ce qui la convertit en ["short", "looooooooooooooong", "short again"] , quon lie au nom allLines . Cette liste de chanes est ensuite filtre pour ne garder que les lignes de moins de 10 caractres, produisant ["short", "short again"] . Et finalement, unlines joint cette liste en une seule chane, donnant "short\nshort again" . Testons cela. i'm short so am i i am a loooooooooong line!!! yeah i'm long so what hahahaha!!!!!! short line loooooooooooooooooooooooooooong short

$ ghc --make shortlinesonly [1 of 1] Compiling Main ( shortlinesonly.hs, shortlinesonly.o ) Linking shortlinesonly ... $ cat shortlines.txt | ./shortlinesonly i'm short so am i short

On connecte le contenu de shortlines.txt lentre de shotlinesonly et en sortie, on obtient les lignes courtes. Ce motif qui rcupre une chane en entre, la transforme avec une fonction, puis crit le rsultat est tellement commun quil existe une fonction qui rend cela encore plus simple, appele interact . interact prend une fonction de type String -> String en paramtre et retourne une action I/O qui va lire une entre, lancer cette fonction dessus, et afficher le rsultat. Modifions notre programme en consquence : main = interact shortLinesOnly shortLinesOnly :: String -> String shortLinesOnly input = let allLines = lines input shortLines = filter (\line -> length line < 10) allLines result = unlines shortLines in result

Juste pour montrer que ceci peut tre fait en beaucoup moins de code (bien que ce soit moins lisible) et pour dmontrer notre matrise de la composition de fonctions, on va retravailler cela. main = interact $ unlines . filter ((<10) . length) . lines

Wow, on a rduit cela juste une ligne, cest plutt cool ! interact peut tre utilis pour faire des programmes qui lon connecte un contenu en entre et qui affichent un rsultat en consquence, ou bien pour faire des programmes qui semblent attendre une entre de lutilisateur, et rendent un rsultat bas sur la ligne entre, puis prend une autre ligne, etc. Il ny a en fait pas de relle distinction entre les deux, a dpend juste de la faon dont lutilisateur veut utiliser le programme. Faisons un programme qui lit continuellement une ligne et nous dit si la ligne tait un palindrome ou pas. On pourrait utiliser getLine pour lire une ligne, dire lutilisateur si cest un palindrome, puis lancer main nouveau. Mais il est plus simple dutiliser interact . Quand vous utilisez interact , pensez tout ce que vous avez besoin de faire pour transformer une entre en la sortie dsire. Dans notre cas, on doit remplacer chaque ligne de lentre par soit "palindrome" , soit "not a palindrome" . Donc on doit crire une fonction qui transforme quelque chose comme "elephant\nABCBA\nwhatever" en "not a palindrome\npalindrome\nnot a palindrome" . Faisons a ! respondPalindromes contents = unlines (map (\xs -> if isPalindrome xs then "palindrome" else "not a palindrome") (lines contents where isPalindrome xs = xs == reverse xs

crivons a sans point. respondPalindromes = unlines . map (\xs -> if isPalindrome xs then "palindrome" else "not a palindrome") . lines where isPalindrome xs = xs == reverse xs

Plutt direct. Dabord, on change quelque chose comme "elephant\nABCBA\nwhatever" en ["elephant", "ABCBA", "whatever"] , puis on mappe la lambda sur a, donnant ["not a palindrome", "palindrome", "not a palindrome"] et enfin unlines joint cette liste en une seule chane de caractres. prsent, on peut faire : main = interact respondPalindromes

Testons a : $ runhaskell palindromes.hs hehe not a palindrome ABCBA palindrome cookie not a palindrome

Bien quon ait fait un programme qui transforme une grosse chane de caractres en une autre, a se passe comme si on avait fait un programme qui faisait cela ligne par ligne. Cest parce quHaskell est paresseux et veut afficher la premire ligne de la chane de caractres en sortie, mais ne peux pas parce quil ne la pas encore. Ds quon lui donne une ligne en entre, il va lafficher en sortie. On termine le programme en envoyant un caractre de fin de fichier. On peut aussi utiliser ce programme en connectant simplement un fichier en entre. Disons quon ait ce fichier : dogaroo radar rotor madam

et quon le sauvegarde comme words.txt . Voici ce quon obtient en le connectant en entre de notre programme : $ cat words.txt | runhaskell palindromes.hs not a palindrome palindrome palindrome palindrome

Encore une fois, on obtient la mme sortie que si lon avait tap les mots dans le programme nous-mme. Seulement, on ne voit pas lentre affiche puisquelle est venue du programme et quon ne la pas tape ici.

Vous voyez probablement comment les entres-sorties paresseuses fonctionnent prsent, et comment on peut en tirer parti. Vous pouvez penser simplement ce que doit tre la sortie en fonction de lentre, et crire une fonction qui effectue cette transformation. En entre-sortie paresseuse, rien nest consomm en entre tant que ce nest pas absolument ncessaire, par exemple parce que lon souhaite afficher un rsultat qui dpend de cette entre. Jusquici, nous avons travaill avec les entres-sorties en affichant des choses dans le terminal, et en lisant des choses depuis ce dernier. Mais pourquoi pas lire et crire des fichiers ? En quelque sorte on a dj fait a. Une manire de penser au terminal est de se dire que cest un fichier (un peu spcial). On peut appeler le fichier de ce quon tape dans le terminal stdin , et le fichier qui saffiche dans notre terminal stdout , pour entre standard et sortie standard, respectivement. En gardant cela lesprit, on va voir qucrire ou lire dans un fichier est trs similaire crire ou lire dans lentre ou la sortie standard. On va commencer avec un programme trs simple qui ouvre un fichier nomm girlfriend.txt, qui contient un vers du tube dAvril Lavigne numro 1 Girlfriend, et juste afficher cela dans le terminal. Voici girlfriend.txt : Hey! Hey! You! You! I don't like your girlfriend! No way! No way! I think you need a new one!

Et voici notre programme : import System.IO main = do handle <- openFile "girlfriend.txt" ReadMode contents <- hGetContents handle putStr contents hClose handle

En le lanant, on obtient le rsultat attendu : $ runhaskell girlfriend.hs Hey! Hey! You! You! I don't like your girlfriend! No way! No way! I think you need a new one!

Regardons cela ligne par ligne. La premire ligne contient juste quatre exclamations, pour attirer notre attention. Dans la deuxime ligne, Avril nous indique quelle napprcie pas notre partenaire romantique actuelle. La troisime ligne sert mettre en emphase cette dsapprobation, alors que la quatrime ligne suggre que nous devrions chercher une nouvelle petite amie. Hum, regardons plutt le programme ligne par ligne ! Notre programme consiste en plusieurs actions I/O combines ensemble par un bloc do. Dans la premire ligne du bloc do, on remarque une fonction nouvelle nomme openFile. Voici sa signature de type : openFile :: FilePath -> IOMode -> OI Handle . Si vous lisez ceci tout haut, cela donne : openFile prend un chemin vers un fichier et un IOMode et retourne une action I/O qui va ouvrir le fichier et encapsule pour rsultat la poigne vers le fichier associ. FilePath est juste un synonyme du type String , simplement dfini comme : type FilePath = String

IOMode est dfini ainsi : data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode

Tout comme notre type qui reprsente sept valeurs pour les jours de la semaine, ce type est une numration qui reprsente ce quon veut faire avec le fichier ouvert. Trs simple. Notez bien que le type est IOMode et non pas IO Mode . Ce dernier serait le type dune action I/O qui contient une valeur dun type Mode , alors qu IOMode est juste une simple numration. Finalement, la fonction retourne une action I/O qui ouvre le fichier spcifi dans le mode indiqu. Si lon lie cette action un nom, on obtient un Handle . Une valeur de type Handle reprsente notre fichier ouvert. On utilise cette poigne pour savoir de quel fichier on parle. Il serait stupide douvrir un fichier mais ne pas lier cette poigne un nom, puisquon ne pourrait alors pas savoir quel fichier on a ouvert. Dans notre cas, on lie la poigne au nom handle . la prochaine ligne, on voit une fonction nomme hGetContents . Elle prend un Handle , afin de savoir de quel fichier on veut rcuprer le contenu, et retourne une IO String - une action I/O qui contient en rsultat le contenu du fichier.

Cette fonction est proche de getContents . La seule diffrence, cest que getContents lit automatiquement depuis lentre standard (votre terminal), alors que hGetContents prend une poigne pour savoir quel fichier elle doit lire. Pour le reste, elles font la mme chose. Et tout comme getContents , hGetContents ne va pas lire tout le fichier et le stocker en mmoire, mais lire ce qui sera ncessaire pour progresser. Cest trs cool parce quon peut traiter contents comme le contenu de tout le fichier, alors quil nest pas rellement charg en entier dans la mmoire. Donc, mme pour un norme fichier, faire hGetContents ne va pas touffer notre mmoire, parce que le fichier est lu seulement quand cest ncessaire. Notez bien la diffrence entre la poigne, utilise pour identifier le fichier, et le contenu de ce fichier, lis respectivement dans ce programme aux noms handle et contents . La poigne est juste quelque chose qui indique quel est notre fichier. Si vous imaginez que votre systme de fichiers est un norme livre, dont les fichiers sont des chapitres, la poigne est une sorte de marque-page qui montre quel chapitre vous souhaitez lire (ou crire), alors que le contenu est celui du chapitre. Avec putStr contents , on affiche seulement le contenu sur la sortie standard, puis on fait hClose , qui prend une poigne et retourne une action I/O qui ferme le fichier. Il faut fermer le fichier vous-mme aprs lavoir ouvert avec openFile ! Un autre moyen de faire tout a consiste utiliser la fonction withFile , qui a pour type withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a . Elle prend un chemin vers un fichier, un IOMode et une fonction qui prend une poigne et retourne une action I/O. Elle retourne alors une action I/O qui ouvre le fichier, applique notre fonction, puis ferme le fichier. Le rsultat retourn par cette action I/O est le mme que le rsultat retourn par la fonction quon lui fournit. a peut vous sembler compliqu, mais cest en fait trs simple, surtout avec des lambdas, voici le prcdent exemple rcrit avec withFile : import System.IO main = do withFile "girlfriend.txt" ReadMode (\handle -> do contents <- hGetContents handle putStr contents)

Comme vous le voyez, cest trs similaire au code prcdent. (\handle -> ...) est une fonction qui prend une poigne et retourne une action I/O, et on le fait gnralement comme a avec une lambda. La raison pour laquelle withFile doit prendre une fonction qui retourne une action I/O, plutt que de prendre directement une action I/O, est que laction I/O ne saurait pas sur quel fichier elle doit agir autrement. Ainsi, withFile ouvre le fichier et passe la poigne la fonction quon lui a donne. Elle rcupre ainsi une action I/O, et cre son tour une action I/O qui est comme la prcdente mais ferme le fichier la fin. Voici comment coder notre propre fonction withFile : withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a withFile' path mode f = do handle <- openFile path mode result <- f handle hClose handle return result

On sait que le rsultat sera une action I/O, donc on peut commencer par crire un do. Dabord, on ouvre le fichier pour rcuprer une poigne. Puis, on applique notre fonction sur cette poigne pour obtenir une action I/O qui fait son travail sur ce fichier. On lie le rsultat de cette action result , on ferme la poigne, et on fait return result . En retournant le rsultat encapsul dans laction I/O obtenue par f , notre action I/O compose encapsule le mme rsultat que celui de f handle . Ainsi, si f handle retourne une action I/O qui lit un nombre de lignes de lentre standard, les crit dans un fichier, et encapsule pour rsultat le nombre de lignes quelle a lues, alors en lutilisant avec withFile' , laction I/O rsultante aurait galement pour rsultat le nombre de lignes lues. Tout comme on a hGetContents qui fonctionne comme getContents mais pour un fichier, il y a aussi hGetLine , hPutStr , hPutStrLn , hGetChar , etc. Elles fonctionnent toutes comme leur quivalent sans h, mais prennent une poigne en paramtre et oprent sur le fichier correspondant plutt que lentre ou la sortie standard. Par exemple : putStrLn est une fonction qui prend une chane de caractres et retourne une action I/O qui affiche cette chane dans le terminal avec un retour la ligne la fin. hPutStrLn prend une poigne et une chane et retourne une action I/O qui crit cette chane dans le fichier associ la poigne, suivie dun retour la ligne. Dans la mme veine, hGetLine prend une poigne et retourne une action I/O qui lit une ligne de ce fichier. Charger des fichiers et traiter leur contenu comme des chanes de caractres est tellement commun quon a ces trois fonctions qui facilitent le travail : readFile a pour signature readFile :: FilePath -> IO String . Souvenez-vous que FilePath est juste un nom plus joli pour String . readFile prend un chemin vers un fichier et retourne une action I/O qui lit ce fichier (paresseusement bien sr) et lie son contenu une chane de caractres. Cest gnralement plus pratique que de faire openFile , de lier le retour une poigne, puis de faire hGetContents . Ainsi, le prcdent exemple se rcrit :

import System.IO main = do contents <- readFile "girlfriend.txt" putStr contents

Puisquon ne rcupre pas de poigne, on ne peut pas fermer le fichier manuellement, donc Haskell le fait tout seul quand on utilise readFile . writeFile a pour type writeFile :: FilePath -> String -> IO () . Elle prend un chemin vers un fichier et une chane de caractres crire dans ce fichier et retourne une action I/O qui effectuera lcriture. Si le fichier existe dj, il sera cras compltement avant que lcriture ne commence. Voici comment transformer girlfriend.txt en une version en MAJUSCULES et crire cette version dans girlfriendcaps.txt. import System.IO import Data.Char main = do contents <- readFile "girlfriend.txt" writeFile "girlfriendcaps.txt" (map toUpper contents)

$ runhaskell girlfriendtocaps.hs $ cat girlfriendcaps.txt HEY! HEY! YOU! YOU! I DON'T LIKE YOUR GIRLFRIEND! NO WAY! NO WAY! I THINK YOU NEED A NEW ONE!

appendFile a la mme signature de type que writeFile , mais elle ne tronque pas le fichier sil existe dj, la place elle crit la suite du contenu du fichier existant. Mettons quon ait un fichier todo.txt qui a une tche par ligne de chose quon doit penser faire. Faisons un programme qui prend une ligne de lentre standard et lajoute notre liste de tches. import System.IO main = do todoItem <- getLine appendFile "todo.txt" (todoItem ++ "\n")

$ runhaskell appendtodo.hs Iron the dishes $ runhaskell appendtodo.hs Dust the dog $ runhaskell appendtodo.hs Take salad out of the oven $ cat todo.txt Iron the dishes Dust the dog Take salad out of the oven

On a d ajouter le "\n" la fin de chaque ligne, parce que getLine ne met pas de caractre pour aller la ligne la fin. Ooh, encore une chose. On a dit que contents <- hGetContents handle ne lisait pas tout le fichier dun coup pour le stocker en mmoire. Cest une entresortie paresseuse, donc faire : main = do withFile "something.txt" ReadMode (\handle -> do contents <- hGetContents handle putStr contents)

cest comme connecter un tube depuis le fichier vers la sortie. Comme on peut penser aux listes comme des flots, on peut aussi penser les fichiers comme des flots. Ceci va lire une ligne la fois et lafficher sur le terminal au passage. Vous vous demandez peut-tre quelle est la taille de ce tube alors ? Combien de fois va-t-on accder au disque ? Eh bien, pour les fichiers textes, le tampon par dfaut est par ligne gnralement. Cela veut dire que la plus petite unit du fichier lue la fois est une ligne. Cest pourquoi dans ce cas il lit une ligne, affiche le rsultat, lit la prochaine ligne, affiche le rsultat, etc. Pour des fichiers binaires, la mise en tampon par dfaut est dun bloc. Cela veut dire que le fichier sera lu bout par bout. La taille de ces bouts de fichiers dpend de ce que votre systme dexploitation considre comme une taille cool. Vous pouvez contrler comment la mise en tampon est effectue en utilisant la fonction hSetBuffering . Elle prend une poigne et un BufferMode et retourne

une action I/O qui dfinit le mode de mise en tampon. BufferMode est un simple type de donnes numr, et les valeurs possibles sont : NoBuffering , LineBuffering ou BlockBuffering (Maybe Int) . Le Maybe Int permet de prciser la taille des blocs, en octets. Si cest Nothing , alors le systme dexploitation dtermine la taille du bloc. NoBuffering signifie que les caractres sont lus un par un. NoBuffering nest gnralement pas efficace, parce quil doit accder le disque tout le temps. Voici notre code prcdent, sauf quil ne lit pas ligne par ligne, mais en blocs de 2048 octets. main = do withFile "something.txt" ReadMode (\handle -> do hSetBuffering handle $ BlockBuffering (Just 2048) contents <- hGetContents handle putStr contents)

Lire des fichiers en plus gros morceaux peut aider minimiser les accs au disque, ou lorsque notre fichier est en fait une ressource rseau lente. On peut aussi utiliser hFlush , qui est une fonction qui prend une poigne et retourne une action I/O qui va vider le tampon du fichier associ la poigne. Quand on est en mise en tampon par ligne, le tampon est vid chaque nouvelle ligne. Quand on est en mise en tampon par bloc, le tampon est vid chaque bloc. Il est aussi vid lorsquon ferme la poigne. Cela signifie que lorsquon atteint un caractre de nouvelle ligne, le mcanisme de lecture (ou dcriture) rapporte toutes les donnes quil a lues jusquici. Mais on peut utiliser hFlush pour le forcer rapporter ce quil a lu nimporte quel instant. Aprs avoir vid le tampon en criture, les donnes sont disponibles pour nimporte quel autre programme qui accde au fichier en lecture en mme temps. Pensez la mise en tampon par bloc comme cela : votre cuvette de toilettes est faite pour se vider ds quelle contient un litre deau. Vous pouvez donc commencer la remplir deau, et ds que vous atteignez un litre, leau est automatiquement vide, et les donnes que vous aviez mises dans cette eau sont lues. Mais vous pouvez aussi vider la cuvette manuellement en actionnant la chasse deau. Cela force la cuvette se vider, et toute leau (les donnes) est lue. Au cas o vous nauriez pas saisi, tirer la chasse deau est une mtaphore pour hFlush . Ce nest pas une analogie trs sensationnelle en termes de critres danalogies dans la programmation, mais je voulais une analogie avec le monde rel dun objet quon peut vider, pour ma chute. On a dj fait un programme pour ajouter un nouvel lment une liste de tches dans todo.txt, prsent, faisons un programme qui retire une tche. Je vais coller le code, et on va le regarder en dtail par la suite ensemble, pour que vous voyiez que cest trs simple. On va utiliser quelques nouvelles fonctions sorties de System.Directory et une nouvelle fonction de System.IO , mais jexpliquerai cela en temps voulu. Bon, voici le programme qui retire un lment de todo.txt : import System.IO import System.Directory import Data.List main = do handle <- openFile "todo.txt" ReadMode (tempName, tempHandle) <- openTempFile "." "temp" contents <- hGetContents handle let todoTasks = lines contents numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks putStrLn "These are your TO-DO items:" putStr $ unlines numberedTasks putStrLn "Which one do you want to delete?" numberString <- getLine let number = read numberString newTodoItems = delete (todoTasks !! number) todoTasks hPutStr tempHandle $ unlines newTodoItems hClose handle hClose tempHandle removeFile "todo.txt" renameFile tempName "todo.txt"

Au dpart, on ouvre seulement todo.txt en lecture, et on lie sa poigne au nom handle . Ensuite, on utilise une fonction que nous navons pas encore rencontre, qui vient de System.IO - openTempFile . Son nom est assez expressif. Elle prend un chemin vers un rpertoire temporaire, et un prfixe de nom, et ouvre un fichier temporaire. On a utilis "." pour le dossier temporaire, parce que . indique le rpertoire courant sur peu prs tous les systmes dexploitation. On a utilis "temp" pour le prfixe de nom du fichier temporaire, ce qui signifie que le fichier sera nomm temp suivi de caractres alatoires. Cette fonction retourne une action I/O qui cre le fichier temporaire, et le rsultat de cette action est une paire : le nom du fichier cr et sa poigne. On pouvait simplement ouvrir un fichier todo2.txt ou quelque chose comme a, mais il vaut mieux ouvrir un fichier temporaire laide d openTempFile pour tre certain de ne pas en craser un autre. On na pas utilis getCurrentDirectory pour rcuprer le dossier courant avant de le passer openTempFile parce que . indique le rpertoire courant sous les systmes Unix ainsi que sous Windows. Puis, on lie le contenu de todo.txt au nom contents . Ensuite, on coupe cette chane de caractres en une liste de chanes de caractres, chacune contenant

une ligne. todoTasks est prsent sous la forme ["Iron the dishes", "Dust the dog", "Take salad out of the oven"] . On zippe les nombres 0 et suivants avec cette liste laide dune fonction qui prend un nombre, comme 3 , et une chane comme "hey" et retourne "3 - hey" , ainsi, numberedTasks est ["0 - Iron the dishes", "1 - Dust the dog" ... . On joint cette liste de chanes en une simple chane avec unlines et on affiche cette chane sur le terminal. Notez quon aurait pu faire mapM putStrLn numberedTasks ici. On demande ensuite lutilisateur laquelle il souhaite effacer. Mettons quil souhaite effacer la numro 1, Dust the dog , donc il tape 1 . numberString est prsent "1" , et puisquon veut un nombre, et pas une chane de caractres, on utilise read sur cela pour obtenir 1 et on le lie number . Souvenez-vous de delete et !! de Data.List . !! retourne llment dune liste lindice donn, et delete supprime la premire occurrence dun lment dans une liste et retourne une nouvelle liste sans cette occurrence. (todoTasks !! number) ( number tant 1 ) retourne "Dust the dog" . On lie todoTasks laquelle on a supprim la premire occurrence de "Dust the dog" newTodoItems , puis on joint cela en une seule chane avec unlines avant de lcrire dans notre fichier temporaire. Lancien fichier nest pas modifi et le fichier temporaire contient toutes les lignes de lancien, lexception de celle quon a efface. Aprs cela, on ferme les deux fichiers, on supprime lancien avec removeFile , qui, comme vous pouvez le voir, prend un chemin vers un fichier et le supprime. Aprs avoir supprim le vieux todo.txt, on utilise renameFile pour renommer le fichier temporaire en todo.txt. Faites attention, removeFile et renameFile (qui sont dans System.Directory ) prennent des chemins de fichiers, et pas des poignes. Et cest tout ! On aurait pu faire cela encore plus court, mais on a fait trs attention ne pas craser de fichier existant et demander poliment au systme dexploitation de nous dire o lon pouvait mettre notre fichier temporaire. Essayons prsent ! $ runhaskell deletetodo.hs These are your TO-DO items: 0 - Iron the dishes 1 - Dust the dog 2 - Take salad out of the oven Which one do you want to delete? 1 $ cat todo.txt Iron the dishes Take salad out of the oven $ runhaskell deletetodo.hs These are your TO-DO items: 0 - Iron the dishes 1 - Take salad out of the oven Which one do you want to delete? 0 $ cat todo.txt Take salad out of the oven

Arguments de ligne de commande


Grer les arguments de ligne de commande est plutt ncessaire si vous voulez faire un script ou une application lancer depuis le terminal. Heureusement, la bibliothque standard Haskell a un moyen trs sympa pour rcuprer les arguments de ligne de commande du programme. Dans la section prcdente, on a fait un programme qui ajoute un lment une liste de tches et un programme pour retirer une tche. Il y a deux problmes avec lapproche choisie. La premire, cest quon a cod en dur le nom du fichier. On a en quelque sorte dcid que le fichier serait nomm todo.txt et que lutilisateur naura jamais grer plus dune liste de tches. Un moyen de rsoudre ce problme consiste demander chaque fois quel fichier lutilisateur veut modifier lorsquil utilise nos programmes. On a utilis cette approche quand on a voulu savoir quel lment lutilisateur voulait supprimer. a marche, mais ce nest pas optimal, parce que cela requiert que lutilisateur lance le programme, attende que le programme lui demande quelque chose, et ensuite indique le fichier au programme. Ceci est appel un programme interactif, et le problme est le suivant : que faire si lon souhaite automatiser lexcution du programme, comme avec un script ? Il est plus dur de faire un script qui interagit avec un programme que de faire un script qui appelle un ou mme plusieurs programmes. Cest pourquoi il est parfois prfrable que lutilisateur dise au programme ce quil veut lorsquil lance le programme, plutt que le programme demande lutilisateur des choses une fois lanc. Et quel meilleur moyen pour lutilisateur dindiquer ce quil veut au programme que de donner des arguments en ligne de commande !

Le module System.Environment a deux actions I/O cool. Lune est getArgs , qui a pour type getArgs :: IO [String] et est une action I/O qui va rcuprer les arguments du programme et les encapsuler dans son rsultat sous forme dune liste. getProgName a pour type getProgName :: IO String et est une action I/O qui contient le nom du programme. Voici un petit programme qui dmontre leur fonctionnement : import System.Environment import Data.List main = do args <- getArgs progName <- getProgName putStrLn "The arguments are:" mapM putStrLn args putStrLn "The program name is:" putStrLn progName

On lie getArgs et getProgName args et progName . On dit The arguments are: et ensuite, pour chaque argument dans args , on applique putStrLn . Finalement, on affiche aussi le nom du programme. Compilons cela comme arg-test . $ ./arg-test first second w00t "multi word arg" The arguments are: first second w00t multi word arg The program name is: arg-test

Bien. Arm de cette connaissance, vous pouvez crer des applications en ligne de commande plutt cool. En fait, crons-en une ds maintenant. Dans la section prcdente, nous avions cr deux programmes spars pour ajouter des tches et en supprimer. Maintenant, on va faire cela en un seul programme, et ce quil fera dpendra des arguments. On va aussi le faire de manire ce quil puisse oprer sur diffrents fichiers, pas seulement todo.txt. On va lappeler todo ( faire) et il servira faire (haha !) trois choses diffrentes : Voir des tches Ajouter des tches Supprimer des tches On se fichera un peu des erreurs dentre pour linstant. Notre programme sera fait de faon ce que si lon souhaite ajouter la tche Find the magic sword of power au fichier todo.txt, on ait simplement faire todo add todo.txt "Find the magic sword of power" dans notre terminal. Pour voir les tches, on fera todo view todo.txt et pour supprimer la tche qui a pour indice 2, on fera todo remove todo.txt 2 . On va commencer par crer une liste associative de rsolution. Ce sera une simple liste associative qui aura pour cls des arguments de ligne de commande, et pour valeurs une fonction correspondante. Ces fonctions auront toute pour type [String] -> IO () . Elles prendront en paramtre la liste des arguments, et retourneront une action I/O qui fera laffichage, lajout, la suppression, etc. import import import import System.Environment System.Directory System.IO Data.List

dispatch :: [(String, [String] -> IO ())] dispatch = [ ("add", add) , ("view", view) , ("remove", remove) ]

Il nous reste encore dfinir main , add , view et remove , commenons par main : main = do (command:args) <- getArgs let (Just action) = lookup command dispatch action args

Dabord, on rcupre les arguments et on les lie (command:args) . Si vous vous souvenez de votre filtrage par motif, cela signifie que le premier argument est

li command et que le reste est li args . Si on appelle notre programme en faisant todo add todo.txt "Spank the monkey" , command sera "add" et args sera ["todo.xt", "Spank the monkey"] . la prochaine ligne, on cherche notre commande dans la liste de rsolution. Puisque "add" pointe vers add , on rcupre Just add en rsultat. On utilise nouveau un filtrage par motifs pour sortir notre fonction du Maybe . Que se passe-t-il si notre commande ne fait pas partie de la liste de rsolution ? Eh bien la rsolution retournera Nothing , mais comme on a dit quon ne se souciait pas trop de a, le filtrage va chouer et notre programme va lancer une exception. Finalement, on appelle notre fonction action avec le reste de la liste des arguments. Cela retourne une action I/O qui ajoute un lment, affiche les lments ou supprime un lment, et puisque cette action fait partie du bloc do de main , elle sera excute. Si on suit notre exemple concret, jusquici action vaut add , et sera appele avec args (donc ["todo.txt", "Spank the monkey"] ) et retournera une action I/O qui ajoute Spank the monkey todo.txt. Gnial ! Il ne reste plus qu implmenter add , view et remove . Commenons par add : add :: [String] -> IO () add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")

Si on appelle notre programme avec todo add todo.txt "Spank the monkey" , le "add" sera li command dans le premier filtrage du bloc main , alors que ["todo.txt", "Spank the monkey"] sera pass la fonction obtenue dans la liste de rsolution. Donc, puisquon ne se soucie pas des mauvaises entres, on filtre simplement cela contre une liste deux lments, et on retourne une action I/O qui ajoute cette ligne la fin du fichier, avec un caractre de retour la ligne. Ensuite, implmentons la fonctionnalit daffichage. Si on veut voir les lments dun fichier, on fait todo view todo.txt . Donc dans le premier filtrage par motif, command sera "view" et args sera ["todo.txt"] . view :: [String] -> IO () view [fileName] = do contents <- readFile fileName let todoTasks = lines contents numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks putStr $ unlines numberedTasks

On a dj fait peu prs la mme chose dans le programme qui neffaait les tches que quand on les avait affiches afin que lutilisateur en choisisse une supprimer, seulement ici, on ne fait quafficher. Et enfin, on va implmenter remove . Ce sera trs similaire au programme qui effaait une tche, donc si vous ne comprenez pas comment la suppression se passe ici, allez relire lexplication sous ce programme. La diffrence principale, cest quon ne code pas le nom todo.txt en dur mais quon lobtient en argument. Aussi, on ne demande pas lutilisateur de choisir un numro puisquon lobtient galement en argument. remove :: [String] -> IO () remove [fileName, numberString] = do handle <- openFile fileName ReadMode (tempName, tempHandle) <- openTempFile "." "temp" contents <- hGetContents handle let number = read numberString todoTasks = lines contents newTodoItems = delete (todoTasks !! number) todoTasks hPutStr tempHandle $ unlines newTodoItems hClose handle hClose tempHandle removeFile fileName renameFile tempName fileName

On a ouvert le fichier bas sur fileName et ouvert un fichier temporaire, supprim la ligne avec lindice donn par lutilisateur, crit cela dans le fichier temporaire, supprim le fichier original et renomm le fichier temporaire en fileName . Voil le programme final en entier, dans toute sa splendeur ! import import import import System.Environment System.Directory System.IO Data.List

dispatch :: [(String, [String] -> IO ())] dispatch = [ ("add", add) , ("view", view) , ("remove", remove) ] main = do

(command:args) <- getArgs let (Just action) = lookup command dispatch action args add :: [String] -> IO () add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n") view :: [String] -> IO () view [fileName] = do contents <- readFile fileName let todoTasks = lines contents numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks putStr $ unlines numberedTasks remove :: [String] -> IO () remove [fileName, numberString] = do handle <- openFile fileName ReadMode (tempName, tempHandle) <- openTempFile "." "temp" contents <- hGetContents handle let number = read numberString todoTasks = lines contents newTodoItems = delete (todoTasks !! number) todoTasks hPutStr tempHandle $ unlines newTodoItems hClose handle hClose tempHandle removeFile fileName renameFile tempName fileName

Pour rsumer notre solution : on a fait une liste associative de rsolution qui associe chaque commande une fonction qui prend des arguments de la ligne de commande et retourne une action I/O. On regarde ce quest la commande, et en fonction de a on rsout lappel sur la fonction approprie de la liste de rsolution. On appelle cette fonction avec le reste des arguments pour obtenir une action I/O qui fera laction attendue et ensuite on excute cette action ! Dans dautres langages, on aurait pu implmenter ceci avec un gros switch case ou ce genre de structure, mais utiliser des fonctions dordre suprieure nous permet de demander la liste de rsolution de nous donner une fonction, puis de demander cette fonction une action I/O correspondant nos arguments. Testons cette application ! $ 0 1 2 ./todo - Iron - Dust - Take view todo.txt the dishes the dog salad out of the oven

$ ./todo add todo.txt "Pick up children from drycleaners" $ 0 1 2 3 ./todo - Iron - Dust - Take - Pick view todo.txt the dishes the dog salad out of the oven up children from drycleaners

$ ./todo remove todo.txt 2 $ 0 1 2 ./todo - Iron - Dust - Pick view todo.txt the dishes the dog up children from drycleaners

Une autre chose cool propos de a, cest quil est trs facile dajouter de nouvelles fonctionnalits. Ajoutez simplement une entre dans la liste de rsolution et implmentez la fonction correspondante, les doigts dans le nez ! Comme exercice, vous pouvez implmenter la fonction bump qui prend un fichier et un numro de tche, et retourne une action I/O qui pousse cette tche en tte de la liste de tches. Vous pourriez aussi faire chouer ce programme plus gracieusement dans le cas dune entre mal forme (par exemple, si quelquun lance todo UP YOURS HAHAHAHA ) en faisant une action I/O qui rapporte une erreur (disons, errorExit :: IO () ), puis en vrifiant si lentre est errone, et le cas chant, en utilisant cette action. Un autre moyen est dutiliser les exceptions, quon va bientt rencontrer.

Alatoire
Souvent quand on programme, on a besoin de donnes alatoires. Par exemple, lorsque vous faites un jeu o un d doit tre lanc, ou quand vous voulez gnrer des entres pour tester votre programme. Des donnes alatoires peuvent avoir plein dutilisations en programmation. En fait, je devrais dire pseudo-alatoire, puisquon sait tous que la seule source de vrai alatoire est un singe

sur un monocycle tenant un fromage dans une main et ses fesses dans lautre. Dans cette section, on va voir comment Haskell gnre des donnes quasiment alatoires. Dans la plupart des langages de programmation, vous avez des fonctions qui vous donnent un nombre alatoire. Chaque fois que vous appelez cette fonction, vous obtenez (on lespre) un nombre alatoire diffrent. Quid dHaskell ? Eh bien, souvenez-vous, Haskell est un langage fonctionnel pur. Cela signifie quil a la proprit de transparence rfrentielle. Ce que CELA signifie, cest quune fonction laquelle on donne les mmes paramtres plusieurs fois retournera toujours le mme rsultat. Cest trs cool, parce que a nous permet de raisonner diffremment propos de nos programmes, et de retarder lvaluation jusqu ce quelle soit ncessaire. Si jappelle une fonction, je peux tre certain quelle ne va pas faire des choses folles avant de me donner son rsultat. Tout ce qui importe, cest son rsultat. Cependant, cela rend un peu difficile les choses dans le cas des nombres alatoires. Si jai une fonction comme a : randomNumber :: (Num a) => a randomNumber = 4

Ce nest pas trs utile comme fonction de nombre alatoire parce quelle retourne toujours 4 , bien que je puisse vous garantir que ce 4 est totalement alatoire puisque jai lanc un d pour le dterminer. Comment les autres langages font-ils des nombres apparemment alatoires ? Eh bien, ils prennent diffrentes informations de lordinateur, comme lheure actuelle, combien et comment vous avez dplac votre souris, quels genres de bruits vous faites devant votre ordinateur, et mlangent tout a pour vous donner un nombre qui a lair alatoire. La combinaison de ces facteurs (lalatoire) est probablement diffrente chaque instant, donc vous obtenez un nombre alatoire diffrent. Ah. Donc en Haskell, on peut faire un nombre alatoire en faisant une fonction qui prend en paramtre cet alatoire et, bas sur ceci, retourne un nombre (ou un autre type de donnes). Pntrez dans le module System.Random . Il a toutes les fonctions qui satisfont notre besoin dalatoire. Plongeons donc dans lune des fonctions quil exporte, jai nomm random . Voici son type : random :: (RandomGen g, Random a) => g -> (a, g) . Ouah ! Que de nouvelles classes de types dans cette dclaration ! La classe RandomGen est pour les types qui peuvent agir comme des sources dalatoire. La classe de types Random est pour les choses qui peuvent avoir une valeur alatoire. Un boolen peut prendre une valeur alatoire, soit True soit False . Un nombre peut prendre une plthore de diffrentes valeurs alatoires. Est-ce quune fonction peut prendre une valeur alatoire ? Je ne pense pas, probablement pas ! Si lon essaie de traduire la dclaration de random , cela donne quelque chose comme : elle prend un gnrateur alatoire (cest notre source dalatoire) et retourne une valeur alatoire et un nouveau gnrateur alatoire. Pourquoi retourne-t-elle un nouveau gnrateur en plus de la valeur alatoire ? Eh bien, on va voir a dans un instant. Pour utiliser notre fonction random , il nous faut obtenir un de ces gnrateurs alatoires. Le module System.Random exporte un type cool, StdGen , qui est une instance de la classe RandomGen . On peut soit crer notre StdGen nous-mme, soit demander au systme de nous en faire un bas sur une multitude de choses alatoires. Pour crer manuellement un gnrateur alatoire, utilisez la fonction mkStdGen . Elle a pour type mkStdGen :: Int -> StdGen . Elle prend un entier et en fonction de celui-ci, nous donne un gnrateur alatoire. Ok, essayons dutiliser random et mkStdGen en tandem pour obtenir un nombre (pas trs alatoire). ghci> random (mkStdGen 100)

<interactive>:1:0: Ambiguous type variable `a' in the constraint: `Random a' arising from a use of `random' at <interactive>:1:0-20 Probable fix: add a type signature that fixes these type variable(s)

De quoi ? Ah, oui, la fonction random peut retourner une valeur de nimporte quel type membre de la classe Random , on doit donc informer Haskell du type quon dsire. Noublions tout de mme pas quelle retourne une valeur alatoire et un gnrateur alatoire sous forme de paire. ghci> random (mkStdGen 100) :: (Int, StdGen) (-1352021624,651872571 1655838864)

Enfin ! Un nombre qui a lair un peu alatoire ! La premire composante du tuple est notre nombre, alors que la seconde est la reprsentation textuelle du nouveau gnrateur. Que se passe-t-il si lon appelle random nouveau, avec le mme gnrateur ? ghci> random (mkStdGen 100) :: (Int, StdGen) (-1352021624,651872571 1655838864)

Bien sr. Le mme rsultat pour les mmes paramtres. Essayons de donner un gnrateur alatoire diffrent comme paramtre. ghci> random (mkStdGen 949494) :: (Int, StdGen) (539963926,466647808 1655838864)

Bien, cool, super, un nombre diffrent. On peut utiliser les annotations de type pour obtenir diffrents types de cette fonction. ghci> random (mkStdGen 949488) :: (Float, StdGen) (0.8938442,1597344447 1655838864) ghci> random (mkStdGen 949488) :: (Bool, StdGen) (False,1485632275 40692) ghci> random (mkStdGen 949488) :: (Integer, StdGen) (1691547873,1597344447 1655838864)

Crons une fonction qui simule trois lancers de pice. Si random ne retournait pas un nouveau gnrateur avec la valeur alatoire, on devrait donner cette fonction trois gnrateurs alatoires en paramtre, et retourner le jet de pice pour chacun des trois. Mais a semble mauvais, parce que si un gnrateur peut retourner une valeur alatoire de type Int (qui peut prendre normment de valeurs), il devrait pouvoir renvoyer un jet de trois pices (qui ne peut prendre que huit valeurs prcisment). Cest ici que le fait que random retourne un nouveau gnrateur savre trs utile. On va reprsenter une pice avec un simple Bool . True pour pile, False pour face. threeCoins :: StdGen -> (Bool, Bool, Bool) threeCoins gen = let (firstCoin, newGen) = random gen (secondCoin, newGen') = random newGen (thirdCoin, newGen'') = random newGen' in (firstCoin, secondCoin, thirdCoin)

On appelle random avec le gnrateur pass en paramtre pour obtenir un jet et un nouveau gnrateur. On lappelle nouveau avec le nouveau gnrateur, pour obtenir un deuxime jet. On fait de mme pour le troisime. Si on avait appel chaque fois avec le mme gnrateur, toutes les pices auraient eu la mme valeur, et on aurait seulement pu avoir (False, False, False) ou (True, True, True) comme rsultat. ghci> threeCoins (mkStdGen (True,True,True) ghci> threeCoins (mkStdGen (True,False,True) ghci> threeCoins (mkStdGen (True,False,True) ghci> threeCoins (mkStdGen (True,True,True) 21) 22) 943) 944)

Remarquez que lon na pas eu faire random gen :: (Bool, StdGen) . Cest parce que nous avions dj spcifi que lon voulait des boolens dans la dclaration de type de la fonction. Cest pourquoi Haskell peut infrer quon veut une valeur boolenne dans ce cas. Et si lon veut lancer quatre pices ? Ou cinq ? Eh bien, il y a une fonction randoms qui prend un gnrateur, et retourne une liste infinie de valeurs bases sur ce gnrateur. ghci> take 5 $ randoms (mkStdGen 11) :: [Int] [-1807975507,545074951,-1015194702,-1622477312,-502893664] ghci> take 5 $ randoms (mkStdGen 11) :: [Bool] [True,True,True,True,False] ghci> take 5 $ randoms (mkStdGen 11) :: [Float] [7.904789e-2,0.62691015,0.26363158,0.12223756,0.38291094]

Pourquoi randoms ne retourne pas de nouveau gnrateur avec la liste ? On peut implmenter randoms trs facilement ainsi : randoms' :: (RandomGen g, Random a) => g -> [a] randoms' gen = let (value, newGen) = random gen in value:randoms' newGen

Une dfinition rcursive. On obtient une valeur alatoire et un nouveau gnrateur partir du gnrateur courant, et on cre une liste qui contient la valeur alatoire en tte, et une liste de nombres alatoires obtenue par le nouveau gnrateur en queue. Puisquon doit potentiellement gnrer une liste infinie de nombres, on ne peut pas renvoyer le nouveau gnrateur. On pourrait faire une fonction qui gnre un flot fini de nombres alatoires et un nouveau gnrateur ainsi : finiteRandoms :: (RandomGen g, Random a, Num n) => n -> g -> ([a], g) finiteRandoms 0 gen = ([], gen)

finiteRandoms n gen = let (value, newGen) = random gen (restOfList, finalGen) = finiteRandoms (n-1) newGen in (value:restOfList, finalGen)

Encore, une dfinition rcursive. On dit que si lon ne veut aucun nombre, on retourne la liste vide et le gnrateur quon nous a donn. Pour tout autre nombre de valeurs alatoires, on rcupre une premire valeur alatoire et un nouveau gnrateur. Ce sera la tte. Et on dit que la queue sera compose de n - 1 nombres gnrs partir du nouveau gnrateur. Enfin, on renvoie la tte jointe la queue, ainsi que le gnrateur obtenu par lappel rcursif. Et si lon voulait une valeur alatoire dans un certain intervalle ? Tous les entiers alatoires rencontrs jusqu prsent taient outrageusement grands ou petits. Si lon voulait lancer un d ? On utilise randomR cet effet. Elle a pour type randomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g) , signifiant quelle est comme random , mais prend en premier paramtre une paire de valeurs qui dfinissent une borne infrieure et une borne suprieure pour la valeur produite. ghci> randomR (6,1494289578 ghci> randomR (3,1250031057 (1,6) (mkStdGen 359353) 40692) (1,6) (mkStdGen 35935335) 40692)

Il y a aussi randomRs , qui produit un flot de valeurs alatoires dans lintervalle spcifi. Regardez a : ghci> take 10 $ randomRs ('a','z') (mkStdGen 3) :: [Char] "ndkxbvmomg"

Super, on dirait un mot de passe ultra secret ou quelque chose comme a. Vous vous demandez peut-tre ce que cette section vient faire dans le chapitre sur les entres-sorties ? On na pas fait dI/O jusquici. Eh bien, jusquici, on a cr notre gnrateur manuellement avec un entier arbitraire. Le problme si lon fait a dans nos vrais programmes, cest quils retourneront toujours les mmes suites de nombres alatoires, ce qui ne nous convient pas. Cest pourquoi System.Random offre laction I/O getStdGen , qui a pour type IO StdGen . Lorsque votre programme dbute, il demande au systme un bon gnrateur alatoire et le stocke comme un gnrateur global. getStdGen vous rcupre ce gnrateur lorsque vous le liez un nom. Voici un simple programme qui gnre une chane alatoire. import System.Random main = do gen <- getStdGen putStr $ take 20 (randomRs ('a','z') gen)

$ runhaskell random_string.hs pybphhzzhuepknbykxhe $ runhaskell random_string.hs eiqgcxykivpudlsvvjpg $ runhaskell random_string.hs nzdceoconysdgcyqjruo $ runhaskell random_string.hs bakzhnnuzrkgvesqplrx

Attention cependant, faire getStdGen deux fois vous donnera le mme gnrateur global deux fois. Donc si vous faites : import System.Random main = do gen <- getStdGen putStrLn $ take 20 (randomRs ('a','z') gen) gen2 <- getStdGen putStr $ take 20 (randomRs ('a','z') gen2)

vous obtiendrez la mme chane deux fois ! Un moyen dobtenir deux chanes de longueur 20 est de mettre en place un flot infini de caractres, de prendre les 20 premiers, les afficher sur une ligne, puis prendre les 20 suivants et les afficher sur une seconde ligne. Pour cela, on peut utiliser splitAt de Data.List , qui coupe une liste un indice donn et retourne le tuple form par la partie coupe en premire composante, et le reste en seconde composante. import System.Random import Data.List main = do

gen <- getStdGen let randomChars = randomRs ('a','z') gen (first20, rest) = splitAt 20 randomChars (second20, _) = splitAt 20 rest putStrLn first20 putStr second20

Un autre moyen est dutiliser laction newStdGen , qui coupe notre gnrateur courant en deux gnrateurs. Elle remplace le gnrateur global par lun deux, et encapsule lautre comme son rsultat. import System.Random main = do gen <- getStdGen putStrLn $ take 20 (randomRs ('a','z') gen) gen' <- newStdGen putStr $ take 20 (randomRs ('a','z') gen')

Non seulement on obtient un nouveau gnrateur quand on lie newStdGen un nom, mais en plus le gnrateur global est chang, donc si on fait getStdGen nouveau et quon le lie un nom, on obtiendra un gnrateur diffrent de gen . Voici un petit programme qui fait deviner lutilisateur le numro auquel il pense. import System.Random import Control.Monad(when) main = do gen <- getStdGen askForNumber gen askForNumber :: StdGen -> IO () askForNumber gen = do let (randNumber, newGen) = randomR (1,10) gen :: (Int, StdGen) putStr "Which number in the range from 1 to 10 am I thinking of? " numberString <- getLine when (not $ null numberString) $ do let number = read numberString if randNumber == number then putStrLn "You are correct!" else putStrLn $ "Sorry, it was " ++ show randNumber askForNumber newGen

On cre une fonction askForNumber , qui prend un gnrateur alatoire et retourne une action I/O qui demande un nombre lutilisateur et lui dit sil a bien devin. Dans cette fonction, on gnre dabord un nombre alatoire et un nouveau gnrateur bass sur le gnrateur obtenu en paramtre, et on les nomme respectivement randNumber et newGen . Disons que le nombre gnr tait 7 . On demande ensuite lutilisateur de deviner quel nombre on pense. On fait getLine et lie le rsultat numberString . Quand lutilisateur tape 7 , numberString devient "7" . Ensuite, on utilise when pour vrifier si la chane entre par lutilisateur est vide. Si elle lest, une action I/O vide return () est excute, qui termine le programme. Sinon, laction combine de ce bloc do est effectue. On utilise read sur numberString pour la convertir en nombre, donc number est 7 .

Excusez-moi ! Si lutilisateur nous donne ici une entre que read ne peut pas lire (comme "haha" ), notre programme va planter avec un message derreur horrible. Si vous ne voulez pas que votre programme plante sur une entre errone, utilisez reads , qui retourne une liste vide quand elle narrive pas lire la chane. Quand elle y parvient, elle retourne une liste singleton avec notre valeur en premire composante, et le reste de la chane quelle na pas consomm dans lautre composante.

On vrifie si le nombre quon a entr est gal celui gnr alatoirement et on donne lutilisateur le message appropri. Puis on appelle askForNumber rcursivement, seulement avec le nouveau gnrateur quon a obtenu, ce qui nous donne une nouvelle action I/O qui sera comme celle quon vient dexcuter, mais dpendra dun gnrateur diffrent. main consiste en la rcupration dun gnrateur alatoire du systme, suivie dun appel askForNumber avec celui-ci pour obtenir laction initiale. Voici notre programme en action ! $ runhaskell guess_the_number.hs

Which number in the Sorry, it was 3 Which number in the You are correct! Which number in the Sorry, it was 4 Which number in the Sorry, it was 10 Which number in the

range from 1 to 10 am I thinking of? 4 range from 1 to 10 am I thinking of? 10 range from 1 to 10 am I thinking of? 2 range from 1 to 10 am I thinking of? 5 range from 1 to 10 am I thinking of?

Une autre manire dcrire le mme programme est comme suit : import System.Random import Control.Monad(when) main = do gen <- getStdGen let (randNumber, _) = randomR (1,10) gen :: (Int, StdGen) putStr "Which number in the range from 1 to 10 am I thinking of? " numberString <- getLine when (not $ null numberString) $ do let number = read numberString if randNumber == number then putStrLn "You are correct!" else putStrLn $ "Sorry, it was " ++ show randNumber newStdGen main

Cest trs similaire la version prcdente, mais plutt que de faire une fonction qui prend un gnrateur et sappelle rcursivement avec un nouveau gnrateur, on fait tout le travail dans main . Aprs avoir dit lutilisateur sil a correctement devin ou pas, on met jour le gnrateur global et on appelle main nouveau. Les deux approches sont valides, mais je prfre la premire parce quelle fait moins de choses dans main et nous offre une fonction facilement rutilisable.

Chanes doctets
Les listes sont des structures de donnes cool et utiles. Jusquici, on les a utilises peu prs partout. Il y a une multitude de fonctions oprant sur elles et la paresse dHaskell nous permet dchanger les boucles for et while des autres langages contre des filtrages et des mappages sur des listes, parce que lvaluation naura lieu que lorsque cela sera ncessaire, donc des choses comme des listes infinies (et mme des listes infinies de listes infinies !) ne nous posent pas de problme. Cest pourquoi les listes peuvent tre utilises pour reprsenter les flots, que ce soit en lecture depuis lentre standard ou un fichier. On peut juste ouvrir un fichier, et le lire comme une chane de caractres, alors quen ralit il ne sera accd que quand ce sera ncessaire. Cependant, traiter les fichiers comme des listes a un inconvnient : a a tendance tre assez lent. Comme vous le savez, String est un synonyme de type pour [Char] . Les Char nont pas une taille fixe, parce quil faut plusieurs octets pour reprsenter un caractre, par exemple Unicode. De plus, les listes sont vraiment paresseuses. Si vous avez une liste comme [1, 2, 3, 4] , elle ne sera value que lorsque ce sera vraiment ncessaire. La liste entire est une promesse de liste. Rappelez-vous que [1, 2, 3, 4] est un sucre syntaxique pour 1:2:3:4:[] . Lorsque le premier lment de la liste est forc tre valu (par exemple, en laffichant), le reste de la liste 2:3:4:[] est toujours une promesse de liste, et ainsi de suite. Vous pouvez donc imaginer les listes comme des promesses que llment suivant sera dlivr quand on en aura besoin, ainsi que la promesse que la suite fera pareil. Il ne faut pas se creuser lesprit bien longtemps pour se dire que traiter une simple liste de nombres comme une srie de promesses nest peut-tre pas la manire la plus efficace au monde. Ce cot supplmentaire ne nous drange pas la plupart du temps, mais il savre handicapant lorsque lon lit et manipule des gros fichiers. Cest pourquoi Haskell a des chanes doctets. Les chanes doctets sont un peu comme des listes, seulement chaque lment fait un octet (ou 8 bits) de taille. La manire dont elles sont paresseuses est aussi diffrente. Les chanes doctets viennent sous deux dclinaisons : les strictes et les paresseuses. Les chanes doctets strictes rsident dans Data.ByteString et elles abandonnent compltement la paresse. Plus de promesses impliques, une chane doctets stricte est une srie doctets dans un tableau. Vous ne pouvez pas avoir de chane doctets stricte infinie. Si vous valuez le premier octet dune chane stricte, elle est value en entier. Le bon ct des choses, cest quil y a moins de cot supplmentaire puisquil ny a plus de glaons (le terme technique pour les promesses) impliqus. Le mauvais ct, cest quelles risquent plus de remplir votre mmoire, parce quelles sont lues entirement dans la mmoire dun coup. Lautre varit de chane doctets rside dans Data.ByteString.Lazy . Elles sont paresseuses, mais pas autant que les listes. Comme on la dit plus tt, il y a autant de glaons dans une liste que dlments dans la liste. Cest ce qui les rend un peu lentes pour certaines oprations. Les chanes doctets paresseuses

prennent une approche diffrente - elles sont stockes dans des morceaux ( ne pas confondre avec des glaons !), chaque morceau ayant une taille de 64K. Donc, lorsque vous valuez un octet dune chane doctets paresseuse (en laffichant ou autre), les premiers 64K sont valus. Aprs cela, cest juste une promesse pour le reste des morceaux. Les chanes doctets sont un peu comme des listes de chanes doctets strictes de taille 64K. Quand vous traitez un fichier avec des chanes doctets paresseuses, il sera lu morceau par morceau. Cest cool parce que cela ne causera pas une monte en flche de lutilisation mmoire et les 64K tiennent probablement correctement dans le cache L2 de votre CPU. Si vous lisez la documentation de Data.ByteString.Lazy , vous verrez quil a beaucoup de fonctions qui ont les mmes noms que celles de Data.List , seulement les signatures de type ont ByteString au lieu de [a] et Word8 au lieu de a . Les fonctions ayant le mme nom fonctionnent principalement identiquement celles sur les listes. Puisque les noms sont identiques, on va faire un import qualifi dans un script et charger ce script dans GHCi pour jouer avec les chanes doctets. import qualified Data.ByteString.Lazy as B import qualified Data.ByteString as S

B contient les chanes doctets paresseuses, alors que S contient les strictes. On utilisera principalement les paresseuses. La fonction pack a pour signature de type pack :: [Word8] -> ByteString . Cela signifie quelle prend une liste doctets de type Word8 et retourne une ByteString . Vous pouvez limaginer comme prenant une liste, qui est paresseuse, et la rendant moins paresseuse, cest--dire seulement paresseuse chaque intervalle de 64K. Cest quoi ce type Word8 au fait ? Eh bien, cest comme Int , mais avec un plus petit intervalle de valeurs, cest dire 0 255. Cela reprsente un nombre sur 8 bits. Tout comme Int , il est dans la classe de types Num . Par exemple, on sait que la valeur 5 est polymorphe et peut se faire passer pour nimporte quel type numrique. Eh bien elle peut avoir pour type Word8 . ghci> Chunk ghci> Chunk B.pack [99,97,110] "can" Empty B.pack [98..120] "bcdefghijklmnopqrstuvwx" Empty

Comme vous le voyez, vous navez gnralement pas trop vous soucier de Word8 , parce que le systme de types peut faire choisir aux nombres ce type. Si vous essayez un nombre trop gros, comme 336 , il sera juste modul 80 . On a juste plac une poigne de valeurs dans une ByteString , donc elles tenaient dans un seul morceau. Empty est comme [] des listes. unpack est la fonction inverse de pack . Elle prend une chane doctets et la transforme en liste doctets. fromChunks prend une liste de chanes doctets strictes et la convertit en une chane doctets paresseuse. toChunks prend une chane doctets paresseuse et la convertit en une liste de chanes doctets strictes. ghci> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]] Chunk "()*" (Chunk "+,-" (Chunk "./0" Empty))

Cest bien lorsque vous avez beaucoup de petites chanes doctets strictes et que vous voulez les traiter efficacement sans les joindre en une grosse chane stricte en mmoire. La version chane doctets de : est appele cons . Elle prend un octet et une chane doctets et place loctet au dbut de la chane. Ceci est paresseux, donc elle va crer un nouveau morceau, mme si le morceau prcdent ntait pas rempli. Cest pourquoi il vaut mieux utiliser la version stricte de cons , cons' si vous voulez insrez plein doctets au dbut dune chane doctets. ghci> B.cons 85 $ B.pack [80,81,82,84] Chunk "U" (Chunk "PQRT" Empty) ghci> B.cons' 85 $ B.pack [80,81,82,84] Chunk "UPQRT" Empty ghci> foldr B.cons B.empty [50..60] Chunk "2" (Chunk "3" (Chunk "4" (Chunk "5" (Chunk "6" (Chunk "7" (Chunk "8" (Chunk "9" (Chunk ":" (Chunk ";" (Chunk "<" Empty)))))))))) ghci> foldr B.cons' B.empty [50..60] Chunk "23456789:;<" Empty

Comme vous pouvez le constater, empty cre une chane doctets vide. Vous voyez la diffrence entre cons et cons' ? Avec foldr , on est parti dune chane doctets vide, et on a ajout tous les nombres dune liste en partant de la droite au dbut de cette chane. Quand on a utilis cons , on sest retrouv avec un morceau pour chaque octet, ce qui dtruit lutilit de la chane. Autrement, les modules pour les chanes doctets ont une pellete de fonctions analogues celles de Data.List , incluant, mais non limites , head , tail , init , null , length , map , reverse , foldl , foldr , concat , takeWhile , filter , etc.

Ils ont aussi des fonctions qui ont le mme nom que certaines des fonctions de System.IO , mais avec ByteString la place de String . Par exemple, readFile de System.IO a pour type readFile :: FilePath -> IO String , alors que readFile des modules de chanes doctets a pour type readFile :: FilePath -> IO ByteString . Attention, si vous utilisez des chanes doctets strictes et que vous essayez de lire un fichier, il sera lu en entier en mmoire dun coup ! Avec des chanes doctets paresseuses, il sera lu par morceaux. Crons un simple programme qui prend deux noms de fichiers en arguments et copie le premier fichier dans le second. Notez que System.Directory a dj une fonction copyFile , mais on va implmenter notre propre fonction de copie pour ce programme de toute faon. import System.Environment import qualified Data.ByteString.Lazy as B main = do (fileName1:fileName2:_) <- getArgs copyFile fileName1 fileName2 copyFile :: FilePath -> FilePath -> IO () copyFile source dest = do contents <- B.readFile source B.writeFile dest contents

On cre notre propre fonction qui prend deux FilePath (rappelez-vous, FilePath est un synonyme de String ) et retourne une action I/O qui copie un fichier sur lautre en utilisant des chanes doctets. Dans la fonction main , on rcupre seulement les arguments et on appelle notre fonction avec ceux-ci pour obtenir laction I/O, quon excute ensuite. $ runhaskell bytestringcopy.hs something.txt ../../something.txt

Remarquez quun programme qui nutiliserait pas de chane doctets paresseuses ressemblerait exactement celui-ci, part le fait quon ait utilis B.readFile et B.writeFile au lieu de readFile et writeFile . Beaucoup de fois, vous pouvez convertir un programme qui utilise des chanes de caractres normales en un programme qui utilise des chanes doctets en faisant les imports ncessaires et en qualifiant les bonnes fonctions par le nom des modules appropris. Parfois, vous devez tout de mme convertir certaines fonctions que vous avez crites pour quelle fonctionne sur des chanes doctets, mais cest assez facile. Chaque fois que vous avez besoin de meilleures performances dans un programme qui lit beaucoup de donnes, essayez les chanes doctets, il se peut que vous obteniez de bons gains de performance sans trop defforts de votre part. Jcris gnralement mes programmes avec des chanes de caractres, et je les convertis en chanes doctets si les performances sont insatisfaisantes.

Exceptions
Tous les langages ont des procdures, des fonctions et des bouts de code qui peuvent chouer dune certaine faon. Cest la vie. Diffrents langages ont diffrentes manires de grer ces erreurs. En C, on utilise gnralement une valeur anormale (comme -1 ou un pointeur nul) pour indiquer que ce que la fonction a retourn ne devrait pas tre trait comme une valeur normale. Java et C#, dun autre ct, tendent utiliser les exceptions pour grer les checs. Quand une exception est leve, le flot de contrle saute jusqu un code quon a dfini pour nettoyer un peu et possiblement lever une autre exception de manire ce quun autre gestionnaire dexceptions soccupe dautre chose. Haskell a un trs bon systme de types. Les types de donnes algbriques permettent dutiliser des types comme Maybe ou Either et des valeurs de ces types pour reprsenter des choses qui peuvent tre prsentes ou non. En C, retourner, disons, -1 en cas dchec est un problme de convention. Cette valeur nest spciale que pour nous humains, et si lon ne fait pas attention, on pourrait la traiter par erreur comme une valeur normale, et provoquer le chaos et le dsarroi dans notre code. Le systme de types dHaskell nous offre une sret bien ncessaire sur cet aspect. Une fonction qui a pour type a -> Maybe b indique clairement quelle peut produire un b envelopp dans un Just ou retourner Nothing . Le type est diffrent de a -> b , et si on essaie de remplacer lune par lautre, le compilateur se plaindra. Bien quil ait un systme de types expressif qui supporte les checs de calculs, Haskell supporte quand mme les exceptions, parce quelles sont plus senses dans un contexte dI/O. Beaucoup de choses peuvent mal tourner quand on traite avec le monde extrieur, car il est trs imprvisible. Par exemple, quand on ouvre un fichier, beaucoup de choses peuvent mal se passer. Le fichier peut tre verrouill, ne pas tre l, voire le disque dur lui-mme peut ne pas tre l. Ainsi, il est pratique de sauter une partie du code qui soccupe de grer cela quand une telle erreur a lieu. Ok, donc le code I/O (i.e. le code impur) peut lever des exceptions. Cest sens. Mais quen est-il du code pur ? Eh bien, il peut aussi lever des exceptions. Pensez div ou head . Elles ont respectivement pour type (Integral a) => a -> a -> a et [a] -> a . Pas de Maybe ou d Either dans leur type de retour, et pourtant elles peuvent toute deux chouer ! div vous explose au visage lorsque vous essayez de diviser par zro et head pique une crise de colre lorsquon lui donne une liste vide.

ghci> 4 `div` 0 *** Exception: divide by zero ghci> head [] *** Exception: Prelude.head: empty list

Du code pur peut lancer des exceptions, mais elles ne peuvent tre attrapes que dans du code impur (dans un bloc do sous main ). Cest parce que lon ne sait jamais quand (ou si) quelque chose sera valu dans du code pur, puisquil est paresseux et na pas dordre dexcution spcifi, contrairement au code dentre-sortie. Plus tt, on a parl de passer le moins de temps possible de notre programme dans les entres-sorties. La logique de notre programme doit rsider principalement dans nos fonctions pures, parce que leur rsultat ne dpend que des paramtres avec lesquelles elles sont appeles. Quand vous manipulez des fonctions pures, vous navez qu penser ce quelles retournent, parce quelles ne peuvent rien faire dautre. Bien quun peu de logique dans les I/O soit ncessaire (pour ouvrir des fichiers par exemple), elle devrait prfrablement tre restreinte au minimum. Les fonctions pures sont paresseuses par dfaut, ce qui veut dire quon ne sait pas quand elles seront values et que cela ne doit pas importer. Cependant, ds que des fonctions pures se mettent lancer des exceptions, leur ordre dvaluation devient important. Cest pourquoi on ne peut attraper ces exceptions que dans la partie impure de notre code. Et cest mauvais, parce que lon veut garder cette partie aussi petite que possible. Cependant, si on nattrape pas ces erreurs, notre programme plante. La solution ? Ne pas mlanger les exceptions et le code pur. Tirez profit du puissant systme de types dHaskell et utilisez des types comme Either et Maybe pour reprsenter des rsultats pouvant chouer. Cest pourquoi nous nallons regarder que les exceptions dI/O pour linstant. Les exceptions dI/O sont des exceptions causes par quelque chose se passant mal lorsquon communique avec le monde extrieur dans une action I/O qui fait partie de notre main . Par exemple, on peut tenter douvrir un fichier, puit sapercevoir quil a t supprim, ou quelque chose comme a. Regardez ce programme qui ouvre un fichier dont le nom lui est donn en argument et nous dit combien de lignes le fichier contient. import System.Environment import System.IO main = do (fileName:_) <- getArgs contents <- readFile fileName putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!"

Un programme trs simple. On effectue laction I/O getArgs et lie la premire chane de la liste retourne fileName . Puis on appelle contents le contenu du fichier qui porte ce nom. Finalement, on applique lines sur ce contenu pour obtenir une liste de lignes, et on rcupre la longueur de cette liste et on la donne show pour obtenir une reprsentation textuelle de ce nombre. Cela fonctionne comme prvu, mais que se passe-t-il lorsquon donne le nom dun fichier inexistant ? $ runhaskell linecount.hs i_dont_exist.txt linecount.hs: i_dont_exist.txt: openFile: does not exist (No such file or directory)

Aha, on obtient une erreur de GHC, nous disant que le fichier nexiste pas. Notre programme plante. Et si lon voulait afficher un message plus joli quand le fichier nexiste pas ? Un moyen de faire cela est de vrifier lexistence du fichier laide de doesFileExist de System.Directory . import System.Environment import System.IO import System.Directory main = do (fileName:_) <- getArgs fileExists <- doesFileExist fileName if fileExists then do contents <- readFile fileName putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!" else do putStrLn "The file doesn't exist!"

On a fait fileExists <- doesFileExist fileName parce que doesFileExist a pour type doesFileExist :: FilePath -> IO Bool , ce qui signifie quelle retourne une action I/O qui a pour rsultat une valeur boolenne nous indiquant si le fichier existe. On ne peut pas utiliser simplement doesFileExist dans une expression if directement. Une autre solution ici serait dutiliser des exceptions. Cest parfaitement acceptable dans ce contexte. Un fichier inexistant est une exception qui est leve par une I/O, donc lattraper dans une I/O est propre et correct. Pour grer ceci en utilisant des exceptions, on va tirer parti de la fonction catch de System.IO.Error . Son type est catch :: IO a -> (IOError -> IO a) -> IO a . Elle prend deux paramtres. Le premier est une action I/O. Par exemple, a pourrait tre une action I/O qui

essaie douvrir un fichier. Le second est le gestionnaire dexceptions. Si laction I/O passe en premier paramtre catch lve une exception I/O, cette exception sera passe au gestionnaire, qui dcidera alors quoi faire. Le rsultat final est une action I/O qui se comportera soit comme son premier paramtre, ou bien excutera le gestionnaire en fonction de lexception leve par la premire action I/O. Si vous tes familier avec les blocs try-catch de langages comme Java ou Python, la fonction catch est similaire. Le premier paramtre est la chose essayer, un peu comme ce quon met dans le bloc try dans dautres langages impratifs. Le second paramtre est le gestionnaire dexceptions, un peu comme la plupart des blocs catch qui reoivent des exceptions que vous pouvez examiner pour savoir ce qui sest mal pass. Le gestionnaire est invoqu lorsquune exception est leve. Le gestionnaire prend une valeur de type IOError , qui est une valeur signifiant que lexception qui a eu lieu tait lie une I/O. Elle contient aussi des informations sur le type de lexception leve. La faon dont ce type est implment dpend de limplmentation du langage, donc on ne peut pas inspecter les valeurs de type IOError par filtrage par motif, tout comme on ne peut pas filtrer par motif les valeurs de type IO something . On peut tout de mme utiliser tout un tas de prdicats utiles pour savoir des choses propos de valeurs de type IOError comme nous le verrons dans une seconde. Mettons notre nouvelle amie catch lessai ! import System.Environment import System.IO import System.IO.Error main = toTry `catch` handler toTry :: IO () toTry = do (fileName:_) <- getArgs contents <- readFile fileName putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!" handler :: IOError -> IO () handler e = putStrLn "Whoops, had some trouble!"

Tout dabord, vous verrez quon a mis des apostrophes renverses autour de catch pour lutiliser de manire infixe, parce quelle prend deux paramtres. Lutiliser de manire infixe la rend plus lisible. Donc, toTry `catch` handler est la mme chose que catch toTry handler , qui correspond bien son type. toTry est une action I/O quon essaie dexcuter, et handler est la fonction qui prend une IOError et retourne une action excuter en cas dexception. Essayons : $ runhaskell count_lines.hs i_exist.txt The file has 3 lines! $ runhaskell count_lines.hs i_dont_exist.txt Whoops, had some trouble!

Dans le gestionnaire, nous navons pas vrifi de quel type d IOError il sagissait. On a juste renvoy "Whoops, had some trouble!" quelque que soit le type derreur. Attraper tous les types derreur dans un seul gestionnaire est une mauvaise pratique en Haskell tout comme dans la plupart des autres langages. Et si une exception arrivait que lon ne dsirait pas attraper, comme une interruption du programme par lutilisateur ? Cest pour cela quon va faire comme dans la plupart des autres langages : on va vrifier de quel type dexception il sagit. Si cest celui quon attendait, on la traite. Sinon, on la lve nouveau dans la nature. Modifions notre programme pour nattraper que les exceptions lies linexistence du fichier. import System.Environment import System.IO import System.IO.Error main = toTry `catch` handler toTry :: IO () toTry = do (fileName:_) <- getArgs contents <- readFile fileName putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!" handler :: IOError -> IO () handler e | isDoesNotExistError e = putStrLn "The file doesn't exist!" | otherwise = ioError e

Tout reste pareil part le gestionnaire, quon a modifi pour nattraper quun certain groupe dexceptions I/O. Ici, on a utilis deux nouvelles fonctions de

System.IO.Error - isDoesNotExistError et ioError . isDoesNotExistError est un prdicat sur les IOError , ce qui signifie que cest une fonction prenant une IOError et retournant True ou False , ayant donc pour type isDoesNotExistError :: IOError -> Bool . On utilise cette fonction sur lexception que notre gestionnaire reoit pour savoir si cest une erreur cause par linexistence du fichier. On utilise la syntaxe des gardes ici, mais on aurait aussi pu utiliser un if else. Si ce ntait pas caus par un fichier inexistant, on lve nouveau lexception passe au gestionnaire laide de la fonction ioError . Elle a pour type ioError :: IOError -> IO a , donc elle prend une IOError et produit une action I/O qui va lever cette exception. Laction I/O a pour type IO a , parce quelle ne retourne jamais de rsultat, donc elle peut se faire passer pour une IO anything . Donc, si lexception leve dans laction I/O toTry quon a colle avec un bloc do nest pas cause par linexistence du fichier, toTry `catch` handler va attraper cette exception et la lever nouveau. Plutt cool, hein ? Il y a plusieurs prdicats qui agissent sur des IOError , et lorsquune garde nest pas value comme True , lvaluation passe la prochaine garde. Les prdicats sur les IOError sont :

isAlreadyExistsError isDoesNotExistError isAlreadyInUseError isFullError isEOFError isIllegalOperation isPermissionError isUserError La plupart dentre eux sont vidents. isUserError svalue True quand on utilise la fonction userError pour lever lexception, qui sert utiliser nos propres exceptions en les accompagnant dune chane de caractres. Par exemple, vous pouvez faire ioError $ userError "remote computer unplugged!" , bien quil soit prfrable dutiliser des types comme Either et Maybe pour exprimer des checs plutt que de lancer vous-mme des exceptions avec userError . Vous pourriez ainsi avoir un gestionnaire de la sorte : handler :: IOError -> IO () handler e | isDoesNotExistError e = putStrLn "The file doesn't exist!" | isFullError e = freeSomeSpace | isIllegalOperation e = notifyCops | otherwise = ioError e

O notifyCops et freeSomeSpace sont des actions I/O que vous dfinissez. Soyez certain de lever nouveau les exceptions si elles ne correspondent pas vos critres, sinon votre programme chouera silencieusement l o il ne devrait pas. System.IO.Error exporte aussi des fonctions qui nous premettent de demander nos exceptions certains de leurs attributs, comme la poigne de fichier qui a caus lerreur, ou le nom du fichier. Elles commencent par ioe et vous pouvez trouver la liste complte dans la documentation. Mettons quon veuille afficher le nom du fichier responsable de lerreur. On ne peut pas afficher le fileName quon a reu de getArgs , parce que seule l IOError est passe au gestionnaire, et ce dernier ne connat rien dautre. Une fonction ne dpend que des paramtres avec lesquels elle a t appele. Cest pourquoi on peut utiliser la fonction ioeGetFileName , qui a pour type ioeGetFileName :: IOError -> Maybe FilePath . Elle prend une IOError en paramtre et retourne ventuellement un FilePath (qui est un synonyme de String , souvenez-vous en). En gros, elle extrait le chemin du fichier de l IOError , si elle le peut. Modifions notre programme pour afficher le chemin du fichier responsable de lexception. import System.Environment import System.IO import System.IO.Error main = toTry `catch` handler toTry :: IO () toTry = do (fileName:_) <- getArgs contents <- readFile fileName putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!" handler :: IOError -> IO () handler e | isDoesNotExistError e = case ioeGetFileName e of Just path -> putStrLn $ "Whoops! File does not exist at: " ++ path Nothing -> putStrLn "Whoops! File does not exist at unknown location!" | otherwise = ioError e

Dans la garde o isDoesNotExistError est True , on a utilis une expression case pour appeler ioeGetFileName avec e , puis on a filtr par motif contre la

valeur Maybe retourne. Utiliser une expression case se fait gnralement pour filtrer par motif sur quelque chose sans introduire une nouvelle fonction. Vous navez pas utiliser un unique gestionnaire pour attraper (i.e. catch ) toutes les exceptions de la partie I/O de votre code. Vous pouvez simplement protger certaines parties de votre code avec catch ou vous pouvez couvrir plusieurs parties avec catch et utiliser diffrents gestionnaires pour chacune, ainsi : main = do toTry `catch` handler1 thenTryThis `catch` handler2 launchRockets

Ici, toTry utilise le gestionnaire handler1 et thenTryThis utilise le gestionnaire handler2 . launchRockets nest pas un paramtre de catch , donc toute exception quelle pourra lancer plantera probablement votre programme, moins que launchRockets nutilise un catch en interne pour grer ses propres exceptions. Bien sr, toTry , thenTryThis et launchRockets sont des actions I/O qui ont t colles ensemble avec la notation do et, hypothtiquement dfinies quelque part ailleurs. Cest un peu similaire aux blocs try-catch des autres langages, o lon peut entourer notre programme entier dun simple try-catch, ou bien utiliser une approche plus fine et utiliser diffrents try-catch sur diffrentes parties du code pour contrler le type derreur qui peut avoir lieu chaque endroit. Maintenant, vous savez grer les exceptions I/O ! Lever des exceptions depuis un code pur na pas encore t couvert, principalement parce que, comme je lai dj dit, Haskell offre de bien meilleurs faons dindiquer des erreurs, plutt que sen remettre aux I/O pour les attraper. Mme en collant des actions I/O les unes aux autres, je prfre que leur type soit IO (Either a b) , cest--dire des actions I/O normales mais dont le rsultat peut tre Left a ou Right b lorsquelles sont excutes.

Crer nos propres types et classes de types

Table des matires

Rsoudre des problmes fonctionnellement

Rsoudre des problmes fonctionnellement


Entres et sorties Table des matires Foncteurs, foncteurs applicatifs et monodes Dans ce chapitre, nous allons regarder quelques problmes intressants et comment les rsoudre fonctionnellement et aussi lgamment que possible. On ne dcouvrira probablement pas de nouveau concept, on va juste chauffer nos muscles Haskell tout frachement acquis et sentraner coder. Chaque section prsentera un problme diffrent. On commencera par dcrire le problme, puis on essaiera de trouver le meilleur moyen de le rsoudre (ou le moins mauvais).

Calculatrice de notation polonaise inverse


Gnralement, lorsquon crit des expressions mathmatiques lcole, on les crit de manire infixe. Par exemple, on crit 10 - (4 + 3) * 2 . + , * et - sont des oprateurs infixes, tout comme les fonctions infixes quon a rencontres en Haskell ( + , `elem` , etc.). Cest pratique puisquen tant quhumains, il nous est facile de dcomposer cette expression dans notre esprit. Linconvnient, cest quon a besoin de parenthses pour indiquer la prcdence. La notation polonaise inverse est une autre faon dcrire les expressions mathmatiques. Au dpart, a semble un peu bizarre, mais cest en fait assez simple comprendre et utiliser puisquil ny a pas besoin de parenthses, et parce que cest trs simple taper dans une calculatrice. Bien que la plupart des calculatrices modernes utilisent la notation infixe, certaines personnes ne jurent toujours que par leur calculatrice NPI. Voici ce quoi lexpression infixe prcdente ressemble en NPI : 10 4 3 + 2 * - . Comment calcule-t-on le rsultat de cela ? Eh bien, imaginez une pile. Vous parcourez lexpression de gauche droite. Chaque fois quun nombre est rencontr, vous lempilez. Ds que vous rencontrez un oprateur, vous retirez les deux nombres en sommet de pile (on dit aussi quon les dpile), utilisez loprateur sur ces deux nombres, et empilez le rsultat. Si lexpression est bien forme, en arrivant la fin vous ne devriez plus avoir quun nombre dans la pile, et ce nombre est le rsultat.

Parcourons lexpression 10 4 3 + 2 * - ensemble ! Dabord, on empile 10 , et la pile est donc 10 . Le prochain lment est 4 , on lempile galement. La pile est maintenant 10, 4 . De mme avec 3 , la pile est prsent 10, 4, 3 . Soudain, on rencontre un oprateur, jai nomm + ! On dpile les deux nombres en sommet de la pile (donc la pile devient 10 ), on somme ces deux nombres, et on empile le rsultat. La pile est dsormais 10, 7 . On empile le 2 , la pile devient 10, 7, 2 . On rencontre un nouvel oprateur, on dpile donc 7 et 2 , on les multiplie et on empile le rsultat. 7 fois 2 donne 14 , la pile est donc 10, 14 . Finalement, il y a un - . On dpile 10 et 14 , on soustrait 14 de 10 et on empile le rsultat. La pile est maintenant -4 , et puisquil ny a plus de nombres ou doprateurs dans notre expression, cest notre rsultat ! Maintenant quon sait calculer nimporte quelle expression NPI la main, rflchissons une manire dcrire une fonction Haskell qui prendrait en paramtre une chane de caractres contenant une expression NPI, comme "10 4 3 + 2 * -" , et nous renvoie son rsultat. Quel serait le type dune telle fonction ? On veut quelle prenne une chane de caractres en paramtre, et produise un nombre en rsultat. Ce sera donc probablement quelque chose comme solveRPN :: (Num a) => String -> a (NDT : RPN pour Reverse Polish Notation). Astuce : cela aide beaucoup de rflchir dabord la dclaration de type dune fonction avant de sinquiter de limplmentation, et dcrire cette dclaration. En Haskell, une dclaration de type nous en dit beaucoup sur une fonction, grce au systme de types puissant.

Cool. Quand on implmente une solution un problme en Haskell, il est aussi bien de penser la faon dont vous le feriez la main et den tirer des ides. Ici, on voit quon traite chaque nombre ou oprateur spar par un espace comme un lment unique. Il serait donc peut-tre utile de commencer par dcouper une chane comme "10 4 3 + 2 * -" en une liste dlments comme ["10","4","3","+","2","*","-"] . Ensuite, que faisions-nous avec cette liste dlments dans notre tte ? On la parcourait de la gauche vers la droite, et on

maintenait une pile tout du long. Est-ce que cette phrase vous rappelle quelque chose ? Souvenez-vous, dans la section sur les plis, on a dit que quasiment toute fonction qui traverse une liste de gauche droite ou de droite gauche, lment par lment, et construit (accumule) un rsultat (que ce soit un nombre, une liste, une pile, peu importe), peut tre implmente comme un pli. Dans ce cas, on va utiliser un pli gauche, puisquon traverse la liste de la gauche vers la droite. La valeur de laccumulateur sera notre pile et ainsi, le rsultat du pli sera aussi une pile, seulement, comme on la vu, elle ne contiendra quun lment. Une autre chose pondrer est, eh bien, comment va-t-on reprsenter cette pile ? Je propose dutiliser une liste. galement, je propose de garder la somme de notre pile du ct de la tte de la liste. Cest parce quajouter en tte (dbut) de liste est bien plus rapide que dajouter la fin. Donc si lon a une pile qui consiste en, mettons, 10, 4, 3 , nous la reprsenterons comme la liste [3, 4, 10] . On a prsent assez dinformations pour baucher notre fonction. Elle va prendre une liste, comme "10 4 3 + 2 * -" et la dcomposer en liste dlments en utilisant words pour obtenir ["10","4","3","+","2","*","-"] . Ensuite, on va utiliser un pli gauche sur la liste et terminer avec une pile un seul lment, [-4] . On sort cet lment de la liste, et cest notre rsultat final ! Voici donc lesquisse de notre fonction : import Data.List solveRPN :: (Num a) => String -> a solveRPN expression = head (foldl foldingFunction [] (words expression)) where foldingFunction stack item = ...

On prend lexpression et on la change en une liste dlments. Puis on plie la liste dlments avec la fonction de pli. Remarquez le [] , qui reprsente laccumulateur initial. Laccumulateur est notre pile, donc [] reprsente une pile vide, avec laquelle on dbute. Une fois quon rcupre la pile finale qui ne contient quun lment, on appelle head sur cette liste pour obtenir cet lment, et on applique read . Tout ce quil reste faire consiste implmenter une fonction de pli qui va prendre une pile, comme [4, 10] , et un lment, comme "3" , et retourner une nouvelle pile [3, 4, 10] . Si la pile est [4, 10] et que llment est "*" , alors elle devra retourner [40] . Mais avant cela, transformons notre fonction en style sans point, parce quelle est pleine de parenthses qui meffraient : import Data.List solveRPN :: (Num a) => String -> a solveRPN = head . foldl foldingFunction [] . words where foldingFunction stack item = ...

Ah, nous voil. Beaucoup mieux. Ainsi, la fonction de pli prend une pile et un lment et retourne une nouvelle pile. On va utiliser du filtrage par motif pour obtenir les deux lments en haut de pile, et filtrer les oprateurs comme "*" et "-" . solveRPN :: (Num a, Read a) => String -> a solveRPN = head . foldl foldingFunction [] . words where foldingFunction (x:y:ys) "*" = (x * y):ys foldingFunction (x:y:ys) "+" = (x + y):ys foldingFunction (x:y:ys) "-" = (y - x):ys foldingFunction xs numberString = read numberString:xs

On a tendu cela sur quatre motifs. Les motifs seront essays de haut en bas. Dabord, la fonction de pli regarde si llment courant est "*" . Si cest le cas, alors elle prendra une liste comme [3, 4, 9, 3] et nommera ses deux premiers lments x et y respectivement. Dans ce cas, x serait 3 et y serait 4 . ys serait [9, 3] . Elle retourne une liste comme ys , mais avec le produit de x et y en tte. Ainsi, on a dpil les deux nombres en haut de pile, on les a multiplis et on a empil le rsultat. Si llment nest pas "*" , le filtrage par motif continue avec le motif "+" , et ainsi de suite. Si llment nest aucun des oprateurs, alors on suppose que cest une chane qui reprsente un nombre. Si cest un nombre, on appelle read sur la chane pour obtenir un nombre, et on retourne la pile prcdente avec ce nombre empil. Et cest tout ! Remarquez aussi quon a ajout une contrainte de classe supplmentaire Read a la dclaration de type de la fonction, parce quon appelle read sur la chane de caractres pour obtenir le nombre. Ainsi, cette dclaration signifie que le rsultat peut tre de nimporte quel type membre des classes de types Num et Read (comme Int , Float , etc.). Pour la liste dlments ["2", "3", "+"] , notre fonction va commencer plier par la gauche. La pile initiale sera [] . Elle appellera la fonction de pli avec [] en tant que pile (accumulateur) et "2" en tant qulment. Puisque cet lment nest pas un oprateur, il sera lu avec read et ajout au dbut de [] . La nouvelle pile est donc [2] , et la fonction de pli sera appele avec [2] pour pile et ["3"] pour lment, produisant une nouvelle pile [3, 2] . Ensuite, elle est appele pour la troisime fois avec [3, 2] pour pile et "+" pour lment. Cela cause le dpilement des deux nombres, qui sont alors somms, et leur rsultat est empil. La pile finale est [5] , qui est le nombre quon retourne.

Jouons avec notre fonction : ghci> -4 ghci> 5 ghci> -3947 ghci> 4037 ghci> 4037 ghci> 87 solveRPN "10 4 3 + 2 * -" solveRPN "2 3 +" solveRPN "90 34 12 33 55 66 + * - +" solveRPN "90 34 12 33 55 66 + * - + -" solveRPN "90 34 12 33 55 66 + * - + -" solveRPN "90 3 -"

Cool, elle marche ! Une chose sympa avec cette fonction, cest quelle peut tre facilement modifie pour supporter une varit dautres oprateurs. Ils nont mme pas besoin dtre binaires. Par exemple, on peut crer un oprateur "log" qui ne dpile quun nombre, et empile son logarithme. On peut aussi faire un oprateur ternaire qui dpile trois nombres et empile le rsultat, comme "sum" qui dpile tous les nombres et empile leur somme. Modifions notre fonction pour grer quelques nouveaux oprateurs. Par simplicit, on va changer sa dclaration de type pour quelle retourne un type Float . import Data.List solveRPN :: String -> Float solveRPN = head . foldl foldingFunction [] . words where foldingFunction (x:y:ys) "*" = (x * y):ys foldingFunction (x:y:ys) "+" = (x + y):ys foldingFunction (x:y:ys) "-" = (y - x):ys foldingFunction (x:y:ys) "/" = (y / x):ys foldingFunction (x:y:ys) "^" = (y ** x):ys foldingFunction (x:xs) "ln" = log x:xs foldingFunction xs "sum" = [sum xs] foldingFunction xs numberString = read numberString:xs

Wow, gnial ! / est la division bien sr, et ** est lexponentiation des nombres virgule flottante. Pour loprateur logarithme, on filtre avec un motif un seul lment parce quon na besoin que dun lment pour calculer un logarithme naturel. Avec loprateur de somme, on retourne une pile qui na quun lment, gal la somme de tout ce que contenait la pile jusqualors. ghci> solveRPN 0.9932518 ghci> solveRPN 10.0 ghci> solveRPN 12.5 ghci> solveRPN 100.0 "2.7 ln" "10 10 10 10 sum 4 /" "10 10 10 10 10 sum 4 /" "10 2 ^"

Remarquez quon peut inclure des nombres virgule flottante dans nos expressions parce que read sait comment les lire. ghci> solveRPN "43.2425 0.5 ^" 6.575903

Je pense que faire une fonction qui calcule des expressions arbitraires sur les nombres virgule flottante en NPI, et qui peut tre facilement extensible, en une dizaine de lignes, est plutt gnial. Une chose noter propos de cette fonction est quelle nest pas trs rsistante aux erreurs. Si on lui donne une entre qui na pas de sens, cela va juste tout planter. On fera une version rsistante aux erreurs de cette fonction qui aura pour dclaration de type solveRPN :: String -> Maybe Float une fois quon aura dcouvert les monades (elles ne sont pas effrayantes, faites-moi confiance !). On pourrait en crire une ds maintenant, mais ce serait un peu fastidieux parce quil faudrait vrifier les valeurs Nothing chaque tape. Toutefois, si vous vous sentez dhumeur pour le dfi, vous pouvez vous lancer ! Indice : vous pouvez utiliser reads pour voir si un read a russi ou non.

DHeathrow Londres
Notre prochain problme est le suivant : votre avion vient datterrir en Angleterre, et vous louez une voiture. Vous avez un rendez-vous trs bientt, et devez aller de laroport dHeathrow jusqu Londres aussi vite que possible (mais sans vous mettre en danger !). Il y a deux routes principales allant dHeathrow Londres, et un nombre de routes rgionales qui croisent celles-ci. Il vous faut une quantit de temps fixe pour voyager dune intersection une autre. Vous devez trouver le chemin optimal pour arriver Londres aussi vite que possible ! Vous partez du ct gauche et

pouvez soit changer de route principale, soit rouler vers Londres.

Comme vous le voyez sur limage, le chemin le plus court dHeathrow Londres dans ce cas consiste dmarrer sur la route principale B, changer de route principale, avancer sur A, changer nouveau, et continuer jusqu Londres sur la route B. En prenant ce chemin, il nous faut 75 minutes. Si on en avait choisi un autre, il nous faudrait plus longtemps que a. Notre travail consiste crer un programme qui prend une entre reprsentant un systme routier, et affiche le chemin le plus court pour le traverser. Voici quoi ressemblera lentre dans ce cas : 50 10 30 5 90 20 40 2 25 10 8 0

Pour dcouper mentalement le fichier dentre, lisez-le trois lignes par trois lignes, et coupez mentalement le systme routier en sections. Chaque section se compose dun morceau de route A, dun morceau de route B, et dune route croisant A et B. Pour conserver cette lecture trois par trois, on dit quil y a une dernire route transversale qui prend 0 minute traverser. Parce quune fois arriv Londres, ce nest plus important, on est arriv. Tout comme on la fait en rsolvant le problme de la calculatrice NPI, on va rsoudre ce problme en trois tapes : Oublier Haskell un instant et penser la rsolution du problme la main Penser la reprsentation des donnes en Haskell Trouver comment oprer sur les donnes en Haskell pour aboutir la solution Dans la section sur la calculatrice NPI, on a dabord remarqu quen calculant une expression la main, on avait gard une sorte de pile dans notre esprit et travers lexpression un lment la fois. On a dcid dutiliser une liste de chanes de caractres pour reprsenter lexpression. Finalement, on a utilis un pli gauche pour traverser la liste de chanes tout en maintenant la pile pour produire la solution. Ok, donc, comment trouverions-nous le plus court chemin dHeathrow Londres la main ? Eh bien, on peut prendre du recul, essayer de deviner ce plus court chemin, et avec un peu de chance on trouvera le bon rsultat. Cette solution fonctionne pour de petites entres, mais quen sera-t-il si notre route a 10 000 sections ? Ouf ! On ne saura pas non plus dire avec certitude que notre solution est optimale, on pourra simplement se dire quon en est plutt sr. Ce nest donc pas une bonne solution. Voici une image simplifie de notre systme routier :

Parfait, pouvez-vous trouver quel est le plus court chemin jusqu la premire intersection de la route A (le premier point bleu sur la route A, not A1) ? Cest

plutt trivial. On regarde simplement sil est plus court dy aller directement depuis A, ou de passer par B puis traverser. videmment, il vaut mieux passer par B et traverser, puisque cela prend 40 minutes, alors quil faut 50 minutes depuis A. Quen est-il du plus court chemin vers B1 ? La mme chose. On voit quil est beaucoup plus court de passer par B (10 minutes) que de passer par A et traverser, ce qui nous prendrait 80 minutes ! prsent, on connat le chemin le plus court jusqu A1 (passer par B et traverser, on va dire B, C avec un cot de 40 minutes) et on connat le plus court chemin jusqu B1 (directement par B, donc B , en 10 minutes). Est-ce que ce savoir nous aide pour connatre le chemin le plus court jusqu la prochaine intersection de chacune des routes principales ? Mon dieu, mais cest bien sr ! Voyons ce que serait le plus court chemin jusqu A2. Pour aller A2, on peut soit aller directement dA1 A2, soit avancer depuis B1 et traverser (souvenezvous, on ne peut quavancer ou traverser). Et puisquon connat les cots dA1 et B1, on peut facilement trouver le meilleur chemin jusqu A2. Il faut 40 minutes pour aller A1, puis 5 minutes dA1 A2, donc B, C, A cote 45 minutes. Il ne cote que 10 minutes pour aller B2, mais il faut 110 minutes supplmentaires pour avancer jusqu B2 puis traverser ! videmment, le chemin le plus court jusqu A2 est B, C, A . De la mme faon, le chemin le plus court jusqu B2 consiste aller tout droit depuis A1 et traverser. Vous vous demandez peut-tre : mais quen est-il daller jusqu A2 en traversant B1 puis en continuant tout droit ? Eh bien, on a dj couvert la traverse de B1 A1 quand on cherchait le meilleur moyen daller A1, donc on na plus besoin de prendre cela en compte ltape suivante.

prsent quon connat les meilleurs chemins pour aller A2 et B2, on peut rpter le processus indfiniment jusqu atteindre larrive. Une fois quon connat les meilleurs chemins pour A4 et B4, le moins coteux sera notre chemin optimal. En gros, pour la deuxime section, on rpte ce quon a fait pour la premire section, mais en prenant en compte les meilleurs chemins prcdents pour A comme pour B. On pourrait dire quon a aussi pris en compte les meilleurs chemins prcdents la premire tape, en considrant que ces chemins vides avaient un cot de 0. Voil un rsum. Pour trouver le meilleur chemin dHeathrow Londres, on procde ainsi : dabord, on cherche le meilleur chemin jusqu la prochaine intersection de la route A. Il ny a que deux options : aller directement tout droit, ou commencer de la route oppose, avancer puis traverser. On se souvient du meiller chemin et du cot. On utilise la mme mthode pour trouver le meilleur chemin jusqu la prochaine intersection de la route B. Ensuite, on se demande si le chemin pour aller la prochaine intersection de la route A est plus court en partant de lintersection prcdente de A et en allant tout droit, ou en partant de lintersection prcdente de B, en avanant et traversant. On se souvient du chemin le plus court, de son cot, et on fait pareil pour lintersection oppose. On continue pour chaque section jusqu atteindre larrive, et le plus court des deux chemins jusquaux deux intersections de larrive est notre chemin optimal ! Pour faire simple, on garde un chemin le plus court sur A et un chemin le plus court sur B, jusqu atteindre larrive, o le plus court des deux est le chemin optimal. On sait prsent trouver le chemin le plus court la main. Si vous disposiez dassez de temps, de papier et de stylos, vous pourriez trouver le chemin le plus court dans un systme routier arbitrairement grand. Prochaine tape ! Comment reprsenter le systme routier avec des types de donnes dHaskell ? Une manire consiste imaginer les points de dpart et dintersection comme des nuds dun graphe qui pointe vers dautres intersections. Si on imagine que les points de dparts pointent lun vers lautre via une route de longueur nulle, on voit que chaque intersection (ou nud) pointe vers le nud du ct oppos et vers le prochain nud du mme ct. lexception des derniers nuds, qui ne pointent que lun vers lautre. data Node = Node Road Road | EndNode Road data Road = Road Int Node

Un nud est soit un nud normal, avec linformation sur la route qui mne au nud correspondant de lautre route et celle sur la route qui amne au nud suivant, soit un nud final, qui ne contient que linformation vers le nud oppos. Une route contient pour information sa longueur et le nud vers lequel elle pointe. Par exemple, la premire partie de la route A serait Road 50 a1 o a1 serait un nud Node x y , o x et y sont des routes qui pointent vers B1 et A2. Un autre moyen serait dutiliser Maybe pour les parties de la route qui pointent vers lavant. Chaque nud a une route vers le nud oppos, mais seuls les nuds non terminaux ont une route vers le prochain nud. data Node = Node Road (Maybe Road) data Road = Road Int Node

Cest une manire correcte de reprsenter le systme routier en Haskell et on pourrait certainement rsoudre le problme avec, mais on pourrait peut-tre trouver quelque chose de plus simple ? Si on revient sur notre solution la main, on na jamais vrifi que les longueurs de trois parties de routes la fois : la partie de la route A, sa partie oppose sur la route B, et la partie C qui relie larrive des deux parties prcdentes. Quand on cherchait le plus court chemin vers A1 et B1, on ne se souciait que des longueurs des trois premires parties, qui avaient pour longueur 50, 10 et 30. On appellera cela une section. Ainsi, le systme routier quon utilise pour lexemple peut facilement tre reprsent par quatre sections : 50, 10, 30 , 5, 90, 20 , 40, 2, 25 , and 10, 8, 0 . Il est toujours bon de garder nos types de donnes aussi simples que possible, mais pas trop simples non plus ! data Section = Section { getA :: Int, getB :: Int, getC :: Int } deriving (Show) type RoadSystem = [Section]

Cest plutt parfait ! Cest aussi simple que possible, et je pense que a suffira amplement pour implmenter notre solution. Section est un simple type de donnes algbriques qui contient trois entiers pour la longueur des trois parties de route de la section. On introduit galement un synonyme de type, nommant RoadSystem une liste de sections. On aurait aussi pu utiliser un triplet (Int, Int, Int) pour reprsenter une section de route. Utiliser des tuples plutt que vos propres types de donnes algbriques est bien pour des choses petites et localises, mais il est gnralement mieux de dfinir des nouveaux types pour des choses comme ici. Cela donne plus dinformation au systme de types sur ce quest chaque chose. On peut utiliser (Int, Int, Int) pour reprsenter une section de route ou un vecteur dans lespace 3D, et on peut oprer sur ces deux, mais ainsi on peut se confondre et les mlanger. Si on utilise les types de donnes Section et Vector , on ne peut pas accidentellement sommer un vecteur et une section de systme routier.

Notre systme routier dHeathrow Londres peut tre reprsent ainsi : heathrowToLondon :: RoadSystem heathrowToLondon = [Section 50 10 30, Section 5 90 20, Section 40 2 25, Section 10 8 0]

Tout ce dont on a besoin prsent, cest dimplmenter la solution quon a trouve prcdemment en Haskell. Que devrait-tre la dclaration de type de la fonction qui calcule le plus court chemin pour nimporte quel systme routier ? Elle devrait prendre un systme routier en paramtre, et retourner un chemin. On reprsentera un chemin sous forme de liste. Introduisons un type Label , qui sera juste une numration A , B ou C . On fera galement un synonyme de type : Path . data Label = A | B | C deriving (Show) type Path = [(Label, Int)]

Notre fonction, appelons-la optimalPath , devrait donc avoir pour dclaration de type optimalPath :: RoadSystem -> Path . Si elle est appele avec le systme routier heathrowToLondon , elle doit retourner le chemin suivant : [(B,10),(C,30),(A,5),(C,20),(B,2),(B,8)]

On va devoir traverser la liste des sections de la gauche vers la droite, et garder de ct le chemin optimal sur A et celui sur B. On va accumuler le meilleur chemin pendant quon traverse la liste, de la gauche vers la droite. Est-ce que a sonne familier ? Ding, ding, ding ! Et oui, cest un PLI GAUCHE ! Quand on droulait la solution la main, il y avait une tape quon rptait chaque fois. a impliquait de vrifier les chemins optimaux sur A et B jusquici et la section courante pour produire les nouveaux chemins optimaux sur A et B. Par exemple, au dpart, nos chemins optimaux sont [] et [] pour A et B respectivement. On examinait la section Section 50 10 30 et on a conclu que le nouveau chemin optimal jusqu A1 tait [(B,10),(C,30)] et que le chemin optimal jusqu B1 tait [(B,10)] . Si vous regardez cette tape comme une fonction, elle prend une paire de chemins et une section, et produit une nouvelle paire de chemins. Le type est (Path, Path) -> Section -> (Path, Path) . Implmentons directement cette fonction, elle sera forcment utile.

Indice : elle sera utile parce que (Path, Path) -> Section -> (Path, Path) peut tre utilise comme la fonction binaire du pli gauche, qui doit avoir pour type a -> b -> a .

roadStep :: (Path, Path) -> Section -> (Path, Path) roadStep (pathA, pathB) (Section a b c) = let priceA = sum $ map snd pathA priceB = sum $ map snd pathB forwardPriceToA = priceA + a crossPriceToA = priceB + b + c forwardPriceToB = priceB + b crossPriceToB = priceA + a + c newPathToA = if forwardPriceToA <= crossPriceToA then (A,a):pathA else (C,c):(B,b):pathB newPathToB = if forwardPriceToB <= crossPriceToB then (B,b):pathB else (C,c):(A,a):pathA in (newPathToA, newPathToB)

Que se passe-t-il l ? Dabord, il faut calculer le temps optimal sur la route A en se basant sur le prcdent temps optimal sur A, et de mme pour B. On fait sum $ map snd pathA , donc si pathA est quelque chose comme [(A,100),(C,20)] , priceA devient 120 .

forwardPriceToA est le temps optimal pour aller la prochaine intersection de A en venant de la prcdente intersection de A. Il est gal au meilleur temps jusquau prcdent A, plus la longueur de la partie A de la section courante. crossPriceToA est le temps optimal pour aller au prochain A en venant du prcdent B et en traversant. Il est gal au meilleur temps jusquau prcdent B, plus la longueur B de la section, plus la longueur C de la section. On dtermine forwardPriceToB et crossPriceToB de manire analogue. prsent quon connat le meilleur chemin jusqu A et B, il ne nous reste plus qu trouver les nouveaux chemins jusqu A et B. Sil est moins cher daller A en avanant simplement, on dfinit newPathToA comme (A, a):pathA . On prpose simplement le Label A et la longueur de la section a au chemin optimal jusquau point A prcdent. En gros, on dit que le meilleur chemin jusqu la prochaine intersection sur A est le meilleur chemin jusqu la prcdente intersection sur A, suivie par la section en avant sur A. Souvenez-vous qu A nest quune tiquette, alors que a a pour type Int . Pourquoi est-ce quon prpose plutt que de faire pathA ++ [(A, a)] ? Eh bien, ajouter un lment en dbut de liste (aussi appel conser) est bien plus rapide que de lajouter la fin. Cela implique que notre chemin sera lenvers une fois quon aura pli la liste avec cette fonction, mais il sera simple de le renverser plus tard. Sil est moins cher daller la prochaine intersection sur A en avanant sur B puis en traversant, alors newPathToA est le chemin jusqu la prcdente intersection sur B, suivie dune section en avant sur B et dune section traversante. On fait de mme pour newPathToB , lexception que tout est dans lautre sens. Finalement, on retourne newPathToA et newPathToB sous forme de paire. Testons cette fonction sur la premire section d heathrowToLondon . Puisque cest la premire section, les paramtres contenant les meilleurs chemins jusqu lintersection prcdente sur A et sur B sera une paire de listes vides. ghci> roadStep ([], []) (head heathrowToLondon) ([(C,30),(B,10)],[(B,10)])

Souvenez-vous, les chemins sont renverss, lisez-les donc de la droite vers la gauche. Ici, on peut lire que le meilleur chemin jusquau prochain A consiste avancer sur B puis traverser, et le meilleur chemin jusquau prochain B consiste avancer directement sur B. Astuce doptimisation : quand on fait priceA = sum $ map snd pathA , on calcule le prix partir du chemin chaque tape de lalgorithme. On pourrait viter cela en implmentant roadStep comme une fonction ayant pour type (Path, Path, Int, Int) -> Section -> (Path, Path, Int, Int) , o les entiers rprsenteraient le prix des chemins A et B. Maintenant quon a une fonction qui prend une paire de chemins et une section et produit un nouveau chemin optimal, on peut simplement plier sur une liste de sections. roadStep sera appele avec ([], []) et la premire section, et retournera une paire de chemins optimaux pour cette section. Puis, elle sera appele avec cette paire de chemins et la prochaine section, et ainsi de suite. Quand on a travers toutes les sections, il nous reste une paire de chemins optimaux, et le plus court des deux est notre rponse. Avec ceci en tte, on peut implmenter optimalPath . optimalPath :: RoadSystem -> Path optimalPath roadSystem = let (bestAPath, bestBPath) = foldl roadStep ([],[]) roadSystem in if sum (map snd bestAPath) <= sum (map snd bestBPath) then reverse bestAPath else reverse bestBPath

On plie depuis la gauche roadSystem (souvenez-vous, cest une liste de sections) avec pour accumulateur initial une paire de chemins vides. Le rsultat de ce pli est une paire de chemins, quon filtre par motif pour obtenir les chemins. Puis, on regarde lequel est le plus rapide, et on retourne celui-ci. Avant de le retourner, on le renverse, parce que les chemins taient jusqualors renverses parce quon avait choisi de conser plutt que de postposer. Testons cela ! ghci> optimalPath heathrowToLondon [(B,10),(C,30),(A,5),(C,20),(B,2),(B,8),(C,0)]

Cest le bon rsultat ! Gnial ! Il diffre lgrement de celui auquel on sattendait, parce quil y a une tape (C, 0) la fin, qui signifie quon traverse la route son arrive Londres, mais comme cette traverse na aucun cot, le rsultat reste valide. On a la fonction qui trouve le chemin optimal, il ne nous reste plus qu lire une reprsentation littrale dun systme routier de lentre standard, le convertir en un type RoadSystem , lancer notre fonction optimalPath dessus, et afficher le chemin. Dabord, crons une fonction qui prend une liste et la dcoupe en groupes de mme taille. On va lappeler groupsOf . Pour le paramtre [1..10] , groupsOf 3 devrait retourner [[1,2,3],[4,5,6],[7,8,9],[10]] .

groupsOf groupsOf groupsOf groupsOf

:: Int -> [a] -> [[a]] 0 _ = undefined _ [] = [] n xs = take n xs : groupsOf n (drop n xs)

Une fonction rcursive standard. Pour un xs valant [1..10] et un n gal 3 , ceci est gal [1,2,3] : groupsOf 3 [4,5,6,7,8,9,10] . Quand la rcursivit sachve, on a notre liste en groupes de trois lments. Et voici notre fonction main , qui lit lentre standard, cre un RoadSystem et affiche le chemin le plus court. import Data.List main = do contents <- getContents let threes = groupsOf 3 (map read $ lines contents) roadSystem = map (\[a,b,c] -> Section a b c) threes path = optimalPath roadSystem pathString = concat $ map (show . fst) path pathPrice = sum $ map snd path putStrLn $ "The best path to take is: " ++ pathString putStrLn $ "The price is: " ++ show pathPrice

Dabord, on rcupre le contenu de lentre standard. Puis, on appelle lines sur ce contenu pour convertir quelque chose comme "50\n10\n30\n en ["50","10","30" et ensuite, on mappe read l-dessus pour obtenir une liste de nombres. On appelle groupsOf 3 sur cette liste pour la changer en une liste de listes de longueur 3. On mappe la lambda (\[a,b,c] -> Section a b c) sur cette liste de listes. Comme vous le voyez, la lambda prend une liste de longueur 3, et la transforme en une section. Donc roadSystem est prsent notre systme routier et a un type correct, cest--dire RoadSystem (ou [Section] ). On appelle optimalPath avec a et on obtient le chemin optimal et son cot dans une reprsentation textuelle agrable quon affiche. Enregistrons le texte suivant : 50 10 30 5 90 20 40 2 25 10 8 0

dans un fichier paths.txt et donnons-le notre programme. $ cat paths.txt | runhaskell heathrow.hs The best path to take is: BCACBBC The price is: 75

a fonctionne merveille ! Vous pouvez utiliser vos connaissances du module Data.Random pour gnrer un systme routier plus long, que vous pouvez donner la fonction quon a crite. Si vous obtenez un dpassement de pile, essayez de remplacer foldl par foldl' , sa version stricte.

Entres et sorties

Table des matires

Foncteurs, foncteurs applicatifs et monodes

Foncteurs, foncteurs applicatifs et monodes


Rsoudre des problmes fonctionnellement Table des matires Pour une poigne de monades La combinaison de la puret, des fonctions dordre suprieur, des types de donnes algbriques paramtrs, et des classes de types que propose Haskell nous permet dimplmenter du polymorphisme un niveau bien plus lev que dans la plupart des autres langages. On na pas besoin de penser aux types comme appartenant une hirarchie de types. la place, on pense ce que les types peuvent faire, et on les connecte aux classes de types appropries. Un Int peut se faire passer pour beaucoup de choses. Il peut se faire passer pour quelque chose dont on peut tester lgalit, pour quelque chose quon peut ordonner, pour quelque chose quon peut numrer, etc. Les classes de types sont ouvertes, ce qui veut dire quon peut dfinir nos propres types de donnes, rflchir ce quils peuvent faire, et les connecter aux classes de types qui dfinissent ses comportements. Grce cela, et au systme de types dHaskell qui nous permet de savoir beaucoup de choses sur une fonction rien quen regardant sa dclaration de type, on peut dfinir des classes de types qui dfinissent des comportements trs gnraux et abstraits. On a vu des classes de types dfinissant des oprateurs pour tester lgalit ou comparer des choses. Ce sont des comportements assez abstraits et lgants, mais on ne les considre pas comme cela parce quon fait souvent la mme chose dans nos vies relles. On a rcemment dcouvert les foncteurs, qui sont simplement des choses sur lesquelles on peut mapper. Cest un exemple de proprit utile mais plutt abstraite que peuvent dcrire les classes de types. Dans ce chapitre, on va regarder les foncteurs dun peu plus prs, ainsi que des versions plus fortes et utiles de foncteurs, appels foncteurs applicatifs. On va aussi sintresser aux monodes, qui sont un peu comme des chaussettes.

Foncteurs revisits
On a dj parl des foncteurs dans leur propre petite section. Si vous ne lavez pas encore lue, vous devriez probablement le faire prsent, ou plus tard, quand vous aurez plus de temps. Ou faire semblant de lavoir lue. Ceci tant, un petit rappel : les foncteurs sont des choses sur lesquelles on peut mapper, comme des listes, des Maybe , des arbres, et dautres. En Haskell, ils sont dfinis par la classe de types Functor , qui na quune mthode de classe de type, fmap , ayant pour type fmap :: (a -> b) -> f a -> f b . Cela dit : donne-moi une fonction qui prend un a et retourne un b , et une bote avec un (ou plusieurs) a lintrieur, et je te donnerai une bote avec un (ou plusieurs) b lintrieur. Elle applique grosso-modo la fonction aux lments dans la bote.

Un conseil. Souvent, lanalogie de la bote aide se faire une intuition de la faon dont fonctionnent les foncteurs, et plus tard, on utilisera probablement la mme analogie pour les foncteurs applicatifs et les monades. Cest une analogie correcte pour aider les dbutants comprendre les foncteurs, mais ne la prenez pas trop littralement, parce que pour certains foncteurs, lanalogie est fortement tire par les cheveux. Un terme plus correct pour dfinir ce quest un foncteur serait un contexte de calcul. Le contexte peut tre que le calcul peut avoir renvoy une valeur ou chou ( Maybe et Either a ) ou que le calcul renvoie plusieurs valeurs (les listes), ce genre de choses.

Si on veut faire dun constructeur de types une instance de Functor , il doit avoir pour sorte * -> * , ce qui signifie quil doit prendre exactement un type concret en paramtre de type. Par exemple, Maybe peut tre une instance parce quil prend un paramtre de type pour produire un type concret, comme Maybe Int ou Maybe String . Si un constructeur de types prend deux paramtres, comme Either , il faut lappliquer partiellement jusqu ce quil ne prenne plus quun paramtre de type. Ainsi, on ne peut pas crire instance Functor Either where , mais on peut crire instance Functor (Either a) where , et alors en imaginant que fmap ne fonctionne que pour les Either a , elle aurait pour dclaration de type fmap :: (b -> c) -> Either a b -> Either a c . Comme vous pouvez le voir, la partie Either a est fixe, parce que Either a ne prend quun paramtre de type, alors qu Either en prend deux, et ainsi fmap :: (b -> c) -> Either b -> Either c ne voudrait rien dire. On sait prsent comment plusieurs types (ou plutt, des constructeurs de types) sont des instances de Functor , comme [] , Maybe , Either a et un type Tree quon a cr nous-mmes. On a vu comment lon pouvait mapper des fonctions sur ceux-ci pour notre plus grand bien. Dans cette section, on va dcouvrir deux autres instances de foncteurs, IO et (->) r . Si une valeur a pour type, mettons, IO String , cela signifie que cest une action I/O qui, lorsquelle est excute, ira dans le monde rel et nous rcuprera une chane de caractres, quelle rendra comme son rsultat. On peut utiliser <- dans la syntaxe do pour lier ce rsultat un nom. On a mentionn que les actions

I/O sont comme des botes avec des petits pieds qui sortent chercher des valeurs dans le monde lextrieur pour nous. On peut inspecter ce quelles ont ramen, mais aprs inspection, on doit les envelopper nouveau dans IO . En pensant cette analogie de bote avec des petits pieds, on peut voir comment IO agit comme un foncteur. Voyons comment faire d IO une instance de Functor . Quand on fmap une fonction sur une action I/O, on veut obtenir une action I/O en retour qui fait la mme chose, mais applique notre fonction sur la valeur rsultante. instance Functor IO where fmap f action = do result <- action return (f result)

Le rsultat du mappage de quelque chose sur une action I/O sera une action I/O, donc on utilise immdiatement la notation do pour coller deux actions en une. Dans limplmentation de fmap , on cre une nouvelle action I/O qui commence par excuter laction I/O originale, et on appelle son rsultat result . Puis, on fait return (f result) . return est, comme vous le savez, une fonction qui cre une action I/O qui ne fait rien, mais prsente un rsultat. Laction produite par un bloc do aura toujours pour rsultat celui de sa dernire action. Cest pourquoi on utilise return pour crer une action I/O qui ne fait pas grand chose, mais prsente f result comme le rsultat de laction I/O compose. On peut jouer un peu avec pour se faire une intuition. Cest 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 lutilisateur, et on la lui rend, mais renverse. Voici comment rcrire ceci en utilisant 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" pour obtenir Just "halb" , on peut fmap reverse sur getLine . getLine est une action I/O qui a pour type IO String et mapper reverse sur elle nous donne une action I/O qui va aller dans le monde rel et rcuprer une ligne, puis appliquer reverse dessus. Tout comme on peut appliquer une fonction quelque chose enferm dans une bote Maybe , on peut appliquer une fonction quelque chose enferm dans une bote IO , seulement la bote doit toujours aller dans le monde rel pour obtenir quelque chose. Ensuite, lorsquon la lie quelque chose avec <- , le nom sera li au rsultat auquel reverse aura dj t applique. Laction I/O fmap (++"!") getLine se comporte comme getLine , mais ajoute toujours "!" son rsultat ! Si on regarde ce que le type de fmap serait si elle tait limite IO , ce serait fmap :: (a -> b) -> IO a -> IO b . fmap prend une fonction et une action I/O et retourne une nouvelle action I/O comme lancienne, mais avec la fonction applique son rsultat. Si jamais vous liez le rsultat dune action I/O un nom, juste pour ensuite appliquer une fonction ce nom et lui donner un nouveau nom, utilisez plutt fmap , ce sera plus joli. Si vous voulez appliquez des transformations multiples une donne dans un foncteur, vous pouvez soit dclarer une fonction dans lespace de nom global, soit utiliser une lambda expression, ou idalement, utiliser la composition de fonctions : import Data.Char import Data.List 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 toUpper est une fonction qui prend une chane de caractres, mappe toUpper sur celle-ci, applique reverse au rsultat, et applique intersperse '-' ce rsultat. Cest comme crire (\xs -> intersperse '-' (reverse (map toUpper xs))) , mais plus joli. Une autre instance de Functor quon a utilise tout du long sans se douter quelle tait un foncteur est (->) r . Vous tes srement un peu perdu prsent, quest-ce que a veut dire (->) r ? Le type de fonctions r -> a peut tre rcrit (->) r a , tout comme 2 + 3 peut tre rcrit (+) 2 3 . Quand on regarde

(->) r a , on peut voir (->) sous un nouveau jour, et sapercevoir que cest juste un constructeur de types qui prend deux paramtres de types, tout comme Either . Mais souvenez-vous, on a dit quun constructeur de types doit prendre exactement un paramtre pour tre une instance de Functor . Cest pourquoi (->) ne peut pas tre une instance de Functor , mais si on lapplique partiellement en (->) r , a ne pose plus de problme. Si la syntaxe nous permettait dappliquer partiellement les constructeurs de types avec des sections (comme lon peut partiellement appliquer + en faisant (2+) , qui est quivalent (+) 2 ), vous pourriez rcrire (->) r comme (r ->) . Comment est-ce que les fonctions sont-elles des foncteurs ? Eh bien, regardons limplmentation, qui se trouve dans Control.Monad.Instances .

Gnralement, on indique une fonction qui prend nimporte quoi et retourne nimporte quoi a -> b . r -> a est identique, on a juste choisi dautres 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 lcrire de la premire faon. Tout dabord, pensons au type de fmap . Cest fmap :: (a -> b) -> f a -> f b . prsent, remplaons mentalement les f , qui ont pour rle dtre notre foncteur, par des (->) r . On fait cela pour voir comment fmap doit se comporter pour cette instance. On obtient fmap :: (a -> b) -> ((->) r a) -> ((->) r b) . Maintenant, on peut rcrire (->) r a et (->) r b de manire infixe en r -> a et r -> b , comme on lcrit habituellement pour les fonctions. On a donc 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 Maybe produit un Maybe et mapper une fonction sur une liste produit une liste. Quest-ce que le type fmap :: (a -> b) -> (r -> a) -> (r -> b) nous indique-t-il ? Eh bien, on voit que la fonction prend une fonction de a vers b et une fonction de r vers a , et retourne une fonction de r vers b . Cela ne vous rappelle rien ? Oui ! La composition de fonctions ! On connecte la sortie de r -> a lentre de a -> b pour obtenir une fonction r -> b , ce qui est exactement ce que fait la composition de fonctions. Si vous regardez comment linstance est dfinie ci-dessus, vous verrez quon a juste compos les fonctions. Une autre faon dcrire cette instance serait : instance Functor ((->) r) where fmap = (.)

Cela rend vident le fait quutiliser fmap sur des fonctions sert juste composer. Faites :m + Control.Monad.Instances , puisque cest l que linstance est dfinie, et essayez de jouer mapper sur des fonctions. ghci> :t fmap (*3) (+100) fmap (*3) (+100) :: (Num a) => a -> a 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 faon infixe pour souligner la ressemblance avec . . Dans la deuxime ligne dentre, on mappe (*3) sur (+100) , ce qui retourne une fonction qui prend une entre, appelle (+100) sur celle-ci, puis appelle (*3) sur ce rsultat. On appelle cette fonction sur la valeur 1 . Est-ce que lanalogie des botes fonctionne toujours ici ? Avec un peu dimagination, oui. Quand on fait fmap (+3) sur Just 3 , il est facile dimaginer le Maybe bote qui a un contenu sur lequel on applique la fonction (+3) . Mais quen est-il quand on fait fmap (*3) (+100) ? Eh bien, vous pouvez imaginer (+100) comme une bote qui contient son rsultat futur. Comme on imaginait une action I/O comme une bote qui irait chercher son rsultat dans le monde rel. Faire fmap (*3) sur (+100) cre une autre fonction qui se comporte comme (+100) , mais avant de produire son rsultat, applique (*3) dessus. Ainsi, on voit que fmap se comporte comme . pour les fonctions. Le fait que fmap soit la composition de fonctions quand elle est utilise sur des fonctions nest pas trs utile pour linstant, mais cest tout du moins intressant. Cela tord aussi un peu notre esprit et nous fait voir comment des choses agissant plutt comme des calculs que comme des botes (tel IO et (->) r ) peuvent elles aussi tre des foncteurs. La fonction mappe sur un calcul agit comme ce calcul, mais modifie son rsultat avec cette fonction.

Avant de regarder les rgles que fmap doit respecter, regardons encore une fois son type. Celui-ci est fmap :: (a -> b) -> f a -> f b . Il manque la contrainte de classe (Functor f) => , mais on loublie par concision, parce quon parle de foncteurs donc on sait ce que signifie f . Quand on a dcouvert les fonctions curryfies, on a dit que toutes les fonctions Haskell prennent un unique paramtre. Une fonction a -> b -> c ne prend en ralit quun paramtre a et retourne une fonction b -> c , qui prend un paramtre et retourne un c . Cest pourquoi, si lon appelle une fonction avec trop peu de paramtres (cest--dire quon lapplique partiellement), on obtient en retour une fonction qui prend autant de paramtres quil en manquait (on repense nouveau nos fonctions comme prenant plusieurs paramtres). Ainsi, a -> b -> c peut tre crit a -> (b -> c) pour faire apparatre la curryfication. Dans la mme veine, si lon crit fmap :: (a -> b) -> (f a -> f b) , on peut imaginer fmap non pas comme une fonction qui prend une fonction et un foncteur pour retourner un foncteur, mais plutt comme une fonction qui prend une fonction, et retourne une nouvelle fonction, similaire lancienne, mais qui prend et retourne des foncteurs. Elle prend une fonction a -> b , et retourne une fonction f a -> f b . On dit quon lifte la fonction. Jouons avec cette ide en utilisant la commande :t de GHCi : 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]

Lexpression fmap (*2) est une fonction qui prend un foncteur f sur des nombres, et retourne un foncteur sur des nombres. Ce foncteur peut tre une liste, un Maybe , un Either String , peu importe. Lexpression fmap (replicate 3) prend un foncteur de nimporte quel type et retourne un foncteur sur des listes dlments de ce type.

Quand on dit foncteur sur des nombres, vous pouvez imaginer un foncteur qui contient des nombres. La premire version est un peu plus sophistique et techniquement correcte, mais la seconde est plus simple saisir.

Ceci est encore plus apparent si lon applique partiellement, mettons, fmap (++"!") et quon 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 comme 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] signifie que la fonction marchera sur nimporte quel foncteur. Ce quelle fera exactement dpendra du foncteur en question. Si on utilise fmap (replicate 3) sur une liste, limplmentation de fmap pour les listes sera choisie, cest--dire map . Si on lutilise sur Maybe a , cela appliquera replicate 3 la valeur dans le Just , alors quun Nothing restera un 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 quils prsentent certaines proprits et comportements fonctoriels. Ils doivent se comporter de faon fiable comme des choses sur lesquelles on peut mapper. Appeler fmap sur un foncteur devrait seulement mapper une fonction sur le foncteur, rien de plus. Ce comportement est dcrit dans les lois des foncteurs. Il y en a deux, et toute instance de Functor doit les respecter. Elles ne sont cependant pas vrifies automatiquement par Haskell, il faut donc les tester soi-mme. La premire loi des foncteurs dit que si lon mappe la fonction id 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 lon fait fmap id sur un foncteur, cela doit tre pareil que de faire simplement id sur ce foncteur. Souvenez-vous, id est la fonction identit, qui retourne son paramtre lidentique. Elle peut galement tre crite \x -> x . Si lon voit le foncteur comme quelque chose sur laquelle on peut mapper, alors la loi 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 limplmentation de fmap , par exemple pour Maybe , on peut se rendre compte que la premire loi des foncteurs est respecte. instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing

Imaginons qu id soit la place de f dans limplmentation. On voit que si lon fmap id sur Just x , le rsultat sera Just (id x) , et puisqu id retourne son paramtre lidentique, on peut en dduire que Just (id x) est gal Just x . Ainsi, mapper id sur une valeur Maybe construite avec Just retourne la mme valeur. Voir que mapper id sur une valeur Nothing retourne la mme valeur est trivial. De ces deux quations de limplmentation de fmap , on dduit que fmap id = id est vrai. La seconde loi dit que composer deux fonctions, et mapper le rsultat sur un foncteur doit tre identique mapper dabord une des fonctions sur le foncteur, puis mapper lautre sur le rsultat. Formellement, on veut fmap (f . g) = fmap f . fmap g . Ou, dune autre faon, pour tout foncteur F, on souhaite : fmap (f . g) F = fmap f (fmap g F) . Si lon peut montrer quun type obit ces deux lois des foncteurs, alors on peut avoir lassurance quil aura les mmes proprits fondamentales vis--vis du mappage. On sait que lorsque lon fait fmap sur ce type, il ne se passera rien dautre quun 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 limplmentation de fmap pour ce type, et en utilisant la mme mthode quon a utilise pour voir si Maybe obissait la premire loi. Si vous le voulez, on peut vrifier que la seconde loi des foncteurs est vrifie par Maybe . Si on fait fmap (f . g) sur Nothing , on obtient Nothing , parce que quelle que soit la fonction mappe sur Nothing , on obtient Nothing . De mme, faire fmap f (fmap g Nothing) retourne Nothing , pour la mme raison. OK, voir que la seconde loi tient pour une valeur Nothing de Maybe tait plutt facile, presque trivial. Et si cest une valeur Just something ? Eh bien, si lon fait fmap (f . g) (Just x) , on voit de limplmentation que cest Just ((f . g) x) , qui est, videmment, Just (f (g x)) . Si lon fait fmap f (fmap g (Just x)) , on voit que fmap g (Just x) est Just (g x) . Donc, fmap f (fmap g (Just x)) est gal fmap f (Just (g x)) , et de limplmentation, on voit que cela est gal Just (f (g x)) . Si vous tes un peu perdu dans cette preuve, ne vous inquitez pas. Soyez sr de bien comprendre comme fonctionne la composition de fonctions. Trs souvent, on peut voir intuitivement que ces lois sont respectes 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. Intressons-nous au cas pathologique dun constructeur de types instance de Functor mais qui nest pas vraiment un foncteur, parce quil ne satisfait pas les lois. Mettons quon ait un type : data CMaybe a = CNothing | CJust Int a deriving (Show)

Le C ici est pour compteur. Cest un type de donnes qui ressemble beaucoup Maybe a , mais la partie Just contient deux champs plutt quun. Le premier champ du constructeur de valeurs CJust aura toujours pour type Int , et ce sera une sorte de compteur, et le second champ sera de type a , qui vient du paramtre de type, et son type dpendra bien sr du type concret quon choisit pour CMaybe a . Jouons avec notre type pour se faire une intuition. ghci> CNothing CNothing

ghci> CJust 0 "haha" CJust 0 "haha" ghci> :t CNothing CNothing :: CMaybe a ghci> :t CJust 0 "haha" CJust 0 "haha" :: CMaybe [Char] ghci> CJust 100 [1,2,3] CJust 100 [1,2,3]

Si on utilise le constructeur CNothing , il ny a pas de champs, alors que le constructeur CJust a un champ entier et un champ de nimporte quel type. Crons une instance de Functor pour laquelle, chaque fois quon utilise fmap , la fonction est applique au second champ, alors que le premier est incrment de 1. instance Functor CMaybe where fmap f CNothing = CNothing fmap f (CJust counter x) = CJust (counter+1) (f x)

Cest un peu comme limplmentation de Maybe , lexception que lorsquon fait fmap sur une valeur qui nest pas une bote vide (donc sur une valeur CJust ), en plus dappliquer la fonction au contenu, on augmente le compteur de 1. Tout va bien pour linstant, on peut mme jouer un peu avec : ghci> fmap (++"ha") (CJust 0 "ho") CJust 1 "hoha" ghci> fmap (++"he") (fmap (++"ha") (CJust 0 "ho")) CJust 2 "hohahe" ghci> fmap (++"blah") CNothing CNothing

Est-ce que cela vrifie les lois des foncteurs ? Pour dmontrer que ce nest pas le cas, il nous suffit de trouver un contre-exemple. ghci> CJust ghci> CJust fmap id (CJust 0 "haha") 1 "haha" id (CJust 0 "haha") 0 "haha"

Ah ! La premire loi dit que mapper id sur un foncteur quivaut appeler id sur ce mme foncteur, mais dans cet exemple, ce nest pas vrai pour notre foncteur CMaybe . Bien quil soit membre de la classe Functor , il nobit pas aux lois des foncteurs, et nest donc pas un foncteur. Si quelquun utilisait notre type CMaybe comme un foncteur, il sattendrait ce quil obisse aux lois des foncteurs, comme tout bon foncteur. Mais CMaybe choue tre un foncteur bien quil puisse faire semblant den tre un, et lutiliser en tant que tel pourrait amener un code erron. Lorsquon utilise un foncteur, cela ne devrait pas importer que lon compose des fonctions avant de les mapper ou que lon mappe les fonctions une une la suite. Mais pour CMaybe , cela compte, parce quil compte combien de fois on a mapp sur lui. Pas cool ! Si lon voulait que CMaybe obisse aux lois des foncteurs, il faudrait que le champ Int ne soit pas modifi lors dun fmap . Au dpart, les lois des foncteurs peuvent sembler un peu droutantes et peu ncessaires, mais on finit par sapercevoir que si un type obit ces lois, alors on peut prsumer son comportement. Si un type obit aux lois des foncteurs, on sait quappeler fmap sur une valeur va seulement mapper la fonction, rien de plus. Cela amne un code plus abstrait et extensible, parce quon peut utiliser ces lois pour raisonner sur le comportement de nimporte quel foncteur et crer des fonctions qui oprent de faon fiable sur nimporte quel foncteur. Toutes les instances de Functor de la bibliothque standard obissent ces lois, mais vous pouvez vrifier si vous ne me croyez pas. Et la prochaine fois que vous crez une instance de Functor , prenez une minute pour vous assurer quelle obit aux lois des foncteurs. Une fois que vous avez utilis assez de foncteurs, vous dveloppez une intuition pour ces proprits et comportements quils ont en commun et il nest plus dur de se rendre compte intuitivement quun type obit ou non aux lois des foncteurs. Mais mme sans intuition, vous pouvez toujours regarder limplmentation ligne par ligne et voir si les lois sont respectes, ou trouver un contre-exemple. On peut aussi regarder les foncteurs comme des choses qui retournent des valeurs dans un contexte. Par exemple, Just 3 retourne la valeur 3 dans le contexte o il peut ne pas y avoir de valeur retourne. [1, 2, 3] retourne trois valeurs - 1 , 2 et 3 , le contexte tant quil peut y avoir aucune ou plusieurs valeurs. La fonction (+3) retourne une valeur, qui dpend du paramtre quon lui donne. Si vous imaginez les foncteurs comme des choses qui retournent des valeurs, vous pouvez imaginer mapper sur des foncteurs comme attacher des transformations la sortie du foncteur pour changer les valeurs quil retourne. Lorsquon fait fmap (+3) [1, 2, 3] , on attache la transformation (+3) la sortie de [1, 2, 3] , donc quand on observe un des nombres que la liste retourne, (+3) lui est appliqu. Un autre exemple est celui du mappage sur des fonctions. Quand on fait fmap (+3) (*3) , on attache la transformation (+3) ce qui sortira de (*3) . On voit ainsi mieux pourquoi utiliser fmap sur des fonctions consiste juste composer les fonctions ( fmap (+3) (*3) est gal (+3) . (*3) , qui est gal \x -> ((x*3)+3) ), parce quon prend une fonction comme (*3) et quon attache la transformation (+3) sa sortie. Le rsultat est toujours une fonction, seulement quand on lui donne une valeur, elle sera multiplie par trois, puis passera par la transformation attache o on lui ajoutera trois. Cest ce qui se passe avec la composition.

Foncteurs applicatifs

Dans cette section, nous allons nous intresser aux foncteurs applicatifs, qui sont des foncteurs gonfls aux hormones, reprsents en Haskell par la classe de types Applicative , situe dans le module Control.Applicative . Comme vous le savez, les fonctions en Haskell sont curryfies par dfaut, ce qui signifie quune fonction qui semble prendre plusieurs paramtres en prend en fait un seul et retourne une fonction qui prend le prochain paramtre, et ainsi de suite. Si une fonction a pour type a -> b -> c , on dit gnralement quelle prend deux paramtres et retourne un c , mais en ralit, elle prend un a et retourne une fonction b -> c . Cest pourquoi on peut appeler une fonction en faisant f x y ou bien (f x) y . Ce mcanisme nous permet dappliquer partiellement des fonctions en les appelant avec trop peu de paramtres, ce qui rsulte en des fonctions que lon peut passer dautres fonctions. Jusquici, lorsquon mappait des fonctions sur des foncteurs, on mappait gnralement des fonctions qui ne prenaient quun paramtre. Mais que se passe-t-il lorsquon souhaite mapper * , qui prend deux paramtres, sur un foncteur ? Regardons quelques exemples concrets. Si lon a Just 3 et que lon fait fmap (*) (Just 3) , quobtient-on ? En regardant limplmentation de linstance de Functor de Maybe , on sait que si cest une valeur Just something , elle applique la fonction sur le something lintrieur du Just . Ainsi, faire fmap (*) (Just 3) retourne Just ((*) 3) , qui peut tre crit Just (* 3) en utilisant une section. Intressant ! On obtient une fonction enveloppe 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 lon mappe compare , qui a pour type (Ord a) => a -> a -> Ordering sur une liste de caractres, on obtient une liste de fonctions ayant pour type Char -> Ordering , parce que la fonction compare est partiellement applique sur les caractres de la liste. Ce nest pas une liste de fonctions (Ord a) => a -> Ordering , parce que le premier a appliqu tait un Char et donc le deuxime a doit galement tre un Char . On voit quen mappant des fonctions plusieurs paramtres sur des foncteurs, on obtient des foncteurs qui contiennent des fonctions. Que peut-on faire de ceuxci ? Pour commencer, on peut mapper sur ceux-ci des fonctions qui prennent en paramtre une fonction, parce que ce qui est dans le foncteur sera donn la fonction mappe comme paramtre. 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 lon a une valeur fonctorielle Just (3 *) et une autre valeur fonctorielle Just 3 , et quon souhaite sortir la fonction de Just (3 *) pour la mapper sur Just 5 ? Avec des foncteurs normaux, on est bloqu, parce quils ne supportent que la mappage de fonctions normales sur des foncteurs. Mme lorsquon a mapp \f -> f 9 sur un foncteur qui contenait une fonction, on ne mappait quune 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 rcuprer la fonction, puis la mapper sur Just 5 , mais on voudrait une solution plus gnrale et abstraite ce problme, qui fonctionnerait pour tous les foncteurs. Je vous prsente la classe de types Applicative . Elle rside dans le module Control.Applicative et dfinit deux mthodes, pure et <*> . Elle ne fournit pas dimplmentation par dfaut pour celles-ci, il faut donc les dfinir toutes deux nous-mmes si lon veut faire de quelque chose un foncteur applicatif. La classe est dfinie ainsi : class (Functor f) => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b

Ces trois petites lignes de dfinition nous disent beaucoup de choses ! Commenons par la premire ligne. Elle dbute la dfinition de la classe Applicative et introduit une contrainte de classe. Elle dit que si lon veut faire dun constructeur de types une instance d Applicative , il doit dabord tre membre de Functor . Cest pourquoi, si lon sait quun constructeur de type est membre d Applicative , alors cest aussi un Functor , et on peut donc utiliser fmap sur lui. La premire mthode quelle dfinie est appele pure . Sa dclaration de type est pure :: a -> f a . f joue le rle de notre instance de foncteur applicatif. PuisquHaskell a un trs bon systme de types, et puisquune fonction ne peut que prendre des paramtres et retourner une valeur, on peut dire beaucoup de choses avec une dclaration de type, et ceci nest pas une exception. pure prend une valeur de nimporte quel type, et retourne un foncteur applicatif encapsulant cette valeur en son intrieur. Quand on dit en son intrieur, on se rfre nouveau lanalogie de la bote, bien quon ait vu quelle ne soit pas toujours la plus mme dexpliquer ce quil se trame. La dclaration a -> f a est tout de mme plutt descriptive. On prend une valeur et on lenveloppe dans

un foncteur applicatif. Une autre faon de penser pure consiste dire quelle prend une valeur et la met dans un contexte par dfaut (ou pur) - un contexte minimal qui retourne cette valeur. La fonction <*> est trs intressante. Sa dclaration de type est f (a -> b) -> f a -> f b . Cela ne vous rappelle rien ? Bien sr, fmap :: (a -> b) -> f a -> f b . Cest un peu un fmap amlior. Alors que fmap prend une fonction et un foncteur, et applique cette fonction lintrieur du foncteur, <*> prend un foncteur contenant une fonction et un autre foncteur, et dune certaine faon extrait la fonction du premier foncteur pour la mapper sur lautre foncteur. Quand je dis extrait, je veux en fait presque dire excute puis extrait, peut-tre mme squence. On verra pourquoi bientt. Regardons limplmentation de linstance d Applicative de Maybe . instance Applicative Maybe where pure = Just Nothing <*> _ = Nothing (Just f) <*> something = fmap f something

Encore une fois, de la dfinition de la classe, on voit que le f qui joue le rle du foncteur applicatif doit prendre un type concret en paramtre, donc on crit instance Applicative Maybe where plutt que instance Applicative (Maybe a) where . Tout dabord, pure . On a dit prcdemment quelle tait suppose prendre quelque chose et lencapsuler 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 dfinition de <*> . On ne peut pas extraire de fonction dun Nothing , parce quil ne contient rien. Donc si lon essaie dextraire une fonction dun Nothing , le rsultat est Nothing . Si vous regardez la dfinition de classe d Applicative , vous verrez quil y a une classe de contrainte Functor , qui signifie que les deux paramtres de <*> sont des foncteurs. Si le premier paramtre nest pas un Nothing , mais un Just contenant une fonction, on veut mapper cette fonction sur le second paramtre. Ceci prend galement en compte le cas o le second paramtre est Nothing , puisque faire fmap sur un Nothing retourne Nothing . Donc, pour Maybe , <*> extrait la fonction de la valeur de gauche si cest un Just et la mappe sur la valeur de droite. Si nimporte lequel des paramtres est Nothing , le rsultat 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 premires lignes entres montrent comment la fonction extraite est mappe, mais dans ces exemples, elle aurait tout aussi bien pu tre mappe immdiatement sur les foncteurs. La dernire ligne est intressante parce quon essaie dextraire une fonction dun Nothing et de le mapper sur quelque chose, ce qui rsulte en un Nothing videmment. Avec des foncteurs normaux, on peut juste mapper une fonction sur un foncteur, et on ne peut plus rcuprer ce rsultat hors du foncteur de manire gnrale, mme lorsque le rsultat est une fonction applique partiellement. Les foncteurs applicatifs, quant eux, permettent doprer sur plusieurs foncteurs avec la mme 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 dabord, la fonction + est mise dans un foncteur, qui est dans ce cas une valeur Maybe . Donc, au dpart, on a pure (+) qui est gal Just (+) . Ensuite, Just (+) <*> Just 3 a lieu. Le rsultat est Just (3+) . Ceci cause de lapplication partielle. Appliquer la fonction + seulement sur 3 rsulte en une fonction qui prend un paramtre, et lui ajoute 3. Finalement,

Just (3+) <*> Just 5 est effectue, ce qui rsulte en Just 8 . Nest-ce pas gnial !? Les foncteurs applicatifs et le style applicatif dcrire pure f <*> x <*> y <*> nous permettent de prendre une fonction qui attend des paramtres qui ne sont pas ncessairement envelopps dans des foncteurs, et dutiliser cette fonction pour oprer sur des valeurs qui sont dans des contextes fonctoriels. La fonction peut prendre autant de paramtres quon le souhaite, parce quelle est partiellement applique tape par tape, chaque occurrence de <*> . Cela devient encore plus pratique et apparent si lon considre le fait que pure f <*> x est gal fmap f x . Cest une des lois des foncteurs applicatifs. Nous les regarderons plus en dtail plus tard, pour linstant, on peut voir intuitivement que cest le cas. Pensez-y, a tombe sous le sens. Comme on la dit plus tt, pure place une valeur dans un contexte par dfaut. Si lon place une fonction dans un contexte par dfaut, et quon lextrait de ce contexte pour lappliquer une valeur dans un autre foncteur applicatif, on a fait la mme chose que de juste mapper cette fonction sur le second foncteur applicatif. Plutt que dcrire pure f <*> x <*> y <*> , on peut crire fmap f x <*> y <*> . Cest pourquoi Control.Applicative exporte une fonction <$> qui est simplement fmap en tant quoprateur infixe. Voici sa dfinition : (<$>) :: (Functor f) => (a -> b) -> f a -> f b f <$> x = fmap f x

Yo ! Petit rappel : les variables de types sont indpendantes des noms des paramtres ou des noms des valeurs en gnral. Le f de la dclaration de la fonction ici est une variable de type avec une contrainte de classe disant que tout type remplaant f doit tre membre de la classe Functor . Le f dans le corps de la fonction dnote une fonction quon mappe sur x . Le fait que f soit utilis pour reprsenter ces deux choses ne signifie pas quelles reprsentent la mme chose.

En utilisant <$> , le style applicatif devient brillant, parce qu prsent, si lon veut appliquer une fonction f sur trois foncteurs applicatifs, on peut crire f <$> x <*> y <*> z . Si les paramtres ntaient pas des foncteurs applicatifs mais des valeurs normales, on aurait crit f x y z . Regardons cela de plus prs. 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 quon se penche l-dessus, comparez la ligne ci-dessus avec celle-ci : ghci> (++) "johntra" "volta" "johntravolta"

Gnial ! Pour utiliser une fonction sur des foncteurs applicatifs, parsemez quelques <$> et <*> et la fonction oprera 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 dabord mappe sur Just "johntra" , rsultant en une valeur comme Just ("johntra"++) ayant pour type Maybe ([Char] -> [Char]) . Remarquez comme le premier paramtre de (++) a t aval et les a sont devenus des Char . prsent, Just ("johntra"++) <*> Just "volta" est excut, ce qui sort la fonction du Just et la mappe sur Just "volta" , retournant Just "johntravolta" . Si lune de ces deux valeurs tait un Nothing , le rsultat serait Nothing . Jusquici, 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 dautres instances d Applicative , alors dcouvrons en plus ! Les listes (ou plutt, le constructeur de types listes, [] ) sont des foncteurs applicatifs. Quelle surprise ! Voici linstance d Applicative de [] : instance Applicative [] where pure x = [x] fs <*> xs = [f x | f <- fs, x <- xs]

Plus tt, on a dit que pure prenait des valeurs, et les plaait dans un contexte par dfaut. En dautres mots, dans un contexte minimal qui retourne cette valeur. Le contexte minimal pour les listes serait une liste vide, [] , mais la liste vide reprsente labsence de valeurs, donc elle ne peut pas contenir llment quon passe pure . Cest pourquoi pure prend un lment, et le place dans une liste singleton. De manire similaire, le contexte minimal du foncteur applicatif Maybe aurait t Nothing , mais comme il reprsentait labsence de valeurs, pure tait implment avec Just . ghci> pure "Hey" :: [String] ["Hey"]

ghci> pure "Hey" :: Maybe String Just "Hey"

Quen est-il de <*> ? Si on imagine le type de <*> si elle tait limite aux listes, ce serait (<*>) :: [a -> b] -> [a] -> [b] . On limplmente laide dune liste en comprhension. <*> doit dune certaine faon extraire la fonction de son paramtre de gauche, et la mapper sur son paramtre de droite. Mais ici, la liste de gauche peut contenir zro, une ou plusieurs fonctions. La liste de droite peut elle aussi contenir plusieurs valeurs. Cest pourquoi on utilise une liste en comprhension 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 rsultante contient toutes les combinaisons possibles dapplication dune 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 rsultante contient neuf lments. Chaque fonction de la liste de gauche est applique chaque valeur de la liste de droite. Si lon a une liste de fonctions qui prennent deux paramtres, 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 excut en premier, rsultant en une liste quivalente [(1+), (2+), (1*), (2*)] , parce que chaque fonction gauche est applique sur chaque valeur de droite. Puis, [(1+),(2+),(1*),(2*)] <*> [3,4] est excut, produisant le rsultat 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 quon a utilis une fonction normale qui prend deux chanes de caractres entre deux foncteurs applicatifs laide des oprateurs applicatifs appropris. Vous pouvez imaginer les listes comme des calculs non dterministes. Une valeur comme 100 ou "what" peut tre vu comme un calcul dterministe qui ne renvoie quun seul rsultat, alors quune liste [1, 2, 3] peut tre vue comme un calcul qui ne peut pas se dcider sur le rsultat quil doit avoir, et nous prsente donc tous les rsultats 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 dterministes avec + , qui produit ainsi un nouveau calcul non dterministe encore moins certain de son rsultat. Le style applicatif sur les listes est souvent un bon remplaant des listes en comprhension. Dans le deuxime chapitre, on souhaitait connatre 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 dlments. 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 parat plus clair, parce quil est plus simple de voir quon appelle seulement * entre deux calculs non dterministes. Si lon voulait tous les produits possibles de deux listes suprieurs 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 puisquil ny a quune fonction gauche, cest comme mapper. Une autre instance d Applicative quon a dj rencontre est IO . Voici comment son instance est implmente : instance Applicative IO where pure = return a <*> b = do f <- a x <- b return (f x)

Puisque pure ne fait que mettre des valeurs dans un contexte minimal qui puisse toujours renvoyer ce rsultat, il est sens que pure soit juste return , parce que cest exactement ce que fait return : elle cre une action I/O qui ne fait rien, mais retourne la valeur passe en rsultat, sans rien crire sur le terminal ni crire dans un fichier. Si <*> tait spcialise pour IO , elle aurait pour type (<*>) :: IO (a -> b) -> IO a -> IO b . Elle prendrait une action I/O qui retourne une fonction en rsultat, et une autre action I/O, et retournerait une action I/O partir de ces deux, qui, lorsquelle serait excute, effectuerait dabord la premire action pour obtenir la fonction, puis effectuerait la seconde pour obtenir une valeur, et retournerait le rsultat de la fonction applique la valeur. On utilise la syntaxe do pour limplmenter ici. Souvenez-vous, la syntaxe do prend plusieurs actions I/O et les colle les unes aux autres, ce qui est exactement ce que lon veut ici. Avec Maybe et [] , on pouvait imaginer que <*> extrayait simplement une fonction de son paramtre de gauche et lappliquait dune certaine faon sur son paramtre de droite. Avec IO , lextraction est toujours de la partie, mais on a galement une notion dordonnancement, parce quon prend deux actions I/O et quon les ordonne, en les collant lune lautre. On doit extraire la fonction de la premire action I/O, mais pour pouvoir lextraire, il faut excuter laction. Considrez ceci : myAction :: IO String myAction = do a <- getLine b <- getLine return $ a ++ b

Cest une action I/O qui demande lutilisateur dentrer deux lignes, et retourne en rsultat ces deux lignes concatnes. Ceci est obtenu en collant deux actions I/O getLine ensemble avec un return , afin que le rsultat soit a ++ b . Une autre faon dcrire cela en style applicatif serait : myAction :: IO String myAction = (++) <$> getLine <*> getLine

Ce quon faisait prcdemment, ctait crer une action I/O qui appliquait une fonction entre les rsultats de deux actions I/O, et ici cest pareil. Souvenez-vous, getLine est une action I/O qui a pour type getLine :: IO String . Quand on utilise <*> entre deux foncteurs applicatifs, le rsultat est un foncteur applicatif, donc tout va bien. Le type de lexpression (++) <$> getLine <*> getLine est IO String , ce qui signifie que cette expression est une action I/O comme une autre, qui contient galement une valeur rsultante, comme toutes les actions I/O. Cest pourquoi on peut faire : main = do a <- (++) <$> getLine <*> getLine putStrLn $ "The two lines concatenated turn out to be: " ++ a

Si jamais vous vous retrouvez en train de lier des actions I/O des noms, puis faire return sur lapplication dune fonction ces noms, considrez utiliser le style applicatif, qui sera probablement plus concis et simple. Une autre instance d Applicative est (->) r , autrement dit les fonctions. Elles sont rarement utilises en style applicatif, moins que vous ne fassiez du golf avec votre code, mais elles sont tout de mme dintressants foncteurs applicatifs, regardons donc comment leur instance est implmente.

Si vous ne comprenez pas ce que (->) r signifie, lisez la section prcdente o lon expliquait que (->) r tait un foncteur.

instance Applicative ((->) r) where pure x = (\_ -> x) f <*> g = \x -> f x (g x)

Lorsquon encapsule une valeur dans un foncteur applicatif avec pure , le rsultat retourn doit tre cette valeur. Il nous faut un contexte minimal retournant cette valeur. Cest pourquoi, dans linstance des fonctions, pure prend une valeur, et cre une fonction qui ignore le paramtre quelle reoit pour retourner plutt cette valeur l. Le type de pure si elle tait spcialise pour linstance (->) r serait pure :: a -> (r -> a) . ghci> (pure 3) "blah" 3

Grce la curryfication, lapplication des fonctions est associative gauche, on peut donc omettre les parenthses. ghci> pure 3 "blah" 3

Limplmentation pour cette instance de <*> est un peu nigmatique, il vaut donc mieux regarder comment lon utilise les fonctions en foncteurs applicatifs en style applicatif. ghci> :t (+) <$> (+3) <*> (*100) (+) <$> (+3) <*> (*100) :: (Num a) => a -> a ghci> (+) <$> (+3) <*> (*100) $ 5 508

Appeler <*> avec deux foncteurs applicatifs retourne un foncteur applicatif, donc utilis sur deux fonctions elle renvoie une fonction. Que se passe-t-il donc ici ? Quand on fait (+) <$> (+3) <*> (*100) , on cre une fonction qui utilise + sur les rsultats de (+3) et de (*100) , et retourne cela. Pour montrer un exemple rel, quand on fait (+) <$> (+3) <*> (*100) $ 5 , (+3) et (*100) sont appliques sur 5 , retournant 8 et 500 . Puis + est appele sur 8 et 500 , retournant 508 . ghci> (\x y z -> [x,y,z]) <$> (+3) <*> (*2) <*> (/2) $ 5 [8.0,10.0,2.5]

De mme ici. On cre une fonction qui appelle la fonction \x y z -> [x, y, z] sur les rsultats de (+3) , (*2) et (/2) . Le 5 est donn chacune de ces trois fonctions, puis \x y z -> [x, y, z] est appele avec ces trois rsultats. Vous pouvez imaginer les fonctions comme des botes qui contiennent leur rsultat venir, donc faire k <$> f <*> g cre une fonction qui appellera k sur les rsultats venir de f et g . Quand on fait (+) <$> Just 3 <*> Just 5 , on utilise + sur des valeurs qui peuvent tre l ou non, ce qui retourne donc une valeur qui peut tre l ou non. Quand on fait (+) <$> (+10) <*> (+5) , on utilise + sur une valeur future de (+10) et (+5) , et le rsultat sera aussi une valeur future qui ne sera produit que lorsquon appellera la fonction avec un paramtre. On utilise peu les fonctions comme des foncteurs applicatifs, mais cest tout de mme intressant. Ce nest pas trs important pour vous de comprendre comment linstance de (->) r d Applicative fonctionne, donc ne dsesprez pas si vous ne le saisissez pas ds maintenant. Essayez de jouer avec le style applicatif pour amliorer votre intuition des fonctions en tant que foncteurs applicatifs. Une instance d Applicative que lon na jamais rencontre auparavant est ZipList , et elle rside dans Control.Applicative . Il savre quil y a plusieurs faons pour une liste dtre un foncteur applicatif. Lune est celle quon a dj vue, qui dit quappeler <*> sur une liste et une liste de valeurs retourne une liste de toutes les combinaisons possibles dapplication dune fonction de la liste de gauche sur une valeur de la liste de droite. Si lon fait [(+3),(*2)] <*> [1,2] , (+3) est applique la fois sur 1 et sur 2 , et (*2) est galement applique avec 1 et 2 , ce qui nous donne une liste quatre lments, [4, 5, 2, 4] . Cependant, [(+3),(*2)] <*> [1,2] pourrait aussi fonctionner de manire ce que la premire fonction de la liste de gauche soit applique avec la premire valeur de la liste de droite, la deuxime fonction avec la deuxime valeur, et ainsi de suite. Cela retournerait une liste deux valeurs, [4, 4] . Vous pouvez limaginer comme [1 + 3, 2 * 2] . Puisquun type ne peut pas avoir deux instances de la mme classe de types, le type ZipList a est introduit, et il a pour seul constructeur ZipList qui ne prend quun seul champ, de type liste. Voici son instance : instance Applicative ZipList where pure x = ZipList (repeat x) ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)

<*> fait juste ce quon a dit. Elle applique la premire fonction avec la premire valeur, la deuxime fonction avec la deuxime valeur, etc. Cela est ralis avec zipWith (\f x -> f x) fs xs . La liste rsultante sera aussi longue que la plus courte des deux listes, parce que cest ce que fait zipWith . pure est galement intressante ici. Elle prend une valeur, et la met dans une liste qui contient cette valeur un nombre infini de fois. pure "haha" retourne ZipList (["haha", "haha", "haha", . Cest peut-tre un peu droutant, puisquon a dit que pure devait mettre une valeur dans un contexte minimal qui retournait cette valeur. Et vous vous dites srement quune liste infinie est difficilement minimale. Mais cela a du sens pour les listes zippes, parce quelle doit produire cette valeur chaque position. Cela permet aussi de satisfaire la loi disant que pure f <*> xs doit tre gal fmap f xs . Si pure 3 ne retournait que ZipList [3] , pure (*2) <*> ZipList [1, 5, 10] retournerait ZipList [2] , parce que la liste retourne par zipWith est aussi longue que la plus

courte des deux. Alors que si lon zippe une liste finie une liste infinie, la longueur de la liste rsultante sera celle de la liste finie. Que font donc les listes zippes dans le style applicatif ? Voyons cela. Oh, le type ZipList a na pas dinstance de Show , il faut donc utiliser getZipList pour rcuprer une liste normale partir dune liste zippe. ghci> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100,100] [101,102,103] ghci> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100..] [101,102,103] ghci> getZipList $ max <$> ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2] [5,3,3,4] ghci> getZipList $ (,,) <$> ZipList "dog" <*> ZipList "cat" <*> ZipList "rat" [('d','c','r'),('o','a','a'),('g','t','t')]

La fonction (,,) est identique \x y z -> (x, y, z) . galement, la fonction (,) est identique \x y -> (x, y) .

En plus de zipWith , la bibliothque standard a des fonctions comme zipWith3 , zipWith4 , jusqu 7. zipWith prend une fonction deux paramtres et zippe deux listes avec cette fonction. zipWith3 prend une fonction trois paramtres et zippe trois listes laide de celle-ci, et ainsi de suite. En utilisant des listes zippes avec un style applicatif, on na pas besoin davoir une fonction zip diffrente pour chaque nombre de listes zipper. On utilise simplement le style applicatif pour zipper ensemble un nombre arbitraire de listes avec une fonction, cest plutt cool. Control.Applicative dfinit une fonction nomme liftA2 , qui a pour type liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c . Elle est dfinie ainsi : liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c liftA2 f a b = f <$> a <*> b

Rien de spcial, elle applique juste une fonction entre deux foncteurs applicatifs, encapsulant le style applicatif auquel on stait habitu. On observe cette fonction pour la raison quelle dmontre clairement en quoi les foncteurs applicatifs sont plus puissants que les foncteurs ordinaires. Avec des foncteurs ordinaires, on peut seulement mapper des fonctions sur un foncteur. Avec des foncteurs applicatifs, on peut appliquer une fonction entre plusieurs foncteurs. Il est aussi intressant dimaginer le type de cette fonction comme (a -> b -> c) -> (f a -> f b -> f c) . Quand on regarde de cette faon, on voit que liftA2 prend une fonction binaire normale, et la promeut en une fonction binaire sur deux foncteurs. Voici un concept intressant : on peut prendre deux foncteurs applicatifs, et les combiner en un foncteur applicatif qui contient en lui les rsultats de ces deux foncteurs applicatifs, sous forme dune liste. Par exemple, on a Just 3 et Just 4 . Imaginons que ce deuxime a une liste singleton en lui, puisque cest trs simple raliser : ghci> fmap (\x -> [x]) (Just 4) Just [4]

Ok, donc disons quon a Just 3 et Just [4] . Comment obtenir Just [3, 4] ? Facile. ghci> liftA2 (:) (Just 3) (Just [4]) Just [3,4] ghci> (:) <$> Just 3 <*> Just [4] Just [3,4]

Souvenez-vous, : est la fonction qui prend un lment, une liste, et retourne une nouvelle liste qui a cet lment en tte. prsent quon a Just [3, 4] , pourrait-on combiner ceci avec Just 2 pour produire Just [2, 3, 4] ? Bien sr. Il semble quon puisse combiner nimporte quel nombre de foncteurs applicatifs en un foncteur applicatif qui contient une liste de tous les rsultats. Essayons dimplmenter une fonction qui prend une liste de foncteurs applicatifs et retourne un foncteur applicatif qui contienne une liste en rsultat. On lappellera sequenceA . sequenceA :: (Applicative f) => [f a] -> f [a] sequenceA [] = pure [] sequenceA (x:xs) = (:) <$> x <*> sequenceA xs

Ah, de la rcursivit ! Tout dabord, regardons le type. Elle transforme une liste de foncteurs applicatifs en un foncteur applicatif avec une liste. Ainsi, on peut voir apparatre notre cas de base. Si on veut transformer une liste vide en un foncteur applicatif retournant une liste, eh bien il suffit de mettre la liste vide dans un contexte minimal applicatif. Vient ensuite la rcursion. Si on a une liste avec une tte et une queue (souvenez-vous, x est un foncteur applicatif, xs une liste de ceux-ci), on appelle sequenceA sur la queue pour obtenir un foncteur applicatif qui contient une liste. On na plus qu prposer la valeur contenue dans le foncteur x cette liste, et voil !

Ainsi, si lon fait sequenceA [Just 1, Just 2] , cest comme (:) <$> Just 1 <*> sequenceA [Just 2] . Cest gal (:) <$> Just 1 <*> ((:) <$> Just 2 <*> sequenceA []) . Ah ! On sait que sequenceA [] est simplement Just [] , donc cette expression se rduit en (:) <$> Just 1 <*> ((:) <$> Just 2 <*> Just []) , puis en (:) <$> Just 1 <*> Just [2] et finalement en Just [1,2] ! Un autre moyen dimplmenter sequenceA est laide dun pli. Souvenez-vous, presque toute fonction parcourant une liste dlments en accumulant un rsultat peut tre implmente comme un pli. sequenceA :: (Applicative f) => [f a] -> f [a] sequenceA = foldr (liftA2 (:)) (pure [])

On approche la liste par la droite avec un accumulateur initial gal pure [] . On fait listA2 (:) entre laccumulateur et le dernier lment de la liste, ce qui retourne un foncteur applicatif qui contient une liste singleton. Puis lon fait liftA2 (:) sur le nouveau dernier lment et laccumulateur, et ainsi de suite, jusqu ce quil ne reste plus que laccumulateur, contenant la liste des rsultats des foncteurs applicatifs. Testons notre fonction sur quelques foncteurs applicatifs. ghci> sequenceA [Just 3, Just 2, Just 1] Just [3,2,1] ghci> sequenceA [Just 3, Nothing, Just 1] Nothing ghci> sequenceA [(+3),(+2),(+1)] 3 [6,5,4] ghci> sequenceA [[1,2,3],[4,5,6]] [[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]] ghci> sequenceA [[1,2,3],[4,5,6],[3,4,4],[]] []

Ah ! Plutt cool. Quand on lutilise sur des valeurs Maybe , sequenceA cre une valeur Maybe avec tous les rsultats dans une liste. Si lune des valeurs est Nothing , alors le rsultat est Nothing . Cest utile lorsque vous avez une liste de valeurs Maybe , et que vous ne vous intressez ses rsultats que sils sont tous diffrents de Nothing . Quand on lutilise avec des fonctions, sequenceA prend une liste de fonctions et retourne une fonction qui retourne une liste. Dans notre exemple, on a cr une fonction qui prend un nombre en paramtre et appliquait chaque fonction dune liste sur ce nombre, retournant la liste des rsultats. sequenceA [(+3), (+2), (+1)] 3 appellera (+3) avec 3 , (+2) avec 3 et (+1) avec 3 , et retourne tous ces rsultats sous forme de liste. Faire (+) <$> (+3) <*> (*2) crera une fonction qui prend un paramtre, le donne la fois (+3) et (*2) , et appelle + avec ces deux rsultats. Dans la mme veine, il est normal que sequenceA [(+3), (*2)] cre une fonction qui prend un paramtre, et le donne toutes les fonctions de la liste. Plutt que dappeler + sur le rsultat de ces fonctions, une combinaison de : et de pure [] est utilise pour rassembler ces rsultats en une liste, qui est le rsultat de cette fonction. Utiliser sequenceA est cool quand on a une liste de fonctions et quon veut leur donner la mme entre et voir une liste des rsultats. Par exemple, si lon a un nombre et quon se demande sil satisfait tous les prdicats dune liste. Un moyen de faire serait le suivant : ghci> map (\f -> f 7) [(>4),(<10),odd] [True,True,True] ghci> and $ map (\f -> f 7) [(>4),(<10),odd] True

Souvenez-vous, and prend une liste de boolens et ne retourne True que sils sont tous True . Un autre moyen de faire ceci, avec sequenceA : ghci> sequenceA [(>4),(<10),odd] 7 [True,True,True] ghci> and $ sequenceA [(>4),(<10),odd] 7 True

sequenceA [(>4),(<10),odd] cre une fonction qui prendra un nombre et le donnera tous les prdicats de la liste [(>4),(<10),odd] , et retournera une liste de boolens. Elle transforme une liste ayant pour type (Num a) => [a -> Bool] en une fonction ayant pour type (Num a) => a -> [Bool] . Plutt joli, hein ? Parce que les listes sont homognes, toutes les fonctions de la liste doivent avoir le mme type, videmment. Vous ne pouvez pas avoir une liste comme [ord, (+3)] , parce qu ord prend un caractre et retourne un nombre, alors que (+3) prend un nombre et retourne un nombre. Quand on lutilise avec [] , sequenceA prend une liste de listes et retourne une liste de listes. Hmm, intressant. Elle cre en fait des listes contenant toutes les combinaisons possibles des lments. Par exemple, voici ceci ralis avec sequenceA puis avec une liste en comprhension :

ghci> sequenceA [[1,2,3],[4,5,6]] [[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]] ghci> [[x,y] | x <- [1,2,3], y <- [4,5,6]] [[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]] ghci> sequenceA [[1,2],[3,4]] [[1,3],[1,4],[2,3],[2,4]] ghci> [[x,y] | x <- [1,2], y <- [3,4]] [[1,3],[1,4],[2,3],[2,4]] ghci> sequenceA [[1,2],[3,4],[5,6]] [[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]] ghci> [[x,y,z] | x <- [1,2], y <- [3,4], z <- [5,6]] [[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]]

Cest peut-tre un peu dur saisir, mais en jouant un peu avec, vous verrez comment a marche. Mettons quon fasse sequenceA [[1,2],[3,4]] . Pour voir ce quil se passe, utilisons la dfinition sequenceA (x:xs) = (:) <$> x <*> sequenceA xs de sequenceA et son cas de base sequenceA [] = pure [] . Vous ntes pas oblig de suivre cette valuation, mais cela peut vous aider si vous avez du mal imaginer comment sequenceA fonctionne sur des listes de listes, car a peut tre un peu gratin. On commence avec sequenceA [[1,2],[3,4]] Ceci est valu en (:) <$> [1,2] <*> sequenceA [[3,4]] En dveloppant le sequenceA , on obtient (:) <$> [1,2] <*> ((:) <$> [3,4] <*> sequenceA []) On a atteint le cas de base, cela donne donc (:) <$> [1,2] <*> ((:) <$> [3,4] <*> [[]]) On value maintenant la partie (:) <$> [3,4] <*> [[]] , qui va utiliser : sur toutes les valeurs possibles de la liste de gauche ( 3 et 4 sont possibles) et avec chaque valeur possible de la liste de droite (seule [] est possible), ce qui donne [3:[], 4:[]] , autrement dit [[3],[4]] . On a donc (:) <$> [1,2] <*> [[3],[4]] Maintenant, : est utilise sur toutes les valeurs possibles de la liste de gauche ( 1 et 2 ) avec chaque valeur possible de la liste de droite ( [3] et [4] ), ce qui donne [1:[3], 1:[4], 2:[3], 2:[4]] , qui est juste [[1,3],[1,4],[2,3],[2,4] Faire (+) <$> [1,2] <*> [4,5,6] retourne un calcul non dterministe x + y o x prend toute valeur de [1, 2] et y prend toute valeur de [4, 5, 6] . On reprsente ceci comme une liste contenant tous les rsultats possibles. De faon similaire, quand on fait sequence [[1,2],[3,4],[5,6],[7,8]] , le rsultat est un calcul non dterministe [x, y, z, w] , o x prend toute valeur de [1, 2] , y prend toute valeur de [3, 4] et ainsi de suite. Pour reprsenter le rsultat de ce calcul non dterministe, on utilise une liste, dans laquelle chaque lment est une des listes possibles. Cest pourquoi le rsultat est une liste de listes. Quand on lutilise avec des actions I/O, sequenceA est quivalent sequence ! Elle prend une liste dactions I/O et retourne une action I/O qui excutera chacune de ces actions et aura pour rsultat une liste des rsultats de ces actions I/O. Ceci parce que, pour changer une valeur [IO a] en une valeur IO [a] , cest--dire crer une action I/O qui retourne une liste de tous les rsultats, il faut ordonnancer les action I/O afin quelles soient effectues lune aprs lautre lorsque lvaluation sera force. On ne peut en effet pas obtenir le rsultat dune action I/O sans lexcuter. ghci> sequenceA [getLine, getLine, getLine] heyh ho woo ["heyh","ho","woo"]

Comme les foncteurs ordinaires, les foncteurs applicatifs viennent avec quelques lois. La plus importante est celle quon a dj mentionne, qui dit que

pure f <*> x = fmap f x . En exercice, vous pouvez prouver cette loi pour quelques uns des foncteurs applicatifs vus dans ce chapitre. Les autres lois des
foncteurs applicatifs sont :

pure id <*> v = v pure (.) <> u <> v <> w = u <> (v <*> w) pure f <*> pure x = pure (f x) u <> pure y = pure ($ y) <> u
On ne va pas les regarder en dtail pour linstant, parce que cela prendrait trop de pages et serait probablement ennuyeux, mais si vous vous sentez dattaque, regardez-les et voyez si elles tiennent pour certaines instances. En conclusion, les foncteurs applicatifs ne sont pas seulement intressants, mais galement utiles, parce quils nous permettent de combiner diffrents calculs, comme les entres-sorties, les calculs non dterministes, les calculs pouvant chouer, etc. en utilisant le style applicatif. Simplement en utilisant <$> et <*> , on

comme les entres-sorties, les calculs non dterministes, les calculs pouvant chouer, etc. en utilisant le style applicatif. Simplement en utilisant <$> et <*> , on peut utiliser des fonctions ordinaires pour oprer uniformment sur nimporte quel nombre de foncteurs applicatifs, et profiter de la smantique de chacun dentre eux.

Le mot-cl newtype
Jusquici, nous avons appris comment crer nos propres types de donnes algbriques en utilisant le mot-cl data. On a aussi appris comment donner des synonymes des types avec le mot-cl type . Dans cette section, on va regarder comment crer de nouveaux types partir de types existants, et pourquoi on voudrait pouvoir faire cela. Dans la section prcdente, on a vu quil y a plusieurs manires de dfinir un foncteur applicatif pour le type des listes. Lune delles fait prendre <*> chaque fonction dune liste passe en paramtre de gauche et la lui fait appliquer chaque valeur dune liste passe en paramtre de droite, retournant toutes les combinaisons possibles dapplication de fonctions de la liste de gauche sur des valeurs de la liste de droite. ghci> [(+1),(*100),(*5)] <*> [1,2,3] [2,3,4,100,200,300,5,10,15]

La deuxime manire consiste prendre la premire fonction de la liste gauche de <*> , et de lappliquer la premire valeur de la liste de droite, puis prendre la deuxime fonction gauche et lappliquer la deuxime valeur droite, et ainsi de suite. Finalement, cest comme zipper les deux listes ensemble. Mais les listes sont dj des instances d Applicative , alors comment les faire aussi instances d Applicative de lautre manire ? Si vous vous souvenez, on a dit que la type ZipList a tait introduit pour cette raison, avec un seul constructeur de valeurs, ZipList , contenant un seul champ. On place la liste quon enveloppe dans ce champ. Ainsi, ZipList est fait instance d Applicative , de manire ce que si lon souhaite utiliser des listes comme foncteurs applicatifs de la manire zip, on ait juste envelopper la liste dans le constructeur ZipList , puis, une fois les calculs termins, l sortir avec getZipList : ghci> getZipList $ ZipList [(+1),(*100),(*5)] <*> ZipList [1,2,3] [2,200,15]

Donc, que fait le nouveau mot-cl newtype ? Eh bien, rflchissez la faon dont vous cririez la dclaration data du type ZipList a . Une faon de lcrire serait : data ZipList a = ZipList [a]

Un type qui na quun constructeur de valeurs, et ce constructeur de valeurs na quun champ, qui est une liste de choses. On aurait aussi pu vouloir utiliser la syntaxe des enregistrements pour obtenir automatiquement une fonction qui extrait une liste dune ZipList : data ZipList a = ZipList { getZipList :: [a] }

Cela a lair correct, et fonctionnerait en fait plutt bien. Puisquon avait deux manires de faire dun type existant une instance dune classe de types, on a utilis le mot-cl data pour juste envelopper ce type dans un autre type, et fait de ce second type une instance de la seconde manire. Le mot-cl newtype en Haskell est fait exactement pour ces cas o lon veut juste prendre un type et lencapsuler dans quelque chose pour le prsenter comme un nouveau type. Dans la bibliothque, ZipList a est dfinie comme : newtype ZipList a = ZipList { getZipList :: [a] }

Plutt que dutiliser le mot-cl data, on utilise le mot-cl newtype. Pourquoi cela ? Eh bien, premirement, newtype est plus rapide. Quand vous utilisez le mot-cl data pour envelopper un type, il y aura un cot lexcution pour faire lencapsulage et le dcapsulage. Alors quavec le mot-cle newtype, Haskell sait que vous utilisez cela juste pour encapsuler un type existant dans un nouveau type (do le nom du mot-cl), parce que vous voulez la mme chose en interne, mais avec un nom (type) diffrent. Avec cela en tte, Haskell peut se dbarasser de lemballage et du dballage une fois quil a tenu compte du type de chaque valeur pour savoir quelle instance utiliser. Pourquoi ne pas utiliser newtype tout le temps plutt que data dans ce cas ? Eh bien, quand vous crez un nouveau type partir dun type existant en utilisant le mot-cl newtype, vous ne pouvez avoir quun seul constructeur de valeurs, et ce constructeur de valeurs ne peut avoir quun seul champ. Alors quavec data, vous pouvez crer des types de donnes qui ont plusieurs constructeurs de valeurs, chacun pouvant avoir zro ou plusieurs champs : data Profession = Fighter | Archer | Accountant data Race = Human | Elf | Orc | Goblin data PlayerCharacter = PlayerCharacter Race Profession

Quand on utilise newtype, on est restreint un seul constructeur avec un seul champ. On peut galement utiliser le mot-cl deriving avec newtype, comme on le fait avec data. On peut driver les instances d Eq , Ord , Enum , Bounded , Show et Read . Si lon souhaite driver une instance dune classe de types, le type quon enveloppe doit lui-mme tre membre de cette classe. Cest logique, parce que newtype ne fait quenvelopper ce type. On peut donc afficher et tester lgalit de valeurs de notre nouveau type en faisant : newtype CharList = CharList { getCharList :: [Char] } deriving (Eq, Show)

Essayons : ghci> CharList "this will be shown!" CharList {getCharList = "this will be shown!"} ghci> CharList "benny" == CharList "benny" True ghci> CharList "benny" == CharList "oisters" False

Pour ce newtype, le constructeur de valeurs a pour type : CharList :: [Char] -> CharList

Il prend une valeur [Char] , comme "my sharona" , et retourne une valeur CharList . Dans les exemples ci-dessus o lon utilisait le constructeur de valeurs CharList , on voit que cest le cas. Rciproquement, la fonction getCharList , qui a t gnre lorsquon a utilis la syntaxe des enregistrements dans notre newtype, a pour type : getCharList :: CharList -> [Char]

Elle prend une valeur CharList et la convertit en [Char] . Vous pouvez imaginer ceci comme emballer et dballer, mais vous pouvez aussi limaginer comme une conversion dun type vers lautre.

Utiliser newtype pour crer des instances de classes de types


Souvent, on veut faire de nos types des instances de certaines classes de types, mais les paramtres de types ne correspondent pas ce que lon souhaite. Il est facile de faire une instance de Functor pour Maybe , parce que la classe de types Functor est dfinie comme : class Functor f where fmap :: (a -> b) -> f a -> f b

Donc on peut dmarrer en faisant : instance Functor Maybe where

Et implmenter fmap . Tous les paramtres de type salignent parce que Maybe vient prendre la place de f dans la dfinition de la classe de types Functor et donc, si lon considre fmap comme vu des Maybe , elle se comporte ainsi : fmap :: (a -> b) -> Maybe a -> Maybe b

Nest-ce pas chouette ? Maintenant, si lon voulait faire des tuples une instance de Functor de manire ce que si lon applique fmap sur un tuple, elle est applique la premire composante du tuple ? Ainsi, faire fmap (+3) (1, 1) donnerait (4, 1) . Il savre qucrire cette instance est plutt difficile. Avec Maybe , on disait juste instance Functor Maybe where parce que seuls les constructeurs de types prenant exactement un paramtre peuvent tre des instances de Functor . Mais cela semble impossible pour quelque chose comme (a, b) de manire ce que ce soit a qui change quand on utilise fmap . Pour contourner ce problme, on peut envelopper notre tuple dans un newtype de manire ce que le deuxime paramtre de type reprsente la premire composante du tuple : newtype Pair b a = Pair { getPair :: (a,b) }

prsent, on peut en faire une instance de Functor qui mappe la fonction sur la premire composante : instance Functor (Pair c) where

fmap f (Pair (x,y)) = Pair (f x, y)

Comme vous le voyez, on peut filtrer par motif les types dfinis par newtype. On filtre par motif pour obtenir le tuple sous-jacent, puis on applique f la premire composante de ce tuple, et on utilise le constructeur de valeurs Pair pour convertir nouveau le tuple en Pair b a . Le type de fmap si elle ne marchait que sur nos paires serait : fmap :: (a -> b) -> Pair c a -> Pair c b

nouveau, on a dit instance Functor (Pair c) where , et ainsi, Pair c prend la place de f dans la dfinition de classe de Functor : class Functor f where fmap :: (a -> b) -> f a -> f b

Maintenant, si lon convertit un tuple en une Pair b a , on peut utiliser fmap sur celle-ci et la fonction sera mappe sur la premire composante : ghci> getPair $ fmap (*100) (Pair (2,3)) (200,3) ghci> getPair $ fmap reverse (Pair ("london calling", 3)) ("gnillac nodnol",3)

De la paresse de newtype
On a mentionn le fait que newtype tait gnralement plus rapide que data. La seule chose possible avec newtype consiste transformer un type existant en un autre type, donc en interne, Haskell peut reprsenter les valeurs des types dfinis par newtype de la mme faon que celles du type original, et na qu se souvenir que leur type est distinct. Ceci fait quen plus dtre plus rapide, newtype est galement plus paresseux. Regardons ce que cela signifie. Comme on la dit prcdemment, Haskell est paresseux par dfaut, ce qui signifie que cest seulement lorsque lon essaie dafficher les rsultats de fonctions que ceux-ci sont calculs. De plus, seuls les calculs ncessaires pour trouver le rsultat de notre fonction sont excuts. La valeur undefined en Haskell reprsente un calcul erron. Si on essaie de lvaluer (cest--dire, de forcer Haskell la calculer) en laffichant sur le terminal, Haskell piquera une crise (ou en termes techniques, lvera une exception) : ghci> undefined *** Exception: Prelude.undefined

Cependant, si on cre une liste qui contient quelques valeurs undefined mais quon ne demande que sa tte, qui nest pas undefined , tout se passera bien parce quHaskell na pas besoin dvaluer les autres lments de la liste pour trouver son premier lment : ghci> head [3,4,5,undefined,2,undefined] 3

prsent, considrez ce type : data CoolBool = CoolBool { getCoolBool :: Bool }

Un type de donnes algbrique tout droit sorti de lusine, dfini via le mot-cl data. Il na quun constructeur de valeurs, qui na quun champ de type Bool . Crons une fonction qui filtre par motif un CoolBool et retourne la valeur "hello" quelle que soit la valeur du boolen dans CoolBool : helloMe :: CoolBool -> String helloMe (CoolBool _) = "hello"

Plutt que dappliquer cette fonction sur une valeur CoolBool normale, envoyons lui plutt une balle courbe en lappliquant sur un undefined ! ghci> helloMe undefined "*** Exception: Prelude.undefined

Ae ! Une exception ! Pourquoi est-ce que cette exception a t leve ? Les types dfinis avec le mot-cl data peuvent avoir plusieurs constructeurs de valeurs (bien que CoolBool nen ait quun). Ainsi, pour savoir si la valeur donne notre fonction correspond au motif (CoolBool _) , Haskell doit lvaluer juste assez pour voir quel constructeur de valeur a t utilis pour la crer. Et lorsquon essaie dvaluer la valeur undefined , mme un tout petit peu, une exception est leve. Plutt que le mot-cl data pour CoolBool , essayons dutiliser newtype :

newtype CoolBool = CoolBool { getCoolBool :: Bool }

Pas besoin de changer la fonction helloMe , parce que la syntaxe de filtrage par motif est la mme pour newtype que pour data. Faisons-la mme chose ici et appliquons helloMe une valeur undefined : ghci> helloMe undefined "hello"

a a march ! Hmmm, pourquoi cela ? Eh bien, comme on la dit, quand on utilise newtype , Haskell peut reprsenter en interne les valeurs du nouveau type de la mme faon que les valeurs originales. Il na pas besoin dajouter une nouvelle bote autour, mais seulement de se rappeler du fait que les valeurs ont un type diffrent. Et parce quHaskell sait que les types crs avec le mot-cl newtype ne peuvent avoir quun seul constructeur, il na pas besoin dvaluer la valeur passe la fonction pour savoir quelle se conforme au motif (CoolBool _) , puisque les types crs avec newtype ne peuvent avoir quun seul constructeur de valeurs et quun seul champ ! Cette diffrence de comportement peut sembler triviale, mais elle est en fait assez importante pour nous aider raliser que, bien que les types crs avec data et newtype se comportent de faon similaire du point de vue du programmeur parce quils ont chacun des constructeurs de valeurs et des champs, ils sont en fait deux mcanismes diffrents. Alors que data peut crer de nouveaux types partir de rien, newtype ne peut crer de nouveau type qu partir dun type existant. Le filtrage par motif sur les valeurs newtype ne sort pas les choses dune bote (comme le fait data), mais convertit plutt un type en un autre immdiatement.

type vs. newtype vs. data


ce point, vous tes peut-tre perdu concernant les diffrences entre type, data et newtype, alors rafrachissons-nous la mmoire. Le mot-cl type cre des synonymes de types. Cela signifie quon donne simplement un autre nom un type existant, pour quil soit plus simple den parler. Par exemple, ceci : type IntList = [Int]

Tout ce que cela fait, cest nous permettre de faire rfrence [Int] en tapant IntList . Les deux peuvent tre utiliss de manire interchangeable. On nobtient pas de constructeur de valeurs IntList ou quoi que ce soit du genre. Puisque [Int] et IntList sont deux faons de parler du mme 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 lorsquon 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 utiliss. Par exemple, quand on a utilis une liste associative qui a pour type [(String,String)] pour reprsenter un rpertoire tlphonique, 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 cration dinstances de certaines classes de types. Quand on utilise newtype pour envelopper un type existant, le type quon obtient est diffrent du type original. Si lon cre 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 mme pas utiliser ++ pour coller ensemble deux CharList , parce que ++ ne fonctionne que pour les listes, et le type de CharList nest pas une liste, mme si lon peut dire quil 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 dclaration 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 lautre sens, avec la fonction qui extrait la valeur du champ. Le nouveau type nest pas automatiquement fait instance des classes de types du type original, donc il faut les driver ou les crire manuellement. En pratique, on peut imaginer les dclarations newtype comme des dclarations data qui nont quun constructeur de valeurs, et un seul champ. Si vous vous retrouvez crire une telle dclaration data, pensez utiliser newtype. Le mot-cl data sert crer vos propres types de donnes, 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 utiliss pour implmenter nimporte quel type de donnes algbrique vous-mme. 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 lenvelopper dans un nouveau type pour en faire une instance dune classe de types, vous voulez srement un newtype. Et si vous voulez crer quelque chose de neuf, les chances sont bonnes que le mot-cl data soit ce quil vous faut.

Monodes
Les classes de types en Haskell sont uilises pour prsenter 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 lgalit, et Ord pour ceux quon peut mettre en ordre, puis on est pass des classes plus intressantes, comme Functor et Applicative . Lorsquon cre un type, on rflchit aux comportements quil supporte, i.e. pour quoi peut-il se faire passer, et partir de cela, on dcide de quelles classes de types on en fera une instance. Si cela a un sens de tester lgalit de nos valeurs, alors on cre une instance d Eq . Si lon voit que notre type est un foncteur, alors on cre une instance de Functor , et ainsi de suite. Maintenant, considrez cela : * est une fonction qui prend deux nombres et les multiplie entre eux. Si lon multiplie un nombre par 1 , le rsultat est toujours gal ce nombre. Peu importe quon fasse 1 * x ou x * 1 , le rsultat est toujours x . De faon similaire, ++ est aussi une fonction qui prend deux choses et en retourne une troisime. Au lieu de les multiplier, elle les concatne. Et tout comme * , elle a aussi une valeur qui ne change pas lautre 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 proprit commune avec ++ et [] : La fonction prend deux paramtres. Les paramtres et la valeur retourne ont le mme type. Il existe une valeur de ce type qui ne change pas lautre valeur lorsquelle est applique la fonction binaire. Autre chose que ces deux oprations ont en commun et qui nest pas si vident : lorsquon a trois valeurs ou plus et quon utilise la fonction binaire pour les rduire une seule valeur, lordre dans lequel on applique la fonction nimporte pas. Peu importe quon fasse (3 * 4) * 5 ou 3 * (4 * 5) . De toute manire, le rsultat est 60 . De mme 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 proprit lassociativit. * est associative, de mme que ++ , mais par exemple - ne lest pas. Les expressions (5 - 3) - 4 et 5 - (3 - 4) nont pas le mme rsultat. En remarquant ces proprits et en les crivant, on vient de tomber sur les monodes ! On a un monode lorsquon dispose dune fonction binaire associative et dun lment neutre pour cette fonction. Quand quelque chose est un lment neutre pour une fonction, cela signifie que lorsquon appelle la fonction avec cette chose et une autre valeur, le rsultat est toujours gal lautre valeur. 1 est llment neutre vis--vis de * , et [] est le neutre de ++ . Il y a beaucoup dautres monodes trouver dans le monde dHaskell, cest pourquoi la classe de types Monoid existe. Elle est pour ces types qui peuvent agir comme des monodes. Voyons comment la classe de types est dfinie :

class Monoid m where mempty :: m mappend :: m -> m -> m mconcat :: [m] -> m mconcat = foldr mappend mempty

La classe de types Monoid est dfinie dans Data.Monoid . Prenons un peu de temps pour la dcouvrir. Tout dabord, on voit que seuls des types concrets peuvent tre faits instance de Monoid , parce que le m de la dfinition de classe ne prend pas de paramtres. Cest diffrent de Functor et Applicative qui ncessitent que leurs instances soient des constructeurs de types prenant un paramtre. La premire fonction est mempty . Ce nest pas vraiment une fonction puisquelle ne prend pas de paramtres, cest plutt une constante polymorphique, un peu comme minBound de Bounded . mempty reprsente llment neutre de ce monode. Ensuite, on a mappend , qui, comme vous lavez probablement devin, est la fonction binaire. Elle prend deux valeurs du mme type et retourne une valeur de ce type. Il est bon de mentionner que la dcision dappeler mappend de la sorte est un peu malheureuse, parce que ce nom semble impliquer quon 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 dautres instances de Monoid , on verra que la plupart dentre elles ne juxtaposent pas non plus leurs valeurs, vitez donc de penser en termes de juxtaposition, et pensez plutt que mappend est une fonction binaire qui prend deux valeurs monodales et en retourne une troisime. La dernire fonction de la dfinition de la classe de types est mconcat . Elle prend une liste de valeurs monodales et les rduit une unique valeur en faisant mappend entre les lments de la liste. Elle a une implmentation par dfaut, qui prend mempty comme valeur initiale et plie la liste depuis la droite avec mappend . Puisque limplmentation par dfaut convient pour la plupart des instances, on ne se souciera pas trop de mconcat pour linstant. Quand on cre une instance de Monoid , il suffit dimplmenter mempty et mappend . La raison de la prsence de mconcat ici est que, pour certaines instances, il peut y avoir une manire plus efficace de limplmenter, mais pour la plupart des instances, limplmentation par dfaut convient. Avant de voir des instances spcifiques de Monoid , regardons brivement les lois des monodes. On a mentionn quil devait exister une valeur neutre vis--vis de la fonction binaire, et que la fonction binaire devait tre associative. Il est possible de crer des instances de Monoid ne suivant pas ces rgles, 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 monodes. Sinon, quoi cela servirait-il ? Cest pourquoi, en crant des instances, on doit sassurer quelles obissent ces lois :

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


Les deux premires dclarent que mempty doit tre le neutre de mappend , la troisime dit que mappend doit tre associative, i.e. lordre dans lequel on applique mappend pour rduire plusieurs valeurs monodales en une nimporte pas. Haskell ne fait pas respecter ces lois, cest donc nous en tant que programmeur de faire attention ce que nos instances y obissent.

Les listes sont des monodes


Oui, les listes sont des monodes ! Comme on la vu, la fonction ++ et la liste vide [] forment un monode. Linstance est trs simple : instance Monoid [a] where mempty = [] mappend = (++)

Les listes sont une instance de la classe de types Monoid quel que soit le type des lments quelles contiennent. Remarquez quon a crit instance Monoid [a] et pas instance Monoid [] , parce que Monoid ncessite 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 dernire ligne, nous avons d crire une annotation de type explicite, parce que si lon faisait seulement mempty , GHCi ne saurait pas quelle instance utiliser, on indique donc quon veut celle des listes. On a pu utiliser le type gnral [a] (plutt quun type spcifique comme [Int] ou [String] ) parce que la liste vide peut se faire passer pour nimporte quel type de liste. Puisque mconcat a une implmentation par dfaut, on lobtient gratuitement lorsquon cre une instance de Monoid . Dans le cas des listes, mconcat savre tre juste concat . Elle prend une liste de listes et laplatit, parce que cest quivalent faire ++ entre toutes les listes adjacentes dune liste. Les lois des monodes sont en effet respectes par linstance des listes. Quand on a plusieurs listes et quon les mappend (ou ++ ) ensemble, peu importe lordre dans lequel lopration est effectue, parce quau final, elles sont toutes la suite lune de lautre de toute faon. De plus, la liste vide se comporte bien comme un lment neutre, donc tout va bien. Remarquez que les monodes ne ncessitent pas que a `mappend` b soit gal b `mappend` a . Dans le cas des listes, clairement ce nest pas le cas. ghci> "one" `mappend` "two" "onetwo" ghci> "two" `mappend` "one" "twoone"

Et ce nest pas un problme. Le fait que pour la multiplication, 3 * 5 et 5 * 3 soient identiques nest quune proprit de la multiplication, mais na pas tre vrai pour tous les monodes (et ne lest pas pour la plupart dailleurs).

Product et Sum
On a dj examin une faon pour les nombres dtre considrs comme des monodes. La fonction binaire est * et llment neutre est 1 . Il savre que ce nest pas la seule faon pour les nombres de former un monode. Une autre faon consiste prendre pour fonction binaire + et pour lment neutre 0 . ghci> 4 ghci> 5 ghci> 9 ghci> 9 0 + 4 5 + 0 (1 + 3) + 5 1 + (3 + 5)

Les lois des monodes tiennent, parce quajouter 0 tout nombre le laisse inchang. Et laddition est associative, donc pas de problme. prsent quil y a deux manires toute aussi valide lune que lautre pour les nombres dtre des monodes, quelle voie choisir ? Eh bien, pas besoin de choisir. Souvenez-vous, quand il y a plusieurs faons pour un type dtre une instance de la mme classe de types, on peut lenvelopper dans un newtype et faire de ce nouveau type une autre instance de la mme classe de types. On a le beurre et largent du beurre. Le module Data.Monoid exporte deux types pour cela, Product et Sum . Product est dfini comme : newtype Product a = Product { getProduct :: a } deriving (Eq, Ord, Read, Show, Bounded)

Simple, un enrobage avec newtype, avec un paramtre de type et quelques instance drives. Son instance de Monoid ressemble : instance Num a => Monoid (Product a) where mempty = Product 1 Product x `mappend` Product y = Product (x * y)

mempty est simplement 1 envelopp dans un constructeur Product . mappend filtre par motif sur le constructeur Product , multiplie les deux nombres, et enveloppe nouveau le rsultat. Comme vous le voyez, il y a une contrainte de classe Num a . Cela veut dire que Product a est une instance de Monoid pour tout a qui est une instance de Num . Pour utiliser Product a en tant que monode, il faut faire un peu demballage et de dballage. ghci> getProduct $ Product 3 `mappend` Product 9 27 ghci> getProduct $ Product 3 `mappend` mempty 3 ghci> getProduct $ Product 3 `mappend` Product 4 `mappend` Product 2

24 ghci> getProduct . mconcat . map Product $ [3,4,2] 24

Cest pratique pour dmontrer le comportement de la classe de types Monoid , mais personne de bien sens nirait multiplier des nombres de cette faon plutt que dcrire simplement 3 * 9 et 3 * 1 . Mais un peu plus tard, nous verrons comment ces instances de Monoid qui peuvent sembler triviales savrent pratiques. Sum est dfini comme Product et linstance est galement similaire. On lutilise ainsi : ghci> getSum $ Sum 2 `mappend` Sum 9 11 ghci> getSum $ mempty `mappend` Sum 3 3 ghci> getSum . mconcat . map Sum $ [1,2,3] 6

Any et All
Un autre type qui peut agir comme un monode de deux faons distinctes et lgitimes est Bool . La premire faon utilise la fonction ou || comme fonction binaire, avec False pour lment neutre. En logique, ou est True si nimporte lequel de ses paramtres est True , sinon cest False . Ainsi, en utilisant False comme lment neutre, on retourne bien False en faisant ou avec False , et True en faisant ou avec True . Le newtype Any est une instance de monode de cette manire. Il est dfini ainsi : newtype Any = Any { getAny :: Bool } deriving (Eq, Ord, Read, Show, Bounded)

Quant linstance : instance Monoid Any where mempty = Any False Any x `mappend` Any y = Any (x || y)

Il sappelle Any parce que x `mappend` y sera True si nimporte laquelle (NDT : en anglais, any) de ses valeurs est True . Mme si trois ou plus Bool envelopps dans Any sont mappend ensemble, le rsultat sera toujours True lorsque nimporte lequel dentre eux est True : ghci> True ghci> True ghci> True ghci> False getAny $ Any True `mappend` Any False getAny $ mempty `mappend` Any True getAny . mconcat . map Any $ [False, False, False, True] getAny $ mempty `mappend` mempty

Lautre faon de faire une instance de Monoid pour Bool est un peu loppos : utiliser && comme fonction binaire et True comme lment neutre. Le et logique retourne True si ses deux paramtres valent True . Voici la dclaration newtype, rien dextraordinaire : newtype All = All { getAll :: Bool } deriving (Eq, Ord, Read, Show, Bounded)

Et linstance : instance Monoid All where mempty = All True All x `mappend` All y = All (x && y)

Quand on mappend des valeurs de type All , le rsultat sera True seulement si toutes (NDT : en anglais, all), les valeurs passes mappend sont True : ghci> True ghci> False ghci> True ghci> getAll $ mempty `mappend` All True getAll $ mempty `mappend` All False getAll . mconcat . map All $ [True, True, True] getAll . mconcat . map All $ [True, True, False]

False

Tout comme avec la multiplication et laddition, on utilise gnralement les fonctions binaires directement plutt que denvelopper les valeurs dans des newtype pour ensuite utiliser mappend et mempty . mconcat semble utile pour Any et All mais gnralement, il est plus simple dutiliser les fonctions or et and qui prennent une liste de Bool et retourne True si, respectivement, lune delles est True ou toutes sont True .

Le monode Ordering
H, vous vous souvenez du type Ordering ? Cest le type retourn lorsquon compare des choses, qui peut avoir trois valeurs : LT , EQ et GT , qui signifient infrieur, gal et suprieur respectivement : ghci> 1 `compare` 2 LT ghci> 2 `compare` 2 EQ ghci> 3 `compare` 2 GT

Avec les listes, les nombres et les valeurs boolennes, on voit que trouver des monodes est juste laffaire de regarder ce quon utilisait dj afin de voir si ces choses prsentent un comportement monodal. Pour Ordering , il faut y regarder deux fois avant de reconnatre le monode, mais il savre que son instance de Monoid est tout aussi intuitive que celles recontres prcdemment, et plutt utile galement : instance Monoid Ordering where mempty = EQ LT `mappend` _ = LT EQ `mappend` y = y GT `mappend` _ = GT

Linstance est cre de la sorte : quand on mappend deux valeurs Ordering , celle de gauche est prserve, moins que la valeur gauche soit EQ , auquel cas celle de droite est le rsultat. Le neutre est EQ . Au dpart, a peut sembler arbitraire, mais cela ressemble en fait beaucoup la faon donc on compare lexicographiquement des mots. On compare dabord les premires lettres de chaque mot, et si elles diffrent, on peut immdiatement dcider de lordre des mots dans un dictionnaire. Cependant, si elles sont gales, alors on doit comparer la prochaine paire de lettres et rpter le procd. Par exemple, si lon voulait comparer lexicographiquement les mots ox et on, on pourrait commencer par comparer la premire lettre de chaque mot, voir quelles sont gales, puis comparer la seconde lettre de chaque mot. On voit que 'x' est lexicographiquement suprieure 'n' , et on peut donc savoir lordre des deux mots. Pour se faire une intuition sur le fait qu EQ soit lidentit, on peut remarquer que si lon glissait la mme lettre la mme position dans les deux mots, leur ordre lexicographique ne changerait pas. "oix" est toujours lexicographiquement suprieur "oin" . Il est important de noter que dans linstance de Monoid pour Ordering , x `mappend` y nest pas gal y `mappend` x . Puisque le premier paramtre est conserv moins dtre EQ , LT `mappend` GT retourne LT , alors que GT `mappend` LT retournera GT : ghci> LT ghci> GT ghci> LT ghci> GT LT `mappend` GT GT `mappend` LT mempty `mappend` LT mempty `mappend` GT

OK, comment est-ce que ce monode est utile ? Mettons que vous soiyez en train dcrire une fonction qui prend deux chanes de caractres, compare leur longueur, et retourne un Ordering . Mais si les chanes sont de mme longueur, au lieu de retourner EQ immdiatement, vous voulez les comparer lexicographiquement. Une faon de faire serait : lengthCompare :: String -> String -> Ordering lengthCompare x y = let a = length x `compare` length y b = x `compare` y in if a == EQ then b else a

On appelle a le rsultat de la comparaison des longueurs, et b celui de la comparaison lexicographique, et si les longueurs savrent gales, on retourne lordre

lexicographique. Mais, en mettant profit notre comprhension d Ordering comme un monode, on peut rcrire cette fonction dune manire bien plus simple : import Data.Monoid lengthCompare :: String -> String -> Ordering lengthCompare x y = (length x `compare` length y) `mappend` (x `compare` y)

On peut essayer : ghci> lengthCompare "zen" "ants" LT ghci> lengthCompare "zen" "ant" GT

Souvenez-vous, quand on fait mappend , le paramtre de gauche est toujours gard moins dtre EQ , auquel cas celui de droite est gard. Cest pourquoi on a plac la comparaison quon considre comme le critre le plus important en premier paramtre. Si lon voulait tendre la fonction pour comparer le nombre de voyelles comme deuxime critre en importance, on pourrait modifier la fonction ainsi : import Data.Monoid lengthCompare :: String -> String -> Ordering lengthCompare x y = (length x `compare` length y) `mappend` (vowels x `compare` vowels y) `mappend` (x `compare` y) where vowels = length . filter (`elem` "aeiou")

On a fait une fonction auxiliaire qui prend une chane de caractres et nous dit combien de voyelles elle contient en filtrant seulement les lettres qui sont dans la chane "aeiou" , puis en appliquant length au rsultat filtr. ghci> lengthCompare "zen" "anna" LT ghci> lengthCompare "zen" "ana" LT ghci> lengthCompare "zen" "ann" GT

Trs cool. Ici, on voit comme dans le premier exemple les longueurs sont diffrentes et donc LT est retourn, parce que "zen" est moins longue qu "anna" . Dans le deuxime exemple, les longueurs sont gales, mais la deuxime chane a plus de voyelles, donc LT est retourn nouveau. Dans le troisime exemple, elles ont la mme longueur et le mme nombre de voyelles, donc elles sont compares lexicographiquement et "zen" gagne. Le monode Ordering est trs cool parce quil permet de comparer facilement des choses selon plusieurs critres en plaant ces critres dans lordre du plus important au moins important.

Maybe le monode
Regardons les diffrentes faons de faire de Maybe une instance de Monoid , et comment ces instances sont utiles. Une faon est de traiter Maybe comme un monode seulement lorsque son paramtre de type a est un monode galement, et dimplmenter mappend de faon ce quil utilise lopration mappend des valeurs enveloppes dans le Just . Nothing est llment neutre, et ainsi si lune des deux valeurs quon mappend est Nothing , on conserve lautre valeur. Voici la dclaration dinstance : instance Monoid a => Monoid (Maybe a) where mempty = Nothing Nothing `mappend` m = m m `mappend` Nothing = m Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)

Remarquez la contrainte de classe. Elle dit que Maybe a est une instance de Monoid seulement si a est une instance de Monoid . Si lon mappend quelque chose avec Nothing , le rsultat est cette chose. Si on mappend deux valeurs Just , le contenu des Just est mappend et est envelopp de nouveau dans un Just . On peut faire cela parce que la contrainte de classe nous garantit que le type lintrieur du Just est une instance de Monoid . ghci> Nothing `mappend` Just "andy" Just "andy" ghci> Just LT `mappend` Nothing

Just LT ghci> Just (Sum 3) `mappend` Just (Sum 4) Just (Sum {getSum = 7})

Cela savre utile lorsque vous utilisez des monodes comme rsultats de calculs pouvant chouer. Grce cette instance, on na pas besoin de vrifier si les calculs ont chou en regardant si les valeurs sont Nothing ou Just , on peut simplement continuer les traiter comme des monodes ordinaires. Mais, et si le type du contenu de Maybe nest pas une instance de Monoid ? Remarquez que dans la dclaration dinstance prcdente, on ne se repose sur le fait que le contenu soit un monode que lorsque mappend est appliqu deux valeurs Just . Mais si lon ne sait pas si le contenu est un monode, on ne peut pas faire mappend sur ces valeurs, donc que faire ? Eh bien, on peut par exemple toujours jeter la deuxime valeur et garder la premire. Pour cela, le type First a existe, et voici sa dfinition : newtype First a = First { getFirst :: Maybe a } deriving (Eq, Ord, Read, Show)

On prend un Maybe a et on lenveloppe avec un newtype. Linstance de Monoid est comme suit : instance Monoid (First a) where mempty = First Nothing First (Just x) `mappend` _ = First (Just x) First Nothing `mappend` x = x

Comme on la dit. mempty est Nothing emball dans un constructeur de newtype First . Si le premier paramtre de mappend est une valeur Just , on ignore le second. Si le premier est Nothing , alors on retourne lautre, quil soit un Just ou un Nothing . ghci> getFirst $ First (Just 'a') `mappend` First (Just 'b') Just 'a' ghci> getFirst $ First Nothing `mappend` First (Just 'b') Just 'b' ghci> getFirst $ First (Just 'a') `mappend` First Nothing Just 'a'

First est utile quand on a plein de valeurs Maybe et quon souhaite juste savoir sil y a un Just dans le tas. La fonction mconcat savre utile : ghci> getFirst . mconcat . map First $ [Nothing, Just 9, Just 10] Just 9

Si lon veut un monode sur Maybe a de manire ce que le deuxime paramtre soit gard lorsque les deux paramtres de mappend sont des valeurs Just , Data.Monoid fournit un type Last a , qui fonctionne comme First a , lexception que cest la dernire valeur diffrente de Nothing qui est garde par mappend et mconcat : ghci> getLast . mconcat . map Last $ [Nothing, Just 9, Just 10] Just 10 ghci> getLast $ Last (Just "one") `mappend` Last (Just "two") Just "two"

Utiliser des monodes pour plier des structures de donnes


Une des faons les plus intressantes de mettre les monodes lusage est de les utiliser pour nous aider dfinir des plis sur diverses structures de donnes. Jusquici, nous navons fait que des plis sur des listes, mais les listes ne sont pas les seules structures de donnes pliables. On peut dfinir des plis sur presque toute structure de donnes. Les arbres se prtent particulirement bien lexercice du pli. Puisquil y a tellement de structures de donnes qui fonctionnent bien avec les plis, la classe de types Foldable a t introduite. Comme Functor est pour les choses sur lequelles on peut mapper, Foldable est pour les choses qui peuvent tre plies ! Elle est trouvable dans Data.Foldable et puisquelle exporte des fonctions dont les noms sont en collision avec ceux du Prelude , elle est prfrablement importe qualifie (et servie avec du basilic) : import qualified Foldable as F

Pour nous viter de prcieuses frappes de clavier, on limporte qualifie par F . Bien, donc quelles sont les fonctions que cette classe de types dfinit ? Eh bien, parmi celles-ci sont foldr , foldl , foldr1 , foldl1 . Hein ? Mais on connat dj ces fonctions, quoi de neuf ? Comparons le type de foldr dans Foldable avec celui de foldr du Prelude pour voir ce qui diffre : ghci> :t foldr

foldr :: (a -> b -> b) -> b -> [a] -> b ghci> :t F.foldr F.foldr :: (F.Foldable t) => (a -> b -> b) -> b -> t a -> b

Ah ! Donc, alors que foldr prend une liste et la plie, le foldr de Data.Foldable accepte tout type qui peut tre pli, pas seulement les listes ! Comme on peut sy attendre, les deux fonctions font la mme chose sur les listes : ghci> foldr (*) 1 [1,2,3] 6 ghci> F.foldr (*) 1 [1,2,3] 6

Ok, quelles autres structures peuvent tre plies ? Eh bien, le Maybe quon connat tous et quon aime tant ! ghci> F.foldl (+) 2 (Just 9) 11 ghci> F.foldr (||) False (Just True) True

Mais plier une simple valeur Maybe nest pas trs intressant, parce que quand il sagit de se plier, elle se comporte comme une liste avec un lment si cest un Just , et comme une liste vide si cest Nothing . Examinons donc une structure de donnes un peu plus complexe. Vous rappelez-vous la structure de donnes arborescente du chapitre Crer nos propres types et classes de types ? On lavait dfinie ainsi : data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq)

On disait quun arbre tait soit un arbre vide qui ne contient aucune valeur, soit un neud qui contient une valeur ainsi que deux autres arbres. Aprs lavoir dfini, on en a fait une instance de Functor et on a gagn la possibilit de faire fmap sur ce type. prsent, on va en faire une instance de Foldable afin de pouvoir le plier. Une faon de faire dun constructeur de types une instance de Foldable consiste implmenter directement foldr . Une autre faon, souvent bien plus simple, consiste implmenter la fonction foldMap , qui fait aussi partie de la classe de types Foldable . foldMap a pour type : foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

Son premier paramtre est une fonction qui prend une valeur du type que notre structure de donnes pliable contient (ici dnot a ) et retourne une valeur monodale. Son second paramtre est une structure pliable contenant des valeurs de type a . Elle mappe la fonction sur la structure pliable, produisant ainsi une structure pliable contenant des valeurs monodales. Ensuite, en faisant mappend entre toutes ces valeurs monodales, elle les joint en une unique valeur monodale. Cette fonction peut paratre bizarre pour linstant, mais on va voir quelle est trs simple implmenter. Ce qui est aussi cool, cest quil suffit simplement dimplmenter cette fonction pour que notre type soit une instance de Foldable . Ainsi, si lon implmente foldMap pour un type, on obtient foldr et foldl sans effort ! Voici comment un Tree est fait instance de Foldable : instance F.Foldable Tree where foldMap f Empty = mempty foldMap f (Node x l r) = F.foldMap f l `mappend` f x `mappend` F.foldMap f r

On pense ainsi : si lon nous donne une fonction qui prend un lment de notre arbre et retourne une valeur monodale, comment rduit-on notre arbre une simple valeur monodale ? Quand nous faisions fmap sur notre arbre, on appliquait la fonction mappe au nud, puis on mappait rcursivement la fonction sur le sous-arbre gauche et sur le sous-arbre droit. Ici, on nous demande non seulement de mapper la fonction, mais galement de joindre les rsultats en une simple valeur monodale laide de mappend . On considre dabord le cas de larbre vide - un pauvre arbre tout triste et solitaire, sans valeur ni sous-arbre. Il na pas de valeur quon puisse passer notre fonction crant des monodes, donc si notre arbre est vide, la valeur monodale sera mempty . Le cas dun nud non vide est un peu plus intressant. Il contient deux sous-arbres ainsi quune valeur. Dans ce cas, on foldMap rcursivement la mme fonction f sur les sous-arbres gauche et droit. Souvenez-vous, notre foldMap retourne une simple valeur monodale. On applique galement la fonction f la valeur du nud. prsent, nous avons trois valeurs monodales (deux venant des sous-arbres et une venant de lapplication de f sur la valeur du nud) et il suffit de les craser en une seule valeur. Pour ce faire, on utilise mappend , et

naturellement, le sous-arbre gauche vient avant la valeur du nud, suivi du sous-arbre droit. Remarquez quon na pas eu besoin dcrire la fonction qui prend une valeur et retourne une valeur monodale. On la reoit en paramtre de la fonction foldMap et tout ce quon a besoin de dcider est o lappliquer et comment joindre les monodes qui en rsultent. Maintenant quon a une instance de Foldable pour notre type arborescent, on a foldr et foldl gratuitement ! Considrez cet arbre : testTree = Node 5 (Node 3 (Node (Node ) (Node 9 (Node (Node )

1 Empty Empty) 6 Empty Empty)

8 Empty Empty) 10 Empty Empty)

Il a 5 pour racine, puis son nud gauche contient 3 avec 1 sa gauche et 6 sa droite. Le nud droit de la racine contient 9 avec un 8 sa gauche et un 10 tout droite. Avec une instance de Foldable , on peut faire tous les plis quon fait sur des listes : ghci> F.foldl (+) 0 testTree 42 ghci> F.foldl (*) 1 testTree 64800

foldMap ne sert pas qu crer les instances de Foldable , elle sert galement rduire une structure une simple valeur monodale. Par exemple, si lon voulait savoir si nimporte quel nombre de notre arbre est gal 3 , on pourrait faire : ghci> getAny $ F.foldMap (\x -> Any $ x == 3) testTree True

Ici, \x -> Any $ x == 3 est une fonction qui prend un nombre et retourne une valeur monodale, dans ce cas un Bool envelopp en Any . foldMap applique la fonction chaque lment de larbre, puis rduit les monodes rsultants en une unique valeur monodale laide de mappend . Si lon faisait : ghci> getAny $ F.foldMap (\x -> Any $ x > 15) testTree False

Tous les nuds de notre arbre contiendraient la valeur Any False aprs avoir appliqu la fonction dans la lambda chacun deux. Pour valoir True , mappend sur Any doit avoir au moins un True pass en paramtre. Cest pourquoi le rsultat final est False , ce qui est logique puisquaucune valeur de notre arbre nexcde 15 . On peut aussi facilement transformer notre arbre en une liste en faisant foldMap avec la fonction \x -> [] . En projetant dabord la fonction sur larbre, chaque lment devient une liste singleton. Puis, mappend a lieu entre tous ces singletons et retourne une unique liste qui contient tous les lments de notre arbre : ghci> F.foldMap (\x -> [x]) testTree [1,3,6,5,8,9,10]

Ce qui est vraiment cool, cest que toutes ces techniques ne sont pas limites aux arbres, elles fonctionnent pour toute instance de Foldable .

Rsoudre des problmes fonctionnellement

Table des matires

Pour une poigne de monades

Pour une poigne de monades


Foncteurs, foncteurs applicatifs et monodes Table des matires Et pour quelques monades de plus Quand on a parl pour la premire fois des foncteurs, on a vu que ctait un concept utile pour les valeurs sur lesquelles on pouvait mapper. Puis, on a pouss ce concept un cran plus loin en introduisant les foncteurs applicatifs, qui nous permettent de voir les valeurs de certains types de donnes comme des valeurs dans des contextes et dutiliser des fonctions normales sur ces valeurs tout en prservant la smantique de ces contextes. Dans ce chapitre, nous allons dcouvrir les monades, qui sont juste des foncteurs applicatifs gonfls aux hormones, tout comme les foncteurs applicatifs taient juste des foncteurs gonfls aux hormones. Quand on a dbut les foncteurs, on a vu quil tait possible de mapper des fonctions sur divers types de donnes. On a vu qu cet effet, la classe de types Functor avait t introduite, et cela a soulev la question : quand on a une fonction de type a -> b et un type de donnes f a , comment mapper cette fonction sur le type de donnes pour obtenir un f b ? On a vu comment mapper quelque chose sur un Maybe a , une liste [a] , une action I/O IO a , etc. On a mme vu comment mapper une fonction a -> b sur dautres fonctions de type r -> a pour obtenir des fonctions r -> b . Pour rpondre la question de comment mapper une fonction sur un type de donnes, tout ce quil nous a fallu considrer tait le type de fmap : fmap :: (Functor f) => (a -> b) -> f a -> f b

Puis, le faire marcher pour notre type de donnes en crivant linstance de Functor approprie. Puis, on a vu une amlioration possible des foncteurs, et nous nous sommes dit, h, et si cette fonction a -> b tait dj enveloppe dans une valeur fonctorielle ? Comme par exemple, si lon avait Just (*3) , comment appliquer cela Just 5 ? Et si lon voulait lappliquer non pas Just 5 , mais plutt Nothing ? Ou si lon avait [(*2), (+4)] , comment lappliquerions-nous [1, 2, 3] ? Comment cela marcherait-il ? Pour cela, la classe de types Applicative est introduite, afin de rpondre au problme laide du type : (<*>) :: (Applicative f) => f (a -> b) -> f a -> f b

On a aussi vu que lon pouvait prendre une valeur normale et lenvelopper dans un type de donnes. Par exemple, on peut prendre un 1 et lenvelopper de manire ce quil devienne un Just 1 . On en faire un [1] . Ou une action I/O qui ne fait rien, mais retourne 1 . La fonction qui fait cela sappelle pure . Comme on la dit, une valeur applicative peut tre vue comme une valeur avec un contexte. Une valeur spciale en gros. Par exemple, le caractre 'a' est un caractre normal, alors que Just 'a' a un contexte additionnel. Au lieu dtre un Char , cest un Maybe Char , ce qui nous indique que sa valeur peut tre un caractre, mais quil aurait aussi pu tre absent. La classe de types Applicative nous permettait dutiliser des fonctions normales sur des valeurs dans des contextes tout en prservant le contexte de faon trs propre. Observez : ghci> (*) <$> Just 2 <*> Just 8 Just 16 ghci> (++) <$> Just "klingon" <*> Nothing Nothing ghci> (-) <$> [3,4] <*> [1,2,3] [2,1,0,3,2,1]

Ah, cool, prsent quon les traite comme des valeurs applicatives, les valeurs Maybe a reprsentent des calculs qui peuvent avoir chou, les valeurs [a] reprsentent des calculs qui ont plusieurs rsultats (calculs non dterministes), les IO a reprsentent des valeurs qui ont des effets de bord, etc. Les monades sont une extension naturelle des foncteurs applicatifs, et avec elles on cherche rsoudre ceci : si vous avez une valeur dans un contexte, m a , comment appliquer dessus une fonction qui prend une valeur normale a et retourne une valeur dans un contexte ? Cest--dire, comment appliquer une fonction de type a -> m b une valeur de type m a ? En gros, on veut cette fonction : (>>=) :: (Monad m) => m a -> (a -> m b) -> m b

Si lon a une valeur spciale, et une fonction qui prend une valeur normale mais retourne une valeur spciale, comment donner la valeur spciale cette fonction ? Cest la question laquelle on sattaquera en tudiant les monades. On crit m a plutt que f a parce que le m signifie Monad , mais les monades sont seulement des foncteurs applicatifs supportant (>>=) . La fonction (>>=) est prononce bind (NDT : en franais, cela signifie lier). Quand on a une valeur normale a et une fonction normale a -> b , il est trs facile dalimenter la fonction avec la valeur - on applique simplement la fonction avec la valeur et voil. Mais quand on a affaire des valeurs qui viennent de certains contextes, il faut un peu plus y rflchir pour voir comment ces valeurs spciales peuvent tre donnes aux fonctions, et comment prendre en compte leur comportement, mais vous verrez que cest simple comme bonjour.

Trempons-nous les pieds avec Maybe


Maintenant quon a une vague ide de ce que sont les monades, voyons si lon peut claircir cette notion un tant soit peu. Sans surprise, Maybe est une monade, alors explorons cela encore un peu et voyons si lon peut combiner cela avec ce quon sait des monades.

Soyez certain de bien comprendre les foncteurs applicatifs prsent. Cest bien si vous ressentez comment les instances d Applicative fonctionnent et le genre de calculs quelles reprsentent, parce que les monades ne font que prendre nos connaissances des foncteurs applicatifs, et les amliorer.

Une valeur ayant pour type Maybe a reprsente une valeur de type a avec le contexte de lchec potentiel attach. Une valeur Just "dharma" signifie que la chane de caractres "dharma" est prsente, alors quune valeur Nothing reprsente son absence, ou si vous imaginez la chane de caractres comme le rsultat dun calcul, cela signifie que le calcul a chou. Quand on a regard Maybe comme un foncteur, on a vu que si lon veut fmap une fonction sur celui-ci, elle est mappe sur ce qui est lintrieur des valeurs Just , les Nothing tant conservs tels quels parce quil ny a pas de valeur sur laquelle mapper ! Ainsi : ghci> fmap (++"!") (Just "wisdom") Just "wisdom!" ghci> fmap (++"!") Nothing Nothing

En tant que foncteur applicatif, cela fonctionne similairement. Cependant, les foncteurs applicatifs prennent une fonction enveloppe. Maybe est un foncteur applicatif de manire ce que lorsquon utilise <*> pour appliquer une fonction dans un Maybe une valeur dans un Maybe , elles doivent toutes deux tre des valeurs Just pour que le rsultat soit une valeur Just , autrement le rsultat est Nothing . Cest logique, puisque sil vous manque soit la fonction, soit son paramtre, vous ne pouvez pas linventer, donc vous devez propager lchec : ghci> Just (+3) <*> Just 3 Just 6 ghci> Nothing <*> Just "greed" Nothing ghci> Just ord <*> Nothing Nothing

Quand on utilise le style applicatif pour appliquer des fonctions normales sur des valeurs Maybe , cest similaire. Toutes les valeurs doivent tre des Just , sinon le tout est Nothing ! ghci> max <$> Just 3 <*> Just 6 Just 6 ghci> max <$> Just 3 <*> Nothing Nothing

prsent, pensons ce que lon ferait pour faire >>= sur Maybe . Comme on la dit, >>= prend une valeur monadique, et une fonction qui prend une valeur normale et retourne une valeur monadique, et parvient appliquer cette fonction la valeur monadique. Comment fait-elle cela, si la fonction naccepte quune valeur normale ? Eh bien, pour ce faire, elle doit prendre en compte le contexte de la valeur monadique. Dans ce cas, >>= prendrait un Maybe a et une fonction de type a -> Maybe b et appliquerait la fonction au Maybe a . Pour comprendre comment elle fait cela, on peut utiliser notre intuition venant du fait que Maybe est un foncteur applicatif. Mettons quon ait une fonction \x -> Just (x + 1) . Elle prend un nombre, lui ajoute 1 et lenveloppe dans un Just :

ghci> (\x -> Just (x+1)) 1 Just 2 ghci> (\x -> Just (x+1)) 100 Just 101

Si on lui donne 1 , elle svalue en Just 2 . Si on lui donne le nombre 100 , le rsultat est Just 101 . Trs simple. Voil lobstacle : comment donner une valeur Maybe a cette fonction ? Si on pense ce que fait Maybe en tant que foncteur applicatif, rpondre est plutt simple. Si on lui donne une valeur Just , elle prend ce qui est dans le Just et applique la fonction dessus. Si on lui donne un Nothing , hmm, eh bien, on a une fonction, mais rien pour lappliquer. Dans ce cas, faisons juste comme avant et retournons Nothing . Plutt que de lappeler >>= , appelons-la applyMaybe pour linstant. Elle prend un Maybe a et une fonction et retourne un Maybe b , parvenant appliquer la fonction sur le Maybe a . Voici le code : applyMaybe :: Maybe a -> (a -> Maybe b) -> Maybe b applyMaybe Nothing f = Nothing applyMaybe (Just x) f = f x

Ok, prsent jouons un peu. On va lutiliser en fonction infixe pour que la valeur Maybe soit sur le ct gauche et la fonction sur la droite : ghci> Just 3 `applyMaybe` \x -> Just (x+1) Just 4 ghci> Just "smile" `applyMaybe` \x -> Just (x ++ " :)") Just "smile :)" ghci> Nothing `applyMaybe` \x -> Just (x+1) Nothing ghci> Nothing `applyMaybe` \x -> Just (x ++ " :)") Nothing

Dans lexemple ci-dessus, on voit que quand on utilisait applyMaybe avec une valeur Just et une fonction, la fonction tait simplement applique dans le Just . Quand on essayait de lutiliser sur un Nothing , le rsultat tait Nothing . Et si la fonction retourne Nothing ? Voyons : ghci> Just 3 `applyMaybe` \x -> if x > 2 then Just x else Nothing Just 3 ghci> Just 1 `applyMaybe` \x -> if x > 2 then Just x else Nothing Nothing

Comme prvu. Si la valeur monadique gauche est Nothing , le tout est Nothing . Et si la fonction de droite retourne Nothing , le rsultat est galement Nothing . Cest trs similaire au cas o on utilisait Maybe comme foncteur applicatif et on obtenait un Nothing en rsultat lorsquil y avait un Nothing quelque part. Il semblerait que, pour Maybe , on ait trouv un moyen de prendre une valeur spciale et de la donner une fonction qui prend une valeur normale et en retourne une spciale. On la fait en gardant lesprit quune valeur Maybe reprsentait un calcul pouvant chouer. Vous vous demandez peut-tre quoi bon faire cela ? On pourrait croire que les foncteurs applicatifs sont plus puissants que les monades, puisquils nous permettent de prendre une fonction normale et de la faire oprer sur des valeurs dans des contextes. On verra que les monades peuvent galement faire a, car elles sont des amliorations des foncteurs applicatifs, et quelles peuvent galement faire des choses cool dont les foncteurs applicatifs sont incapables. On va revenir Maybe dans une minute, mais dabord, dcouvrons la classe des types monadiques.

La classe de types Monad


Tout comme les foncteurs ont la classe de types Functor et les foncteurs applicatifs ont la classe de types Applicative , les monades viennent avec leur propre classe de types : Monad ! Wow, qui leut cru ? Voici quoi ressemble la classe de types : class Monad m where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b x >> y = x >>= \_ -> y fail :: String -> m a fail msg = error msg

Commenons par la premire ligne. Elle dit class Monad m where . Mais, attendez, navons-nous pas dit que les monades taient des foncteurs applicatifs amliors ? Ne devrait-il pas y avoir une

contrainte de classe comme class (Applicative m) => Monad m where afin quun type doive tre un foncteur applicatif pour tre une monade ? Eh bien, a devrait tre le cas, mais quand Haskell a t cr, il nest pas apparu ses crateurs que les foncteurs applicatifs taient intressants pour Haskell, et donc ils ntaient pas prsents. Mais soyez rassur, toute monade est un foncteur applicatif, mme si la dclaration de classe Monad ne lindique pas. La premire fonction de la classe de types Monad est return . Cest la mme chose que pure , mais avec un autre nom. Son type est (Monad m) => a -> m a . Elle prend une valeur et la place dans un contexte minimal contenant cette valeur. En dautres termes, elle prend une valeur et lenveloppe dans une monade. Elle est toujours dfinie comme pure de la classe de types Applicative , ce qui signifie quon la connat dj. On a dj utilis return quand on faisait des entres-sorties. On sen servait pour prendre une valeur et crer une I/O factice, qui ne faisait rien de plus que renvoyer la valeur. Pour Maybe elle prend une valeur et lenveloppe dans Just .

Un petit rappel : return nest en rien comme le return qui existe dans la plupart des langages. Elle ne termine pas lexcution de la fonction ou quoi que ce soit, elle prend simplement une valeur normale et la place dans un contexte.

La prochaine fonction est >>= , ou bind. Cest comme une application de fonction, mais au lieu de prendre une valeur normale pour la donner une fonction normale, elle prend une valeur monadique (dans un contexte) et alimente une fonction qui prend une valeur normale et retourne une valeur monadique. Ensuite, il y a >> . On ny prtera pas trop attention pour linstant parce quelle vient avec une implmentation par dfaut, et quon ne limplmente presque jamais quand on cre des instances de Monad . La dernire fonction de la classe de types Monad est fail . On ne lutilise jamais explicitement dans notre code. En fait, cest Haskell qui lutilise pour permettre les checs dans une construction syntaxique pour les monades que lon dcouvrira plus tard. Pas besoin de se proccuper de fail pour linstant. prsent quon sait quoi la classe de types Monad ressemble, regardons comment Maybe est instanci comme Monad ! instance Monad Maybe where return x = Just x Nothing >>= f = Nothing Just x >>= f = f x fail _ = Nothing

return est identique pure , pas besoin de rflchir. On fait comme dans Applicative et on enveloppe la valeur dans Just . La fonction >>= est identique notre applyMaybe . Quand on lui donne un Maybe a , on se souvient du contexte et on retourne Nothing si la valeur gauche est Nothing , parce quil ny a pas de valeur sur laquelle appliquer la fonction. Si cest un Just , on prend ce qui est lintrieur et on applique f dessus. On peut jouer un peu avec la monade Maybe : ghci> return "WHAT" :: Maybe String Just "WHAT" ghci> Just 9 >>= \x -> return (x*10) Just 90 ghci> Nothing >>= \x -> return (x*10) Nothing

Rien de neuf ou dexcitant la premire ligne puisquon a dj utilis pure avec Maybe et on sait que return est juste pure avec un autre nom. Les deux lignes suivantes prsentent un peu mieux >>= . Remarquez comment, lorsquon donne Just 9 la fonction \x -> return (x*10) , le x a pris la valeur 9 dans la fonction. Il semble quon ait russi extraire la valeur du contexte Maybe sans avoir recours du filtrage par motif. Et on na pas perdu le contexte de notre valeur Maybe , parce que quand elle vaut Nothing , le rsultat de >>= sera galement Nothing .

Le funambule
prsent quon sait comment donner un Maybe a une fonction ayant pour type a -> Maybe b tout en tenant compte du contexte de lchec potentiel, voyons comment on peut utiliser >>= de

manire rpte pour grer le calcul de plusieurs valeurs Maybe a . Pierre a dcid de faire une pause dans son emploi au centre de pisciculture pour sadonner au funanbulisme. Il nest pas trop mauvais, mais il a un problme : des oiseaux narrtent pas datterrir sur sa perche dquilibre ! Ils viennent se reposer un instant, discuter avec leurs amis aviaires, avant de dcoller en qute de miettes de pain. Cela ne le drangerait pas tant si le nombre doiseaux sur le ct gauche de la perche tait gal celui sur le ct droit. Mais parfois, tous les oiseaux dcident quils prfrent le mme ct, et ils dtruisent son quilibre, lentranant dans une chute embarrassante (il a un filet de scurit). Disons quil arrive tenir en quilibre tant que le nombre doiseaux sur les cts gauche et droit ne sloignent pas de plus de trois. Ainsi, sil y a un oiseau droite et quatre oiseaux gauche, tout va bien. Mais si un cinquime oiseau atterrit sur sa gauche, il perd son quilibre et plonge malgr lui. Nous allons simuler des oiseaux atterrissant et dcollant de la perche et voir si Pierre tient toujours aprs un certain nombre darrives et de dparts. Par exemple, on souhaite savoir ce qui arrive Pierre si le premier oiseau arrive sur sa gauche, puis quatre oiseaux dbarquent sur sa droite, et enfin loiseau sur sa gauche dcide de senvoler. On peut reprsenter la perche comme une simple paire dentiers. La premire composante compte les oiseaux sur sa gauche, la seconde ceux sur sa droite : type Birds = Int type Pole = (Birds,Birds)

Tout dabord, on cre un synonyme du type Int , appel Birds , parce quon va utiliser des entiers pour reprsenter un nombre doiseaux. Puis, on cre un synonyme pour (Birds, Birds) quon appelle Pole ( ne pas confondre avec une personne dorigine polonaise). Ensuite, pourquoi ne pas crer une fonction qui prenne un nombre doiseaux et les fait atterrir sur un ct de la perche ? Voici les fonctions : landLeft :: Birds -> Pole -> Pole landLeft n (left,right) = (left + n,right) landRight :: Birds -> Pole -> Pole landRight n (left,right) = (left,right + n)

Simple. Essayons-les : ghci> landLeft 2 (0,0) (2,0) ghci> landRight 1 (1,2) (1,3) ghci> landRight (-1) (1,2) (1,1)

Pour faire dcoller les oiseaux, on a utilis un nombre ngatif. Puisque faire atterrir des oiseaux sur un Pole retourne un Pole , on peut chaner les applications de landLeft et landRight : ghci> landLeft 2 (landRight 1 (landLeft 1 (0,0))) (3,1)

Quand on applique la fonction landLeft 1 sur (0, 0) , on obtient (1, 0) . Puis, on fait atterrir un oiseau sur le ct droit, ce qui donne (1, 1) . Finalement, deux oiseaux atterrissent gauche, donnant (3, 1) . On applique une fonction en crivant dabord la fonction puis le paramtre, mais ici, il serait plus pratique davoir la perche en premire et ensuite la fonction datterrissage. Si lon cre une fonction : x -: f = f x

On peut appliquer les fonctions en crivant dabord le paramtre puis la fonction : ghci> 100 -: (*3) 300 ghci> True -: not False ghci> (0,0) -: landLeft 2 (2,0)

Ainsi, on peut faire atterrir de faon rpte des oiseaux, de manire plus lisible : ghci> (0,0) -: landLeft 1 -: landRight 1 -: landLeft 2 (3,1)

Plutt cool ! Cet exemple est quivalent au prcdent o lon faisait atterrir plusieurs fois des oiseaux sur la perche, mais il a lair plus propre. Ici, il est plus facile de voir quon commence avec (0, 0) et quon fait atterrir un oiseau gauche, puis un droite, puis deux gauche. Jusquici, tout est bien. Mais que se passe-t-il lorsque 10 oiseaux atterrissent du mme ct ? ghci> landLeft 10 (0,3) (10,3)

10 oiseaux gauche et seulement 3 droite ? Avec a, le pauvre Pierre est sr de se casser la figure ! Ici, cest facile de sen rendre compte, mais si lon avait une squence comme : ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2) (0,2)

On pourrait croire que tout va bien, mais si vous suivez attentivement les tapes, vous verrez qu un moment il y a 4 oiseaux sur la droite et aucun gauche ! Pour rgler ce problme, il faut retourner nos fonctions landLeft et landRight . De ce quon voit, on veut que ces fonctions puissent chouer. Cest--dire, on souhaite quelles retournent une nouvelle perche tant que lquilibre est respect, mais quelles chouent si les oiseaux atterrissent en disproportion. Et quel meilleur moyen dajouter un contexte de possible chec que dutiliser une valeur Maybe ? Reprenons ces fonctions : landLeft :: Birds -> Pole -> Maybe Pole landLeft n (left,right) | abs ((left + n) - right) < 4 = Just (left + n, right) | otherwise = Nothing landRight :: Birds -> Pole -> Maybe Pole landRight n (left,right) | abs (left - (right + n)) < 4 = Just (left, right + n) | otherwise = Nothing

Plutt que de retourner un Pole , ces fonctions retournent un Maybe Pole . Elles prennent toujours un nombre doiseaux et une perche existante, mais elles vrifient si latterrisage des oiseaux dsquilibre ou non Pierre. On a utilis des gardes pour vrifier si la diffrence entre les extrmits de la perche est infrieure 4. Si cest le cas, on enveloppe la nouvelle perche dans un Just et on la retourne. Sinon, on retourne Nothing pour indiquer lchec. Mettons ces nouvelles-nes lpreuve : ghci> landLeft 2 (0,0) Just (2,0) ghci> landLeft 10 (0,3) Nothing

Joli ! Quand on fait atterrir des oiseaux sans dtruire lquilibre de Pierre, on obtient une nouvelle perche encapsule dans un Just . Mais quand trop doiseaux arrivent dun ct de la perche, on obtient Nothing . Cest cool, mais on dirait quon a perdu la capacit de chaner des atterissages doiseaux sur la perche. On ne peut plus faire landLeft 1 (landRight 1 (0,0)) parce que lorsquon applique landRight 1 (0, 0) , on nobtient pas un Pole mais un Maybe Pole . landLeft 1 prend un Pole et retourne un Maybe Pole . On a besoin dune manire de prendre un Maybe Pole et de le donner une fonction qui prend un Pole et retourne un Maybe Pole . Heureusement, on a >>= , qui fait exactement a pour Maybe . Essayons : ghci> landRight 1 (0,0) >>= landLeft 2 Just (2,1)

Souvenez-vous, landLeft 2 a pour type Pole -> Maybe Pole . On ne pouvait pas lui donner directement un Maybe Pole rsultant de landRight 1 (0, 0) , alors on utilise >>= pour prendre une valeur dans un contexte et la passer landLeft 2 . >>= nous permet en effet de traiter la valeur Maybe comme une valeur avec un contexte, parce que si lon donne Nothing landLeft 2 , on obtient bien Nothing et lchec est propag : ghci> Nothing >>= landLeft 2 Nothing

Ainsi, on peut prsent chaner des atterrissages pouvant chouer parce que >>= nous permet de donner une valeur monadique une fonction qui attend une

valeur normale. Voici une squence datterrisages : ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2 Just (2,4)

Au dbut, on utilise return pour prendre une perche et lenvelopper dans un Just . On aurait pu appeler directement landRight 2 sur (0, 0) , ctait la mme chose, mais la notation est plus consistente en utilisant >>= pour chaque fonction. Just (0, 0) alimente landRight 2 , rsultant en Just (0, 2) . son tour, alimentant landLeft 2 , rsultant en Just (2, 2) , et ainsi de suite. Souvenez-vous de lexemple prcdent o lon introduisait la notion dchec dans la routine de Pierre : ghci> (0,0) -: landLeft 1 -: landRight 4 -: landLeft (-1) -: landRight (-2) (0,2)

Cela ne simulait pas trs bien son interaction avec les oiseaux, parce quau milieu, il perdait son quilibre, mais le rsultat ne le refltait pas. Redonnons une chance cette fonction en utilisant lapplication monadique ( >>= ) plutt que lapplication normale. ghci> return (0,0) >>= landLeft 1 >>= landRight 4 >>= landLeft (-1) >>= landRight (-2) Nothing

Gnial. Le rsultat final reprsente lchec, qui est ce quon attendait. Regardons comment ce rsultat est obtenu. Dabord, return place (0, 0) dans un contexte par dfaut, en faisant un Just (0, 0) . Puis, Just (0,0) >>= landLeft 1 a lieu. Puisque Just (0, 0) est une valeur Just , landLeft 1 est applique (0, 0) , retournant Just (1, 0) , parce que loiseau seul ne dtruit pas lquilibre. Ensuite, Just (1,0) >>= landRight 4 prend place, et le rsultat est Just (1, 4) car lquilibre reste intact, bien qu sa limite. Just (1, 4) est donn landLeft (-1) . Cela signifie que landLeft (-1) (1, 4) prend place. Conformment au fonctionnement de landLeft , cela retourne Nothing , parce que la perche rsultante est dsquilibre. prsent on a un Nothing qui est donn landRight (-2) , mais parce que cest un Nothing , le rsultat est automatiquement Nothing , puisquon na rien sur quoi appliquer landRight (-2) . On naurait pas pu faire ceci en utilisant Maybe comme un foncteur applicatif. Si vous essayez, vous vous retrouverez bloqu, parce que les foncteurs applicatifs ne permettent pas que les valeurs applicatives interagissent entre elles. Au mieux, elles peuvent tre utilises en paramtre dune fonction en utilisant le style applicatif. Les oprateurs applicatifs iront chercher leurs rsultats, et les passer la fonction de faon approprie en fonction du foncteur applicatif, puis placeront les valeurs applicatives rsultantes ensemble, mais il ny aura que peu dinteraction entre elles. Ici, cependant, chaque tape dpend du rsultat de la prcdente. chaque atterrissage, le rsultat possible de latterrissage prcdent est examin pour voir lquilibre de la perche. Cela dtermine lchec ou non de latterrissage. On peut aussi crer une fonction qui ignore le nombre doiseaux sur la perche et fait tomber Pierre. On peut lappeler banana : banana :: Pole -> Maybe Pole banana _ = Nothing

On peut maintenant la chaner nos atterrissages doiseaux. Elle causera toujours la chute de notre funambule, parce quelle ignore ce quon lui passe et retourne un chec. Regardez : ghci> return (0,0) >>= landLeft 1 >>= banana >>= landRight 1 Nothing

La valeur Just (1, 0) est passe banana , mais elle produit Nothing , ce qui force le tout rsulter en Nothing . Comme cest triste ! Plutt que de crer des fonctions qui ignorent leur entre et retournent une valeur monadique prdtermine, on peut utiliser la fonction >> , dont limplmentation par dfaut est : (>>) :: (Monad m) => m a -> m b -> m b m >> n = m >>= \_ -> n

Normalement, passer une valeur une fonction qui ignore son paramtre et retourne tout le temps une valeur prdtermine rsulterait toujours en cette valeur prdtermine. Avec les monades cependant, leur contexte et signification doit tre galement considr. Voici comment >> agit sur Maybe : ghci> Nothing >> Just 3

Nothing ghci> Just 3 >> Just 4 Just 4 ghci> Just 3 >> Nothing Nothing

Si vous remplacez >> par >>= \_ -> , il est facile de voir pourquoi cela arrive. On peut remplacer notre fonction banana dans la chane par un >> suivi dun Nothing : ghci> return (0,0) >>= landLeft 1 >> Nothing >>= landRight 1 Nothing

Et voil, chec vident garanti ! Il est aussi intressant de regarder ce quon aurait d crire si lon navait pas fait le choix astucieux de traiter les valeurs Maybe comme des valeurs dans un contexte pour les donner nos fonctions. Voici quoi une srie datterrissages aurait ressembl : routine :: Maybe Pole routine = case landLeft 1 (0,0) of Nothing -> Nothing Just pole1 -> case landRight 4 pole1 of Nothing -> Nothing Just pole2 -> case landLeft 2 pole2 of Nothing -> Nothing Just pole3 -> landLeft 1 pole3

On fait atterrir un oiseau gauche, puis on examine les possibilits dchec et de succs. Dans le cas dun chec, on retourne Nothing . Dans le cas dune russite, on fait atterrir des oiseaux droite et on rpte le tout encore et encore. Convertir cette monstruosit en une jolie chane dapplications monadiques laide de >>= est un exemple classique prouvant comment la monade Maybe nous sauve beaucoup de temps lorsquon doit effectuer des calculs successifs qui peuvent chouer. Remarquez comme limplmentation de >>= pour Maybe fait apparatre exactement la logique de vrifier si une valeur est Nothing , et quand cest le cas, de retourner Nothing immdiatement, alors que quand ce nest pas le cas, on continue avec ce qui est dans le Just . Dans cette section, on a pris quelques fonctions quon avait et vu quelles pouvaient fonctionner encore mieux si les valeurs quelles retournaient supportaient la notion dchec. En transformant ces valeurs en valeurs Maybe et en remplaant lapplication normale des fonctions par >>= , on a obtenu un mcanisme de propagation derreur presque sans effort, parce que >>= prserve le contexte des valeurs quelle applique aux fonctions. Dans ce cas, le contexte indiquait que nos valeurs pouvaient avoir chou, et lorsquon appliquait nos fonctions sur ces valeurs, la possibilit dchec tait prise en compte.

Notation do
Les monades en Haskell sont tellement utiles quelles ont leur propre syntaxe spciale appele notation do . On a dj rencontr la notation do quand on faisait des entres-sorties, et on a alors dit quelle servait coller plusieurs actions I/O en une seule. Eh bien, il savre que la notation do nest pas seulement pour IO , mais peut tre utilise pour nimporte quelle monade. Le principe est identique : coller ensemble plusieurs valeurs monadiques en squence. Nous allons regarder comment la notation do fonctionne et en quoi elle est utile. Considrez lexemple familier de lapplication monadique : ghci> Just 3 >>= (\x -> Just (show x ++ "!")) Just "3!"

Dj vu. Donner une valeur monadique une fonction qui en retourne une autre, rien de grandiose. Remarquez comme en faisant ainsi, x devient 3 lintrieur de la lambda. Une fois dans la lambda, cest juste une valeur normale plutt quune valeur monadique. Et si lon avait encore un >>= dans cette fonction ? Regardez : ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y))) Just "3!"

Ah, une utilisation imbrique de >>= ! Dans la lambda la plus lextrieur, on donne Just "!" la lambda \y -> Just (show x ++ y) . Dans cette lambda

l, le y devient "!" , et le x est toujours 3 parce quon lobtient de la lambda la plus englobante. Cela me rappelle un peu lexpression suivante : ghci> let x = 3; y = "!" in show x ++ y "3!"

La diffrence principale entre les deux, cest que les valeurs dans la premire sont monadiques. Ce sont des valeurs dans le contexte de lchec. On peut remplacer nimporte laquelle dentre elle par un chec : ghci> Nothing >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y))) Nothing ghci> Just 3 >>= (\x -> Nothing >>= (\y -> Just (show x ++ y))) Nothing ghci> Just 3 >>= (\x -> Just "!" >>= (\y -> Nothing)) Nothing

la premire ligne, donner Nothing la fonction rsulte naturellement en Nothing . la deuxime ligne, on donne Just 3 une fonction et x devient 3 , mais ensuite on donne Nothing la lambda intrieure, et le rsultat est Nothing , ce qui cause la lamba extrieure produire Nothing son tour. Cest donc un peu comme assigner des valeurs des variables dans des expressions let, seulement les valeurs sont monadiques. Pour mieux illustrer ce point, crivons un script dans lequel chaque Maybe prend une ligne : foo :: Maybe String foo = Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))

Pour nous viter dcrire ces nervantes lambdas successives, Haskell nous offre la notation do . Elle nous permet de rcrire le bout de code prcdent ainsi : foo :: Maybe String foo = do x <- Just 3 y <- Just "!" Just (show x ++ y)

Il semblerait quon ait gagn la capacit dextraire temporairement des valeurs Maybe sans avoir vrifier si elles sont des Just ou des Nothing chaque tape. Cest cool ! Si nimporte laquelle des valeurs quon essaie dextraire est Nothing , lexpression do entire retourne Nothing . On attrape les valeurs (potentiellement existantes) et on laisse >>= soccuper du contexte de ces valeurs. Il est important de se rappeler que les expressions do sont juste une syntaxe diffrente pour chaner les valeurs monadiques. Dans une expression do , chaque ligne est une valeur monadique. Pour inspecter le rsultat, on utilise <- . Si on a un Maybe String et quon le lie une variable par <- , cette variable aura pour type String , tout comme, lorsquon utilisait >>= pour passer une valeur monadique des lambdas. La dernire valeur monadique dune expression do , comme Just (show x ++ y) ici, ne peut pas tre utilise avec <- pour lier son rsultat, parce que cela ne serait pas logique si lon traduisait nouveau le do en chanes dapplications avec >>= . Plutt, le rsultat de cette dernire expression est le rsultat de la valeur monadique entire, prenant en compte lchec possible des prcdentes. Par exemple, examinez la ligne suivante : ghci> Just 9 >>= (\x -> Just (x > 8)) Just True

Puisque le paramtre de gauche de >>= est une valeur Just , la lambda est applique 9 et le rsultat est Just True . Si lon rcrit cela en notation do , on obtient : marySue :: Maybe Bool marySue = do x <- Just 9 Just (x > 8)

Si lon compare les deux, il est facile de comprendre pourquoi le rsultat de la valeur monadique complte est celui de la dernire valeur monadique crite dans lexpression do , aprs avoir chan toutes les prcdentes.

La routine de notre funambule peut tre exprime avec la notation do . landLeft et landRight prennent un nombre doiseaux et une perche et produisent une perche enveloppe dans un Just , moins que le funambule ne glisse, auquel cas Nothing est produit. On a utilis >>= pour chaner les tapes successives parce que chacune delles dpendait de la prcdente, et chacune ajoutait un contexte dchec ventuel. Voici deux oiseaux atterrissant gauche, suivis de deux oiseaux droite et dun autre gauche : routine :: Maybe Pole routine = do start <- return (0,0) first <- landLeft 2 start second <- landRight 2 first landLeft 1 second

Voyons si cela russit : ghci> routine Just (3,2)

Oui ! Bien. Quand nous faisions ces routines explicitement avec >>= , on faisait souvent quelque chose comme return (0, 0) >>= landLeft 2 , parce que landLeft 2 est une fonction qui retourne une valeur Maybe . Avec les expressions do cependant, chaque ligne doit comporter une valeur monadique. Il faut donc passer explicitement le Pole prcdent aux fonctions landLeft et landRight . Si lon examinait les variables auxquelles on a li nos valeurs Maybe , start vaudrait (0, 0) , first vaudrait (2, 0) , et ainsi de suite. Puisque les expressions do sont crites ligne par ligne, elles peuvent avoir lair de code impratif pour certaines personnes. Mais en fait, elles sont seulement squentielles, puisque chaque valeur de chaque ligne dpend du rsultat des prcdentes, ainsi que de leur contexte (dans ce cas, savoir si elles ont russi ou chou). Encore une fois, regardons quoi ressemblerait le code si nous navions pas utilis les aspects monadiques de Maybe : routine :: Maybe Pole routine = case Just (0,0) of Nothing -> Nothing Just start -> case landLeft 2 start of Nothing -> Nothing Just first -> case landRight 2 first of Nothing -> Nothing Just second -> landLeft 1 second

Voyez comme dans le cas dun succs, le tuple lintrieur de Just (0, 0) devient start , le rsultat de landLeft 2 start devient first , etc. Si lon veut lancer Pierre une peau de banane avec la notation do , on peut le faire ainsi : routine :: Maybe Pole routine = do start <- return (0,0) first <- landLeft 2 start Nothing second <- landRight 2 first landLeft 1 second

Quand on crit une ligne en notation do sans lier sa valeur monadique avec <- , cest comme si lon avait mis >> aprs la valeur monadique dont on souhaite ignorer le rsultat. On squence la valeur monadique mais on ignore son rsultat parce quon sen fiche, et parce que cest plus joli que dcrire _ <- Nothing , qui est quivalent. vous de choisir quand utiliser la notation do et quand utiliser explicitement >>= . Je pense que cet exemple se prte plus lutilisation explicite de >>= parce que chaque tape ne ncessite spcifiquement que le rsultat de la prcdente. Avec la notation do , il a fallu crire chaque fois sur quelle perche les oiseaux atterrissaient, mais ctait chaque fois celle qui prcdait immdiatement. Cependant, cela a fond notre intuition de la notation do . Avec la notation do , lorsquon lie des valeurs monadiques des noms, on peut utiliser du filtrage par motif, tout comme dans les expressions let et les paramtres de fonctions. Voici un exemple de filtrage par motif dans une expression do : justH :: Maybe Char justH = do (x:xs) <- Just "hello" return x

On utilise le filtrage par motif pour obtenir le premier caractre de la chane "hello" et on la prsente comme rsultat. Ainsi, justH est valu en Just 'h' .

On utilise le filtrage par motif pour obtenir le premier caractre de la chane "hello" et on la prsente comme rsultat. Ainsi, justH est valu en Just 'h' . Mais si ce filtrage par motif choue ? Quand un filtrage par motif dans une fonction choue, le prochain motif est essay. Si le filtrage parcourt tous les motifs de la fonction donne sans succs, une erreur est leve et notre programme plante. Dun autre ct, un chec de filtrage par motif dans une expression let rsulte en une erreur immdiate, parce quelles nont pas de mcanisme de motif suivant. Quand un filtrage par motif choue dans une expression do , la fonction fail est appele. Elle fait partie de la classe de types Monad et permet un filtrage par motif chou de rsulter en un chec dans le contexte de cette monade, plutt que de planter notre programme. Son implmentation par dfaut est : fail :: (Monad m) => String -> m a fail msg = error msg

Donc, par dfaut, elle plante notre programme, mais les monades qui contiennent un contexte dchec ventuel (comme Maybe ) implmentent gnralement leur propre version. Pour Maybe , elle est implmente ainsi : fail _ = Nothing

Elle ignore le message et cre un Nothing . Ainsi, quand le filtrage par motif choue pour une valeur Maybe dans une notation do , la valeur de la monade entire est Nothing . Cest prfrable un plantage du programme. Voici une expression do avec un motif vou lchec : wopwop :: Maybe Char wopwop = do (x:xs) <- Just "" return x

Le filtrage par motif choue, donc leffet est le mme que si la ligne entire comprenant le motif tait remplace par Nothing . Essayons : ghci> wopwop Nothing

Le motif chou a provoqu lchec dans le contexte de la monade plutt que de provoquer un chec du programme entier, ce qui est plutt sympa.

La monade des listes


Jusquici, on a vu comment les valeurs Maybe pouvaient tre vues comme des valeurs dans un contexte dchec, et comment incorporer la gestion derreur dans notre code en utilisant >>= pour alimenter les fonctions. Dans cette section, on va voir comment utiliser les aspects monadiques des listes pour introduire le non dterminisme dans notre code de manire claire et lisible. On a dj parl du fait que les listes reprsentent des valeurs non dterministes quand elles sont utilises comme foncteurs applicatifs. Une valeur comme 5 est dterministe. Elle na quun rsultat, et on sait exactement lequel cest. Dun autre ct, une valeur comme [3, 8, 9] contient plusieurs rsultats, on peut donc la voir comme une valeur qui est en fait plusieurs valeurs la fois. Utiliser les listes comme des foncteurs applicatifs dmontre ce non dterminisme lgamment : ghci> (*) <$> [1,2,3] <*> [10,100,1000] [10,100,1000,20,200,2000,30,300,3000]

Toutes les combinaisons possibles de multiplication dun lment de la liste de gauche par un lment de la liste de droite sont inclues dans la liste rsultante. Quand on fait du non dterminisme, il y a beaucoup de choix possibles, donc on les essaie tous, et le rsultat est galement une valeur non dterministe, avec encore plus de rsultats. Ce contexte de non dterminisme se transfre trs joliment aux monades. Regardons immdiatement linstance de Monad pour les listes : instance Monad [] where return x = [x] xs >>= f = concat (map f xs) fail _ = []

return fait la mme chose que pure , on est donc dj familiaris avec return pour les listes. Elle prend une valeur et la place dans un contexte par dfaut minimal retournant cette valeur. En dautres termes, elle cre une liste qui ne contient que cette valeur comme rsultat. Cest utile lorsque lon veut envelopper une valeur normale dans une liste afin quelle puisse interagir avec des valeurs non dterministes. Pour comprendre comment >>= fonctionne pour les listes, il vaut mieux la voir en action pour dvelopper notre intuition dabord. >>= consiste prendre une

valeur dans un contexte (une valeur monadique) et la donner une fonction qui prend une valeur normale et en retourne une dans un contexte. Si cette fonction produisait simplement une valeur normale plutt quune dans un contexte, >>= ne nous serait pas trs utile parce quaprs une utilisation, le contexte serait perdu. Essayons donc de donner une valeur non dterministe une fonction. ghci> [3,4,5] >>= \x -> [x,-x] [3,-3,4,-4,5,-5]

Quand on utilisait >>= avec Maybe , la valeur monadique tait passe la fonction en prenant en compte le possible chec. Ici, elle prend en compte le non dterminisme pour nous. [3, 4, 5] est une valeur non dterministe et on la donne une fonction qui retourne galement une valeur non dterministe. Le rsultat est galement non dterministe, et contient tous les rsultats possibles consistant prendre un lment de la liste [3, 4, 5] et de le passer la fonction \x -> [x, -x] . Cette fonction prend un nombre et produit deux rsultats : le nombre inchang et son oppos. Ainsi, quand on utilise >>= pour donner cette liste la fonction, chaque nombre est gard inchang et oppos. Le x de la lambda prend chaque valeur de la liste quon donne la fonction. Pour voir comment cela a lieu, on peut simplement suivre limplmentation. Dabord, on commence avec la liste [3, 4, 5] . Puis, on mappe la lambda sur la liste, et le rsultat est : [[3,-3],[4,-4],[5,-5]]

La lambda est applique chaque lment et on obtient une liste de listes. Finalement, on aplatit la liste, et voil ! Nous avons appliqu une fonction non dterministe une valeur non dterministe ! Le non dterminisme supporte galement lchec. La liste vide [] est presque lquivalent de Nothing , parce quelle signifie labsence de rsultat. Cest pourquoi lchec est dfini comme la liste vide. Le message derreur est jet. Jouons avec des listes qui chouent : ghci> [] >>= \x -> ["bad","mad","rad"] [] ghci> [1,2,3] >>= \x -> [] []

la premire ligne, une liste vide est donne la lambda. Puisque la liste na pas dlments, aucun lment ne peut tre pass la fonction et ainsi le rsultat est une liste vide. Cest similaire donner Nothing une fonction. la seconde ligne, chaque lment est pass la fonction, mais llment est ignor et la fonction retourne une liste vide. Parce que la fonction choue quelle que soit son entre, le rsultat est un chec. Tout comme les valeurs Maybe , on peut chaner plusieurs listes avec >>= , propageant le non dterminisme : ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch) [(1,'a'),(1,'b'),(2,'a'),(2,'b')]

La liste [1, 2] est lie n et ['a', 'b'] est lie ch . Puis, on fait return (n, ch) (ou [(n, ch)] ), ce qui signifie prendre une paire (n, ch) et la placer dans un contexte minimal. Dans ce cas, cest la liste la plus petite contenant la valeur (n, ch) et tant le moins non dterministe possible. Son effet sur le contexte est donc minimal. Ce quon dit ici, cest : pour chaque lment dans [1, 2] , et pour chaque lment de ['a', 'b'] , produis un tuple dun lment de chaque liste. En gnral, parce que return prend une valeur et lenveloppe dans un contexte minimal, elle na pas deffet additionnel (comme lchec pour Maybe , ou ajouter du non dterminisme pour les listes), mais prsente tout de mme quelque chose en rsultat.

Quand vous avez des valeurs non dterministes qui interagissent, vous pouvez voir leurs calculs comme un arbre o chaque rsultat possible dune liste reprsente une branche.

Voici lexemple prcdent rcrit avec la notation do : listOfTuples :: [(Int,Char)] listOfTuples = do n <- [1,2] ch <- ['a','b'] return (n,ch)

Cela rend un peu plus vident le fait que n prenne chaque valeur dans [1, 2] et ch chaque valeur de ['a', 'b'] . Tout comme avec Maybe , on extrait des

lments de valeurs monadiques, et on les traite comme des valeurs normales, et >>= soccupe du contexte pour nous. Le contexte dans ce cas est le non dterminisme. Utiliser les listes avec la notation do me rappelle quelque chose quon a dj vu. Regardez le code suivant : ghci> [ (n,ch) | n <- [1,2], ch <- ['a','b'] ] [(1,'a'),(1,'b'),(2,'a'),(2,'b')]

Oui ! Les listes en comprhension ! Dans notre exemple avec la notation do , n prenait chaque rsultat de [1, 2] , et pour chacun deux, ch recevait un rsultat de ['a', 'b'] , et enfin la dernire ligne plaait (n, ch) dans un contexte par dfaut (une liste singleton) pour le prsenter comme rsultat sans introduire plus de non dterminisme. Dans cette liste en comprhension, la mme chose a lieu, mais on na pas eu crire return la fin pour prsenter (n, ch) comme le rsultat, parce que la fonction de sortie de la comprhension de liste sen occupe pour nous. En fait, les listes en comprhension sont juste un sucre syntaxique pour utiliser les listes comme des monades. Au final, les listes en comprhension, ainsi que les listes en notation do , sont traduites en utilisations de >>= pour faire les calculs non dterministes. Les listes en comprhension nous permettent de filtrer la sortie. Par exemple, on peut filtrer une liste de nombres pour chercher seulement ceux qui contiennent le chiffre 7 : ghci> [ x | x <- [1..50], '7' `elem` show x ] [7,17,27,37,47]

On applique show x pour transformer notre nombre en chane de caractres, et on vrifie si le caractre 7 fait partie de cette chane. Plutt malin. Pour voir comment le filtrage dans les listes en comprhension se traduit dans la monade des listes, il nous faut regarder la fonction guard et la classe de types MonadPlus . La classe de types MonadPlus est pour les monades qui peuvent aussi se comporter en monodes. Voici sa dfinition : class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a

mzero est synonyme de mempty de la classe de types Monoid , et mplus correspond mappend . Parce que les listes sont des monodes ainsi que des monades, elles peuvent tre instance de cette classe de types : instance MonadPlus [] where mzero = [] mplus = (++)

Pour les listes, mzero reprsente un calcul non dterministe qui na pas de rsultats - un calcul chou. mplus joint deux valeurs non dterministes en une. La fonction guard est dfinie ainsi : guard :: (MonadPlus m) => Bool -> m () guard True = return () guard False = mzero

Elle prend une valeur boolenne et si cest True , elle prend un () et le place dans un contexte minimal par dfaut qui russisse toujours. Sinon, elle cre une valeur monadique choue. La voici en action : ghci> guard Just () ghci> guard Nothing ghci> guard [()] ghci> guard [] (5 > 2) :: Maybe () (1 > 2) :: Maybe () (5 > 2) :: [()] (1 > 2) :: [()]

a a lair intressant, mais comment est-ce utile ? Dans la monade des listes, on lutilise pour filtrer des calculs non dterministes. Observez : ghci> [1..50] >>= (\x -> guard ('7' `elem` show x) >> return x) [7,17,27,37,47]

Le rsultat ici est le mme que le rsultat de la liste en comprhension prcdente. Comment guard fait-elle cela ? Regardons dabord comment elle fonctionne en conjonction avec >> :

ghci> guard (5 > 2) >> return "cool" :: [String] ["cool"] ghci> guard (1 > 2) >> return "cool" :: [String] []

Si guard russit, le rsultat quelle contient est un tuple vide. On utilise alors >> pour ignorer ce tuple vide et prsenter quelque chose dautre en retour. Cependant, si guard choue, alors le return chouera aussi, parce que donner une liste vide une fonction via >>= rsulte toujours en une liste vide. guard dit simplement : si ce boolen est False , alors produis un chec immdiatement, sinon cre une valeur russie factice avec () . Tout ce que cela permet est dautoriser le calcul continuer. Voici lexemple prcdent rcrit en notation do : sevensOnly :: [Int] sevensOnly = do x <- [1..50] guard ('7' `elem` show x) return x

Si nous avions oubli de prsenter x en rsultat final avec return , la liste rsultat serait seulement une liste de tuples vides. Voici la mme chose sous forme de liste en comprhension : ghci> [ x | x <- [1..50], '7' `elem` show x ] [7,17,27,37,47]

La qute dun cavalier


Voici un problme qui se prte particulirement bien une rsolution non dterministe. Disons que vous ayez un chiquier, et seulement un cavalier plac dessus. On souhaite savoir si le cavalier peut atteindre une position donne en trois mouvements. On utilisera simplement une paire de nombres pour reprsenter la position du cavalier sur lchiquier. Le premier nombre dtermine la colonne, et le second la ligne.

Crons un synonyme de type pour la position actuelle du cavalier sur lchiquier : type KnightPos = (Int,Int)

Donc, supposons que le cavalier dmarre en (6, 2) . Peut-il aller en (6, 1) en exactement trois mouvements ? Voyons. Si lon commence en (6, 2) , quel est le meilleur mouvement faire ensuite ? Je sais, pourquoi pas tous les mouvements possibles ! On a notre disposition le non dterminisme, donc plutt que davoir choisir un mouvement, choisissons-les tous la fois. Voici une fonction qui prend la position dun cavalier et retourne toutes ses prochaines positions : moveKnight :: KnightPos -> [KnightPos] moveKnight (c,r) = do (c',r') <- [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1) ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2) ] guard (c' `elem` [1..8] && r' `elem` [1..8])

return (c',r')

Le cavalier peut toujours avancer dune case horizontalement et de deux verticalement, ou bien de deux cases horizontalement et dune verticalement. (c', r') prend chaque valeur de la liste des mouvements, puis guard sassure que le dplacement obtenu (c', r') , reste bien dans les limites de lchiquier. Si ce nest pas le cas, elle produit une liste vide qui cause un chec, et return (c', r') nest pas excute pour cette position l. Cette fonction peut tre rcrite sans utiliser les listes comme des monades, mais on la fait quand mme pour lentranement. Voici la mme fonction avec filter : moveKnight :: KnightPos -> [KnightPos] moveKnight (c,r) = filter onBoard [(c+2,r-1),(c+2,r+1),(c-2,r-1),(c-2,r+1) ,(c+1,r-2),(c+1,r+2),(c-1,r-2),(c-1,r+2) ] where onBoard (c,r) = c `elem` [1..8] && r `elem` [1..8]

Les deux font la mme chose, choisissez celle que vous trouvez plus jolie. Essayons : ghci> moveKnight (6,2) [(8,1),(8,3),(4,1),(4,3),(7,4),(5,4)] ghci> moveKnight (8,1) [(6,2),(7,3)]

a fonctionne merveille ! On prend une position et on essaie tous les mouvements possibles la fois, pour ainsi dire. prsent quon a une position non dterministe, on peut utiliser >>= pour la donner moveKnight . Voici une fonction qui prend une position et retourne toutes les positions quon peut atteindre dans trois mouvements : in3 :: KnightPos -> [KnightPos] in3 start = do first <- moveKnight start second <- moveKnight first moveKnight second

Si vous lui passez (6, 2) , la liste rsultante est assez grande, parce que sil y a beaucoup de faons datteindre une position en trois mouvements, alors le rsultat est dans la liste de multiples fois. La mme chose sans notation do : in3 start = return start >>= moveKnight >>= moveKnight >>= moveKnight

Utiliser >>= une fois nous donne tous les mouvements possibles depuis le point de dpart, puis en utilisant >>= une deuxime fois, pour chacun de ces premiers mouvements, les mouvements suivants sont calculs, et de mme pour le dernier mouvement. Placer une valeur dans un contexte par dfaut en faisant return pour ensuite la passer une fonction avec >>= est quivalent appliquer la fonction normalement la valeur, mais on le fait quand mme pour le style. prsent, crivons une fonction qui prend deux positions et nous dit si on peut aller de lune lautre en exactement trois tapes : canReachIn3 :: KnightPos -> KnightPos -> Bool canReachIn3 start end = end `elem` in3 start

On gnre toutes les solutions possibles pour trois tapes, et on regarde si la position quon cherche est parmi celles-ci. Voyons donc si on peut aller de (6, 2) en (6, 1) en trois mouvements : ghci> (6,2) `canReachIn3` (6,1) True

Oui ! Quen est-il de (6, 2) et (7, 3) ? ghci> (6,2) `canReachIn3` (7,3) False

Non ! En tant quexercice, vous pouvez changer cette fonction afin que, lorsque vous pouvez atteindre une position depuis lautre, elle vous dise quels mouvements faire. Plus tard, nous verrons comment modifier cette fonction pour quon puisse aussi lui passer le nombre de mouvements faire plutt que de le coder en dur comme ici.

Les lois des monades


Tout comme les foncteurs applicatifs, et les foncteurs avant eux, les monades viennent avec quelques lois que toutes les instances de Monad doivent respecter. tre simplement une instance de Monad ne fait pas automatiquement une monade, cela signifie simplement quon a cr une instance. Un type est rellement une monade si les lois des monades sont respectes. Ces lois nous permettent davoir des suppositions raisonnables sur le type et son comportement. Haskell permet nimporte quel type dtre une instance de nimporte quelle classe de types tant que les types correspondent. Il ne peut pas vrifier si les lois sont respectes, donc si lon cre une nouvelle instance de la classe de types Monad , on doit tre raisonnablement convaincu que ce type se comporte bien vis--vis des lois des monades. On peut faire confiance aux types de la bibliothque standard pour satisfaire ces lois, mais plus tard, lorsque lon crira nos propres monades, il faudra vrifier manuellement que les lois tiennent. Mais ne vous inquitez pas, elles ne sont pas compliques.

Composition gauche par return


La premire loi dit que si lon prend une valeur, quon la place dans un contexte par dfaut via return et quon la donne une fonction avec >>= , cest la mme chose que de donner directement la valeur la fonction. Plus formellement :

return x >>= f est gal f x


Si vous regardez les valeurs monadiques comme des valeurs avec un contexte, et return comme prenant une valeur et la plaant dans un contexte minimal qui prsente toujours cette valeur, cest logique, parce que si ce contexte est rellement minimal, alors donner la valeur monadique la fonction ne devrait pas tre diffrent de lapplication de la fonction la valeur normale, et cest en effet le cas. Pour la monade Maybe , return est dfinie comme Just . La monade Maybe se proccupe des checs possibles, et si lon a une valeur quon met dans un tel contexte, cest logique de la traiter comme un calcul russi, puisque lon connat sa valeur. Voici return utilis avec Maybe : ghci> return 3 >>= (\x -> Just (x+100000)) Just 100003 ghci> (\x -> Just (x+100000)) 3 Just 100003

Pour la monade des listes, return place une valeur dans une liste singleton. Limplmentation de >>= prend chaque valeur de la liste et applique la fonction dessus, mais puisquil ny a quune valeur dans une liste singleton, cest la mme chose que dappliquer la fonction la valeur : ghci> return "WoM" >>= (\x -> [x,x,x]) ["WoM","WoM","WoM"] ghci> (\x -> [x,x,x]) "WoM" ["WoM","WoM","WoM"]

On a dit que pour IO , return retournait une action I/O qui navait pas deffet de bord mais retournait la valeur en rsultat. Il est logique que cette loi tienne ainsi pour IO galement.

Composition droite par return


La deuxime loi dit que si lon a une valeur monadique et quon utilise >>= pour la donner return , alors le rsultat est la valeur monadique originale. Formellement :

m >>= return est gal m


Celle-ci peut tre un peu moins vidente que la prcdente, mais regardons pourquoi elle doit tre respecte. Lorsquon donne une valeur monadique une fonction avec >>= , cette fonction prend une valeur normale et retourne une valeur monadique. return est une telle fonction, si vous considrez son type. Comme on la dit, return place une valeur dans un contexte minimal qui prsente cette valeur en rsultat. Cela signifie, par exemple, pour Maybe , quelle nintroduit pas dchec, et pour les listes, quelle najoute pas de non dterminisme. Voici un essai sur quelques monades : ghci> Just "move on up" >>= (\x -> return x) Just "move on up" ghci> [1,2,3,4] >>= (\x -> return x) [1,2,3,4] ghci> putStrLn "Wah!" >>= (\x -> return x) Wah!

Si lon regarde de plus prs lexemple des listes, limplmentation de >>= est :

xs >>= f = concat (map f xs)

Donc, quand on donne [1, 2, 3, 4] return , dabord return est mappe sur [1, 2, 3, 4] , rsultant en [[1],[2],[3],[4]] et ensuite ceci est concatn et retourne notre liste originale. Les lois de composition gauche et droite par return dcrivent simplement comment return doit se comporter. Cest une fonction importante pour faire des valeurs monadiques partir de valeurs normales, et ce ne serait pas bon que les valeurs quelle produit fassent des tas dautres choses.

Associativit
La dernire loi des monades dit que quand on a une chane dapplication de fonctions monadiques avec >>= , lordre dimbrication ne doit pas importer. Formellement nonc :

(m >>= f) >>= g est egal m >>= (\x -> f x >>= g)


Hmmm, que se passe-t-il donc l ? On a une valeur monadique m , et deux fonctions monadiques f et g . Quand on fait (m >>= f) >>= g , on donne m f , ce qui rsulte en une valeur monadique. On donne ensuite cette valeur monadique g . Dans lexpression m >>= (\x -> f x >>= g) , on prend une valeur monadique et on la donne une fonction qui donne le rsultat de f x g . Il nest pas facile de voir que les deux sont gales, regardons donc un exemple qui rend cette galit un peu plus claire. Vous souvenez-vous de notre funambule Pierre qui marchait sur une corde tendue pendant que des oiseaux atterrissaient sur sa perche ? Pour simuler les oiseaux atterrissant sur sa perche, on avait chan plusieurs fonctions qui pouvaient mener lchec : ghci> return (0,0) >>= landRight 2 >>= landLeft 2 >>= landRight 2 Just (2,4)

On commenait avec Just (0, 0) et on liait cette valeur la prochaine fonction monadique, landRight 2 . Le rsultat tait une nouvelle valeur monadique qui tait lie dans la prochaine fonction monadique, et ainsi de suite. Si nous parenthsions explicitement ceci, cela serait : ghci> ((return (0,0) >>= landRight 2) >>= landLeft 2) >>= landRight 2 Just (2,4)

Mais on peut aussi crire la routine ainsi : return (0,0) >>= (\x -> landRight 2 x >>= (\y -> landLeft 2 y >>= (\z -> landRight 2 z)))

return (0, 0) est identique Just (0, 0) et quand on le donne la lambda, x devient (0, 0) . landRight prend un nombre doiseaux et une perche (une paire de nombres) et cest ce quelle reoit. Cela rsulte en Just (0, 2) et quand on donne ceci la prochaine lambda, y est gal (0, 2) . Cela continue jusqu ce que le dernier oiseau se pose, produisant Just (2, 4) , qui est effectivement le rsultat de lexpression entire. Ainsi, il nimporte pas de savoir comment vous imbriquez les appels de fonctions monadiques, ce qui importe cest leur sens. Voici une autre faon de regarder cette loi : considrez la composition de deux fonctions, f et g . Composer deux fonctions se fait ainsi : (.) :: (b -> c) -> (a -> b) -> (a -> c) f . g = (\x -> f (g x))

Si le type de g est a -> b et le type de f est b -> c , on peut les arranger en une nouvelle fonction ayant pour type a -> c , de faon ce que le paramtre soit pass entre les deux fonctions. Et si ces deux fonctions taient monadiques, et retournait des valeurs monadiques ? Si lon avait une fonction de type a -> m b , on ne pourrait pas simplement passer son rsultat une fonction de type b -> m c , parce que la fonction attend un b normal, pas un monadique. On pourrait cependant utiliser >>= pour raliser cela. Ainsi, en utilisant >>= , on peut composer deux fonctions monadiques : (<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c) f <=< g = (\x -> g x >>= f)

On peut prsent composer deux fonctions monadiques : ghci> let f x = [x,-x] ghci> let g x = [x*3,x*2]

ghci> let h = f <=< g ghci> h 3 [9,-9,6,-6]

Cool. Quest-ce que cela a voir avec la loi dassociativit ? Eh bien, quand on regarde la loi comme une loi de composition, elle dit que f <=< (g <=< g) doit tre gal (f <=< g) <=< h . Cest juste une autre faon de dire que limbrication des oprations nimporte pas pour les monades. Si lon traduit les deux premires lois avec <=< , la composition de return gauche dit que pour toute fonction monadique f , f <=< return est gal f et celle droite dit que return <=< f est aussi gal f . Dans ce chapitre, on a vu quelques bases des monades et appris comment la monade Maybe et la monade des listes fonctionnent. Dans le prochain chapitre, nous regarderons tout un tas dautre monades cool, et on apprendra crer les ntres.

Foncteurs, foncteurs applicatifs et monodes

Table des matires

Et pour quelques monades de plus

Et pour quelques monades de plus


Pour une poigne de monades Table des matires Zippeurs Nous avons vu comment les monades pouvaient tre utilises pour prendre des valeurs dans un contexte et appliquer des fonctions dessus, et comment utiliser >>= et la notation do nous permettait de nous concentrer sur les valeurs elles-mme pendant que le contexte tait gr pour nous. Nous avons crois la monade Maybe et avons vu comment elle ajoutait un contexte dchec potentiel. Nous avons appris la monade des listes et vu comment elle nous permettait dintroduire aisment du non dterminisme dans nos programmes. Nous avons aussi appris comment travailler dans la monade IO , avant mme de savoir ce qutait une monade ! Dans ce chapitre, on va apprendre beaucoup dautres monades. On verra comment elles peuvent rendre notre programme plus clair en nous laissant traiter toutes sortes de valeurs comme des valeurs monadiques. Explorer dautres monades nous aidera galement solidifier notre intuition de ce quelles sont. Les monades que nous verrons font toutes partie du paquet mtl . Un paquet Haskell est une collection de modules. Le paquet mtl vient avec la plate-forme Haskell, donc vous lavez probablement. Pour vrifier si cest le cas, tapez ghc-pkg list dans linvite de commande. Cela montrera quels paquets vous avez installs, et lun dentre eux devrait tre mtl suivi dun numro de version.

Lui crire ? Je la connais peine !


Nous avons charg notre pistolet avec les monades Maybe , liste et IO . Plaons maintenant la monade Writer dans la chambre, et voyons ce qui se passe quand on tire ! Alors que Maybe est pour les valeurs ayant un contexte additionnel dchec, et que les listes sont pour les valeurs non dterministes, la monade Writer est faite pour les valeurs qui peuvent avoir une autre valeur attache agissant comme une sorte de registre. Writer nous permet deffectuer des calculs tout en tant srs que toutes les valeurs du registre sont bien combines en un registre qui reste attach au rsultat. Par exemple, on peut vouloir munir nos valeurs dune chane de caractres dcrivant ce qui se passe, probablement pour dboguer notre programme. Considrez une fonction qui prend un nombre de bandits dun gang et nous dit si cest un gros gang ou pas. Cest une fonction trs simple : isBigGang :: Int -> Bool isBigGang x = x > 9

Maintenant, et si au lieu de nous rpondre seulement True ou False , on voulait aussi retourner une chane de caractres indiquant ce quon a fait ? Eh bien, il suffit de crer une chane et la retourner avec le Bool : isBigGang :: Int -> (Bool, String) isBigGang x = (x > 9, "Compared gang size to 9.")

prsent, au lieu de retourner juste un Bool , on retourne un tuple dont la premire composante est la vraie valeur de retour, et la seconde est la chane accompagnant la valeur. Il y a un contexte additionnel prsent. Testons : ghci> isBigGang 3 (False,"Compared gang size to 9.") ghci> isBigGang 30 (True,"Compared gang size to 9.")

Jusquici, tout va bien. isBigGang prend une valeur normale et retourne une valeur dans un contexte. Comme on vient de le voir, lui donner une valeur normale nest pas un problme. Et si lon avait dj une valeur avec un registre attach, comme (3, "Smallish gang.") , et quon voulait la donner isBigGang ? Il semblerait quon se retrouve nouveau face la question : si lon a une fonction qui prend une valeur normale et retourne une valeur dans un contexte, comment lui passer une valeur dans un contexte ? Quand on explorait la monade Maybe , on a cr une fonction applyMaybe , qui prenait un Maybe a et une fonction de type a -> Maybe b et on donnait la valeur Maybe a la fonction, bien quelle attende un a normal et pas un Maybe a . Ceci tait

fait en prenant en compte le contexte qui venait avec la valeur Maybe a , qui tait celui de lchec potentiel. Mais une fois dans la fonction a -> Maybe b , on pouvait traiter la valeur comme une valeur normale, parce que applyMaybe (qui devint ensuite >>= ) soccupait de vrifier si ctait un Nothing ou un Just . Dans la mme veine, crons une fonction qui prend une valeur avec un registre attach, cest--dire, de type (a, String) , et une fonction a -> (b, String) , et qui donne cette valeur cette fonction. On va lappeler applyLog . Mais puisquune valeur (a, String) ne contient pas le contexte dchec potentiel, mais plutt un contexte de valeur additionnelle, applyLog va sassurer que le registre de la valeur originale nest pas perdu, mais est accol au registre de la valeur rsultant de la fonction. Voici limplmentation d applyLog : applyLog :: (a,String) -> (a -> (b,String)) -> (b,String) applyLog (x,log) f = let (y,newLog) = f x in (y,log ++ newLog)

Quand on a une valeur dans un contexte et quon souhaite la donner une fonction, on essaie gnralement de sparer la vraie valeur du contexte, puis on applique la fonction cette valeur, et on soccupe enfin de la gestion du contexte. Dans la monade Maybe , on vrifiait si la valeur tait un Just x et si ctait le cas, on prenait ce x et on appliquait la fonction. Dans ce cas, il est encore plus simple de trouver la vraie valeur, parce quon a une paire contenant la valeur et un registre. On prend simplement la premire composante, qui est x et on applique f avec. On obtient une paire (y, newLog) , o y est le nouveau rsultat, et newLog le nouveau registre. Mais si lon retournait cela en rsultat, on aurait oubli lancien registre, ainsi on retourne une paire (y, log ++ newLog) . On utilise ++ pour juxtaposer le nouveau registre et lancien. Voici applyLog en action : ghci> (3, "Smallish gang.") `applyLog` isBigGang (False,"Smallish gang.Compared gang size to 9") ghci> (30, "A freaking platoon.") `applyLog` isBigGang (True,"A freaking platoon.Compared gang size to 9")

Les rsultats sont similaires aux prcdents, seulement le nombre de personne dans le gang avait un registre laccompagnant, et ce registre a t inclus dans le registre rsultant. Voici dautres exemples dutilisation d applyLog : ghci> ("Tobin","Got outlaw name.") `applyLog` (\x -> (length x, "Applied length.")) (5,"Got outlaw name.Applied length.") ghci> ("Bathcat","Got outlaw name.") `applyLog` (\x -> (length x, "Applied length")) (7,"Got outlaw name.Applied length")

Voyez comme, dans la lambda, x est simplement une chane de caractres normale et non pas un tuple, et comment applyLog soccupe de la juxtaposition des registres.

Monodes la rescousse

Soyez certain de savoir ce que sont les monodes avant de continuer ! Cordialement.

Pour linstant, applyLog prend des valeurs de type (a, String) , mais y a-t-il une raison ce que le registre soit une String ? On utilise ++ pour juxtaposer les registres, ne devrait-ce donc pas marcher pour nimporte quel type de liste, pas seulement des listes de caractres ? Bien sr que oui. On peut commencer par changer son type en : applyLog :: (a,[c]) -> (a -> (b,[c])) -> (b,[c])

prsent, le registre est une liste. Le type des valeurs dans la liste doit tre le mme dans la valeur originale que dans la valeur retourne par la fonction, autrement on ne saurait utiliser ++ pour les juxtaposer. Est-ce que cela marcherait pour des chanes doctets ? Il ny a pas de raison que a ne marche pas. Cependant, le type quon a l ne marche que pour les listes. On dirait quil nous faut une autre fonction applyLog pour les chanes doctets. Mais attendez ! Les listes et les chanes doctets sont des monodes. En tant que tels, elles sont toutes deux des instances de la classe de types Monoid , ce qui signifie quelles implmentent la fonction mappend . Et pour les listes autant que les chanes doctets, mappend sert concatner. Regardez : ghci> [1,2,3] `mappend` [4,5,6] [1,2,3,4,5,6] ghci> B.pack [99,104,105] `mappend` B.pack [104,117,97,104,117,97] Chunk "chi" (Chunk "huahua" Empty)

Cool ! Maintenant, applyLog peut fonctionner sur nimporte quel monode. On doit changer son type pour reflter cela, ainsi que son implmentation pour

remplacer ++ par mappend : applyLog :: (Monoid m) => (a,m) -> (a -> (b,m)) -> (b,m) applyLog (x,log) f = let (y,newLog) = f x in (y,log `mappend` newLog)

Puisque la valeur accompagnante peut tre nimporte quelle valeur monodale, plus besoin de penser un tuple valeur et registre, on peut dsormais penser un tuple valeur et valeur monodale. Par exemple, on peut avoir un tuple contenant un nom dobjet et un prix en tant que valeur monodale. On utilise le newtype Sum pour sassurer que les prix sont bien additionns lorsquon opre sur les objets. Voici une fonction qui ajoute des boissons de la nourriture de cow-boy : import Data.Monoid type Food = String type Price = Sum Int addDrink addDrink addDrink addDrink :: Food -> (Food,Price) "beans" = ("milk", Sum 25) "jerky" = ("whiskey", Sum 99) _ = ("beer", Sum 30)

On utilise des chanes de caractres pour reprsenter la nourriture, et un Int dans un newtype Sum pour tracer le nombre de centimes que quelque chose cote. Juste un rappel, faire mappend sur des Sum rsulte en la somme des valeurs enveloppes : ghci> Sum 3 `mappend` Sum 9 Sum {getSum = 12}

La fonction addDrink est plutt simple. Si lon mange des haricots, elle retourne "milk" ainsi que Sum 25 , donc 25 centimes encapsuls dans un Sum . Si lon mange du buf sch, on boit du whisky, et si lon mange quoi que ce soit dautre, on boit une bire. Appliquer normalement une fonction de la nourriture ne serait pas trs intressant ici, mais utiliser applyLog pour donner une nourriture qui a un prix cette fonction est intressant : ghci> ("beans", Sum 10) `applyLog` addDrink ("milk",Sum {getSum = 35}) ghci> ("jerky", Sum 25) `applyLog` addDrink ("whiskey",Sum {getSum = 124}) ghci> ("dogmeat", Sum 5) `applyLog` addDrink ("beer",Sum {getSum = 35})

Du lait cote 25 centimes, mais si on le prend avec des haricots cotant 10 centimes, on paie au final 35 centimes. Il est prsent clair que la valeur attache na pas besoin dtre un registre, elle peut tre nimporte quel valeur monodale, et la faon dont deux de ces valeurs sont combines dpend du monode. Quand nous faisions des registres, elles taient juxtaposes, mais prsent, les nombres sont somms. Puisque la valeur qu addDrink retourne est un tuple (Food, Price) , on peut donner ce rsultat addDrink nouveau, pour quelle nous dise ce quon devrait boire avec notre boisson et combien le tout nous coterait. Essayons : ghci> ("dogmeat", Sum 5) `applyLog` addDrink `applyLog` addDrink ("beer",Sum {getSum = 65})

Ajouter une boisson de la nourriture pour chien retourne une bire et un prix additionnel de 30 centimes, donc ("beer", Sum 35) . Et si lon utilise applyLog pour donner cela addDrink , on obtient une autre bire et le rsultat est ("beer", Sum 65) .

Le type Writer
Maintenant quon a vu quune valeur couple un monode agissait comme une valeur monadique, examinons linstance de Monad pour de tels types. Le module Control.Monad.Writer exporte le type Writer w a ainsi que son instance de Monad et quelques fonctions utiles pour manipuler des valeurs de ce type. Dabord, examinons le type lui-mme. Pour attacher un monode une valeur, on doit simplement les placer ensemble dans un tuple. Le type Writer w a est juste un enrobage newtype de cela. Sa dfinition est trs simple : newtype Writer w a = Writer { runWriter :: (a, w) }

Cest envelopp dans un newtype afin dtre fait instance de Monad et de sparer ce type des tuples ordinaires. Le paramtre de type a reprsente le type de la valeur, alors que le paramtre de type w est la valeur monodale attache. Son instance de Monad est dfinie de la sorte :

instance (Monoid w) => Monad (Writer w) where return x = Writer (x, mempty) (Writer (x,v)) >>= f = let (Writer (y, v')) = f x in Writer (y, v `mappend` v')

Tout dabord, examinons >>= . Son implmentation est essentiellement identique applyLog , seulement prsent que notre tuple est envelopp dans un newtype Writer , on doit len sortir en filtrant par motif. On prend la valeur x et applique la fonction f . Cela nous rend une valeur Writer w a et on utilise un filtrage par motif via une expression let dessus. On prsente le y comme nouveau rsultat et on utilise mappend pour combiner lancienne valeur monodale avec la nouvelle. On replace ceci et le rsultat dans un constructeur Writer afin que notre rsultat soit bien une valeur Writer et pas simplement un tuple non encapsul. Quen est-il de return ? Elle doit prendre une valeur et la placer dans un contexte minimal qui retourne ce rsultat. Quel serait un tel contexte pour une valeur Writer ? Si lon souhaite que notre valeur monodale affecte aussi faiblement que possible les autres valeurs monodales, il est logique dutiliser mempty . mempty est llment neutre des valeurs monodales, comme "" ou Sum 0 ou une chane doctets vide. Quand on utilise mappend avec mempty et une autre valeur monodale, le rsultat est gal cette autre valeur. Ainsi, si lon utilise return pour crer une valeur Writer et quon utilise >>= pour donner cette valeur une fonction, la valeur monodale rsultante sera uniquement ce que la fonction retourne. Utilisons return sur le nombre 3 quelques fois, en lui attachant un monode diffrent chaque fois : ghci> runWriter (return 3 :: Writer String Int) (3,"") ghci> runWriter (return 3 :: Writer (Sum Int) Int) (3,Sum {getSum = 0}) ghci> runWriter (return 3 :: Writer (Product Int) Int) (3,Product {getProduct = 1})

Puisque Writer na pas dinstance de Show , on a d utiliser runWriter pour convertir nos valeurs Writer en tuples normaux quon peut alors afficher. Pour les String , la valeur monodale est la chane vide. Avec Sum , cest 0 , parce que si lon ajoute 0 quelque chose, cette chose est inchange. Pour Product , le neutre est 1 . Linstance Writer na pas dimplmentation de fail , donc si un filtrage par motif choue dans une notation do , error est appele.

Utiliser la notation do avec Writer


prsent quon a une instance de Monad , on est libre dutiliser la notation do pour les valeurs Writer . Cest pratique lorsquon a plusieurs valeurs Writer et quon veut faire quelque chose avec. Comme les autres monades, on peut les traiter comme des valeurs normales et les contextes sont pris en compte pour nous. Dans ce cas, les valeurs monodales sont attaches et mappend les unes aux autres et ceci se reflte dans le rsultat final. Voici un exemple simple de lutilisation de la notation do avec Writer pour multiplier des nombres. import Control.Monad.Writer logNumber :: Int -> Writer [String] Int logNumber x = Writer (x, ["Got number: " ++ show x]) multWithLog :: Writer [String] Int multWithLog = do a <- logNumber 3 b <- logNumber 5 return (a*b)

logNumber prend un nombre et cre une valeur Writer . Pour le monode, on utilise une liste de chanes de caractres et on donne au nombre une liste singleton qui dit simplement quon a ce nombre. multWithLog est une valeur Writer qui multiplie 3 et 5 et sassure que leurs registres attachs sont inclus dans le registre final. On utilise return pour prsenter a*b comme rsultat. Puisque return prend simplement quelque chose et le place dans un contexte minimal, on peut tre sr de ne rien avoir ajout au registre. Voici ce quon voit en valuant ceci : ghci> runWriter multWithLog (15,["Got number: 3","Got number: 5"])

Parfois, on veut seulement inclure une valeur monodale partir dun endroit donn. Pour cela, la fonction tell est utile. Elle fait partie de la classe de types MonadWriter et dans le cas de Writer , elle prend une valeur monodale, comme ["This is going on"] et cre une valeur Writer qui prsente la valeur factice () comme son rsultat, mais avec notre valeur monodale attache. Quand on a une valeur monodale qui a un () en rsultat, on ne le lie pas une variable. Voici multWithLog avec un message supplmentaire rapport dans le registre :

multWithLog :: Writer [String] Int multWithLog = do a <- logNumber 3 b <- logNumber 5 tell ["Gonna multiply these two"] return (a*b)

Il est important que return (a*b) soit la dernire ligne, parce que le rsultat de la dernire ligne dune expression do est le rsultat de lexpression entire. Si lon avait plac tell la dernire ligne, () serait le rsultat de lexpression do . On aurait perdu le rsultat de la multiplication. Cependant, le registre serait le mme. Place laction : ghci> runWriter multWithLog (15,["Got number: 3","Got number: 5","Gonna multiply these two"])

Ajouter de la tenue de registre nos programmes


Lalgorithme dEuclide est un algorithme qui prend deux nombres et calcule leur plus grand commun diviseur. Cest--dire, le plus grand nombre qui divise la fois ces deux nombres. Haskell contient dj la fonction gcd , qui calcule exactement ceci, mais implmentons la ntre avec des capacits de registre. Voici lalgorithme normal : gcd' :: Int -> Int -> Int gcd' a b | b == 0 = a | otherwise = gcd' b (a `mod` b)

Lalgorithme est trs simple. Dabord, il vrifie si le second nombre est 0. Si cest le cas, alors le premier est le rsultat. Sinon, le rsultat est le plus grand commun diviseur du second nombre et du reste de la division du premier par le second. Par exemple, si lon veut connatre le plus grand commun diviseur de 8 et 3, on suit simplement cet algorithme. Parce que 3 est diffrent de 0, on doit trouver le plus grand commun diviseur de 3 et 2 (car si lon divise 8 par 3, il reste 2). Ensuite, on cherche le plus grand commun diviseur de 3 et 2. 2 est diffrent de 0, on obtient donc 2 et 1. Le second nombre nest toujours pas 0, alors on lance lalgorithme nouveau sur 1 et 0, puisque diviser 2 par 1 nous donne un reste de 0. Finalement, puisque le second nombre est 0, alors le premier est le rsultat final, cest-dire 1. Voyons si le code est daccord : ghci> gcd' 8 3 1

Cest le cas. Trs bien ! Maintenant, on veut munir notre rsultat dun contexte, et ce contexte sera une valeur monodale agissant comme un registre. Comme auparavant, on utilisera une liste de chanes de caractres pour notre monode. Le type de notre nouvelle fonction gcd' devrait donc tre : gcd' :: Int -> Int -> Writer [String] Int

Il ne reste plus qu munir notre fonction de valeurs avec registres. Voici le code : import Control.Monad.Writer gcd' :: Int -> Int -> Writer [String] Int gcd' a b | b == 0 = do tell ["Finished with " ++ show a] return a | otherwise = do tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)] gcd' b (a `mod` b)

La fonction prend deux valeurs Int normales et retourne un Writer [String] Int , cest--dire, un Int avec un registre en contexte. Dans le cas o b vaut 0 , plutt que de donner simplement le a en rsultat, on utilise une expression do pour le placer dans une valeur Writer en rsultat. On utilise dabord tell pour rapporter quon a termin, puis return pour prsenter a comme rsultat de lexpression do . Au lieu de cette expression do , on aurait aussi pu crire : Writer (a, ["Finished with " ++ show a])

Cependant, je pense que lexpression do est plus simple lire. Ensuite, on a le cas o b est diffrent de 0 . Dans ce cas, on crit quon utilise mod pour trouver le reste de la division de a par b . Puis, la deuxime ligne de lexpression do on appelle rcursivement gcd' . Souvenez-vous, gcd' retourne finalement une valeur Writer , il est donc parfaitement valide dutiliser gcd b (a `mod` b) dans une ligne dune expression do . Bien quil puisse tre assez utile de tracer lexcution de notre nouvelle gcd' la main pour voir comment les registres sont juxtaposs, je pense quil sera plus

enrichissant de prendre de la perspective et voir ceux-ci comme des valeurs avec un contexte, et ainsi imaginer ce que notre rsultat devrait tre. Essayons notre nouvelle fonction gcd' . Son rsultat est une valeur Writer [String] Int et si on lextrait de son newtype , on obtient un tuple. La premire composante de cette paire est le rsultat. Voyons si cest le cas : ghci> fst $ runWriter (gcd' 8 3) 1

Bien ! Quen est-il du registre ? Puisquil nest quune liste de chanes de caractres, utilisons mapM_ putStrLn pour afficher ces chanes lcran : ghci> mapM_ putStrLn $ snd $ runWriter (gcd' 8 3) 8 mod 3 = 2 3 mod 2 = 1 2 mod 1 = 0 Finished with 1

Je trouve assez gnial quon ait pu changer notre algorithme ordinaire en un algorithme reportant ce quil fait la vole en changeant simplement les valeurs normales par des valeurs monadiques et en laissant limplmentation de >>= pour Writer soccuper des registres pour nous. On peut ajouter un mcanisme de tenue de registre nimporte quelle fonction. On remplace simplement les valeurs normales par des valeurs Writer , et on remplace lapplication de fonction usuelle par >>= (ou par des expressions do si cela augmente la lisibilit).

Construction inefficace de listes


Quand vous utilisez la monade Writer , il faut tre extrmement prudent avec le choix du monode utiliser, parce quutiliser des listes peut savrer trs lent. Cest parce que les listes utilisent ++ pour mappend , et utiliser ++ pour ajouter quelque chose la fin dune liste peut savrer trs lent si la liste est trs longue. Dans notre fonction gcd' , la construction du registre est rapide parce que la concatnation se droule ainsi : a ++ (b ++ (c ++ (d ++ (e ++ f))))

Les listes sont des structures de donnes construites de la gauche vers la droite, et ceci est efficace parce que lon construit dabord entirement la partie de gauche dune liste, et ensuite on ajoute une liste plus longue droite. Mais si lon ne fait pas attention, utiliser la monade Writer peut produire une concatnation comme celle-ci : ((((a ++ b) ++ c) ++ d) ++ e) ++ f

Celle-ci est associe gauche plutt qu droite. Cest inefficace parce que chaque fois que lon veut ajouter une partie droite une partie gauche, elle doit construire la partie gauche en entier du dbut ! La fonction suivante fonctionne comme gcd' , mais enregistre les choses dans le sens inverse. Elle produit dabord le registre du reste de la procdure, puis ajoute ltape courante la fin du registre. import Control.Monad.Writer gcdReverse :: Int -> Int -> Writer [String] Int gcdReverse a b | b == 0 = do tell ["Finished with " ++ show a] return a | otherwise = do result <- gcdReverse b (a `mod` b) tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)] return result

Elle effectue lappel rcursif dabord, et lie le rsultat au nom result . Puis, elle ajoute ltape courante au registre, mais celle-ci vient donc sajouter la fin du registre produit par lappel rcursif. Finalement, elle prsente le rsultat de lappel rcursif comme rsultat final. La voici en action : ghci> mapM_ putStrLn $ snd $ runWriter (gcdReverse 8 3) Finished with 1 2 mod 1 = 0 3 mod 2 = 1 8 mod 3 = 2

Cest inefficace parce quelle finit par associer les utilisations de ++ gauche plutt qu droite.

Listes diffrentielles

Puisque les listes peuvent parfois tre inefficaces quand on concatne de faon rpte, il vaut mieux utiliser une structure de donnes qui supporte une concatnation toujours efficace. Une liste diffrentielle est une telle structure de donnes. Une liste diffrentielle est similaire une liste, mais au lieu dtre une liste normale, cest une fonction qui prend une liste et lui prpose une autre liste. La liste diffrentielle quivalente la liste [1, 2, 3] serait la fonction \xs -> [1, 2, 3] ++ xs . Une liste vide normale est [] , alors quune liste diffrentielle vide est la fonction \xs -> [] ++ xs . Le truc cool avec les listes diffrentielles, cest quelles supportent une concatnation efficace. Quand on concatne deux listes normales avec ++ , elle doit traverser la liste de la gauche jusqu sa fin pour coller la liste de droite cet endroit. Mais quen est-il avec lapproche des listes diffrentielles ? Eh bien, concatner deux listes diffrentielles peut tre fait ainsi : f `append` g = \xs -> f (g xs)

Souvenez-vous, f et g sont des fonctions qui prennent une liste, et leur prpose quelque chose. Par exemple, si f est la fonction ("dog"++) (qui est juste une autre faon dcrire \xs -> "dog" ++ xs ) et g est la fonction ("meat"++) , alors f `append` g cre une nouvelle fonction quivalente : \xs -> "dog" ++ ("meat" ++ xs)

Nous avons concatn deux listes diffrentielles simplement en en crant une nouvelle fonction qui applique dabord une liste diffrentielle sur une liste, puis applique lautre liste diffrentielle au rsultat. Crons un emballage newtype pour nos listes diffrentielles de faon pouvoir les doter dune instance de monode : newtype DiffList a = DiffList { getDiffList :: [a] -> [a] }

Le type que lon enveloppe est [a] -> [a] parce quune liste diffrentielle est simplement une fonction qui prend une liste et en retourne une autre. Convertir des listes normales en listes diffrentielles et vice versa est trs facile : toDiffList :: [a] -> DiffList a toDiffList xs = DiffList (xs++) fromDiffList :: DiffList a -> [a] fromDiffList (DiffList f) = f []

Pour crer une liste normale partir dune liste diffrentielle, on fait comme on faisait avant en crant une fonction qui prpose une autre liste. Puisquune liste diffrentielle est une fonction qui prpose une autre liste la liste quelle reprsente, pour obtenir cette liste, il suffit de lappliquer une liste vide ! Voici linstance de Monoid : instance Monoid (DiffList a) where mempty = DiffList (\xs -> [] ++ xs) (DiffList f) `mappend` (DiffList g) = DiffList (\xs -> f (g xs))

Remarquez comme pour ces listes, mempty est juste la fonction id et mappend est simplement la composition de fonctions. Voyons si cela marche : ghci> fromDiffList (toDiffList [1,2,3,4] `mappend` toDiffList [1,2,3]) [1,2,3,4,1,2,3]

Tip top ! On peut maintenant amliorer lefficacit de gcdReverse en lui faisant utiliser des listes diffrentielles plutt que des listes normales : import Control.Monad.Writer gcd' :: Int -> Int -> Writer (DiffList String) Int gcd' a b | b == 0 = do tell (toDiffList ["Finished with " ++ show a]) return a | otherwise = do result <- gcd' b (a `mod` b) tell (toDiffList [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)]) return result

On a simplement d changer le type du monode de [String] en DiffList String , et changer nos listes normales en listes diffrentielles avec toDiffList

quand on utilisait tell . Voyons si le registre est assembl correctement : ghci> mapM_ putStrLn . fromDiffList . snd . runWriter $ gcdReverse 110 34 Finished with 2 8 mod 2 = 0 34 mod 8 = 2 110 mod 34 = 8

On fait gcdReverse 110 34 , puis on utilise runWriter pour sortir le rsultat du newtype , et on applique snd pour nobtenir que le registre, puis on applique fromDiffList pour le convertir en une liste normale, et enfin on laffiche entre par entre lcran.

Comparer les performances


Pour vous faire une ide de lordre de grandeur de lamlioration des performances en utilisant les listes diffrentielles, considrez cette fonction qui dcompte partir dun nombre jusqu zro, et produit son registre dans le sens inverse, comme gcdReverse , de manire ce que le registre compte les nombres dans lordre croissant : finalCountDown :: Int -> Writer (DiffList String) () finalCountDown 0 = do tell (toDiffList ["0"]) finalCountDown x = do finalCountDown (x-1) tell (toDiffList [show x])

Si on lui donne 0 , elle lcrit simplement dans le registre. Pour tout autre nombre, elle commence dabord par dcompter depuis son prdcesseur, jusqu 0 et enfin, ajoute ce nombre au registre. Ainsi, si lon applique finalCountDown 100 , la chane de caractres "100" sera la dernire du registre. Si vous chargez cete fonction dans GHCi et que vous lappliquez un nombre gros, comme 500000 , vous verrez quelle compte rapidement depuis 0 vers lavant : ghci> mapM_ putStrLn . fromDiffList . snd . runWriter $ finalCountDown 500000 0 1 2 ...

Cependant, si on la change pour utiliser des listes normales plutt que des listes diffrentielles, de cette manire : finalCountDown :: Int -> Writer [String] () finalCountDown 0 = do tell ["0"] finalCountDown x = do finalCountDown (x-1) tell [show x]

Et quon demande GHCi de compter : ghci> mapM_ putStrLn . snd . runWriter $ finalCountDown 500000

On voit que le dcompte est trs lent. Bien sr, ce nest pas la faon rigoureuse et scientifique de tester la vitesse de nos programmes, mais on peut dj voir que dans ce cas, utiliser des listes diffrentielles commence fournir des rsultats immdiatement alors que pour des listes normales, cela prend une ternit. Oh, au fait, la chanson Final Countdown dEurope est maintenant coince dans votre tte. De rien !

La lire ? Pas cette blague encore.


Dans le chapitre sur les foncteurs applicatifs, on a vu que le type des fonctions, (->) r , tait une instance de Functor . Mapper une fonction f sur une fonction g cre une fonction qui prend la mme chose que g , applique g dessus puis applique f au rsultat. En gros, on cre une nouvelle fonction qui est comme g , mais qui applique f avant de renvoyer son rsultat. Par exemple : ghci> let f = (*5) ghci> let g = (+3) ghci> (fmap f g) 8 55

Nous avons aussi vu que les fonctions taient des foncteurs applicatifs. Elles nous permettaient doprer sur les rsultats terme de fonctions comme si lon avait dj ces rsultats. Par exemple : ghci> let f = (+) <$> (*2) <*> (+10) ghci> f 3 19

Lexpression (+) <$> (*2) <*> (+10) cre une fonction qui prend un nombre, donne ce nombre (*2) et (+10) , et somme les rsultats. Par exemple, si lon applique cette fonction 3 , elle applique la fois (*2) et (+10) sur 3 , rsultant en 6 et 13 . Puis, elle appelle (+) avec 6 et 13 et le rsultat est 19 . Non seulement le type des fonctions (->) r est un foncteur et un foncteur applicatif, mais cest aussi une monade. Tout comme les autres valeurs monadiques que lon a croises jusquici, une fonction peut tre considre comme une valeur dans un contexte. Le contexte dans le cas des fonctions est que la valeur nest pas encore l, et quil faudra donc appliquer la fonction sur quelque chose pour obtenir la valeur rsultante. Puisquon a dj vu comment les fonctions fonctionnent comme des foncteurs et des foncteurs applicatifs, plongeons immdiatement dans le grand bassin et voyons linstance de Monad . Elle est situe dans Control.Monad.Instances et ressemble a : instance Monad ((->) r) where return x = \_ -> x h >>= f = \w -> f (h w) w

Nous avons dj vu comment pure tait implmente pour les fonctions, et return est la mme chose que pure . Elle prend une valeur, et la place dans un contexte minimal contenant cette valeur comme rsultat. Et le seul moyen de crer une fonction qui renvoie toujours le mme rsultat consiste lui faire ignorer compltement son paramtre. Limplmentation de >>= semble un peu plus cryptique, mais elle ne lest pas tant que a. Quand on utilisait >>= pour donner des valeurs monadiques une fonction, le rsultat tait toujours une valeur monadique. Dans ce cas, si lon donne une fonction une autre fonction, le rsultat est toujours une fonction. Cest pourquoi le rsultat commence comme une lambda. Toutes les implmentations de >>= quon a vues jusquici isolaient dune certaine manire le rsultat de la valeur monadique et lui appliquaient la fonction f . Cest la mme chose ici. Pour obtenir le rsultat dune fonction, il faut lappliquer quelque chose, ce quon fait avec (h w) , puis on applique f au rsultat. f retourne une valeur monadique, qui est une fonction dans notre cas, donc on lapplique galement w . Si vous ne comprenez pas comment >>= marche ici, ne vous inquitez pas, avec les exemples on va voir que cest simplement une monade comme une autre. Voici une expression do qui utilise cette monade : import Control.Monad.Instances addStuff :: Int -> Int addStuff = do a <- (*2) b <- (+10) return (a+b)

Cest la mme chose que lexpression applicative quon a crite plus haut, seulement maintenant elle se base sur le fait que les fonctions soient des monades. Une expression do rsulte toujours en une valeur monadique, et celle-ci ne fait pas exception. Le rsultat de cette valeur monadique est une fonction. Ce qui se passe ici, cest quelle prend un nombre, puis fait (*2) sur ce nombre, ce qui rsulte en a . (+10) est galement applique au mme nombre que celui donn (*2) , et le rsultat devient b . return , comme dans les autres monades, na pas dautre effet que de crer une valeur monadique prsente en rsultat. Elle prsente ici a + b comme le rsultat de cette fonction. Si on essaie, on obtient le mme rsultat quavant : ghci> addStuff 3 19

(*2) et (+3) sont appliques au nombre 3 dans ce cas. return (a+b) est galement applique au nombre 3 , mais elle ignore ce paramtre et retourne toujours a+b en rsultat. Pour cette raison, la monade des fonctions est aussi appele la monade de lecture. Toutes les fonctions lisent en effet la mme source. Pour illustrer cela encore mieux, on peut rcrire addStuff ainsi : addStuff :: Int -> Int addStuff x = let a = (*2) x b = (+10) x in a+b

On voit que la monade de lecture nous permet de traiter les fonctions comme des valeurs dans un contexte. On peut agir comme si lon savait dj ce que les fonctions retournaient. Ceci est russi en collant toutes les fonctions ensemble et en donnant le paramtre de la fonction ainsi cre toutes les fonctions qui la

composent. Ainsi, si lon a plein de fonctions qui attendent toutes un mme paramtre, on peut utiliser la monade de lecture pour extraire en quelque sorte leur rsultat, et limplmentation de >>= sassurera que tout se passe comme prvu.

Calculs tats dans tous leurs tats


Haskell est un langage pur, et grce cela, nos programmes sont faits de fonctions qui ne peuvent pas altrer un tat global ou des variables, elles ne peuvent que faire des calculs et retourner des rsultats. Cette restriction rend en fait plus simple la rflexion sur nos programmes, puisquelle nous libre de linquitude de savoir quelle est la valeur de chaque variable un instant donn. Cependant, certains problmes sont intrinsquement composs dtats en ce quils se basent sur des tats pouvant voluer dans le temps. Bien que de tels problmes ne soient pas un problme pour Haskell, ils peuvent parfois tre fastidieux modliser. Cest pourquoi Haskell offre la monade dtats, qui rend la gestion de problmes tats simple comme bonjour tout en restant propre et pur. Lorsquon travaillait avec des nombres alatoires, on manipulait des fonctions qui prenaient un gnrateur alatoire en paramtre et retournaient un nombre alatoire et un nouveau gnrateur alatoire. Si lon voulait gnrer plusieurs nombres alatoires, nous avions toujours un gnrateur obtenu en rsultat en mme temps que le nombre alatoire prcdent. Quand on avait crit une fonction prenant un StdGen et jetant trois fois une pice en se basant sur un gnrateur, on avait fait : threeCoins :: StdGen -> (Bool, Bool, Bool) threeCoins gen = let (firstCoin, newGen) = random gen (secondCoin, newGen') = random newGen (thirdCoin, newGen'') = random newGen' in (firstCoin, secondCoin, thirdCoin)

Elle prenait un gnrateur gen et random gen retournait alors une valeur Bool ainsi quun nouveau gnrateur. Pour lancer la deuxime pice, on utilisait le nouveau gnrateur, et ainsi de suite. Dans la plupart des autres langages, on naurait pas retourn un nouveau gnrateur avec le nombre alatoire. On aurait simplement modifi le gnrateur existant ! Mais puisquHaskell est pur, on ne peut pas faire cela, donc nous devons prendre un tat, crer partir de celui-ci un rsultat ainsi quun nouvel tat, puis utiliser ce nouvel tat pour gnrer dautres rsultats. On pourrait se dire que pour viter davoir grer manuellement ce genre de calculs tats, on devrait se dbarrasser de la puret dHaskell. Eh bien, cela nest pas ncessaire, puisquil existe une petite monade spciale appele la monade dtats qui soccupe de toute cette manipulation dtats pour nous, et sans abandonner la puret qui fait que programmer en Haskell est tellement cool. Donc, pour nous aider mieux comprendre le concept de calculs tats, donnons leur un type. On dira quun calcul tats est une fonction qui prend un tat et retourne une valeur et un nouvel tat. Le type de la fonction serait : s -> (a,s)

s est le type de ltat et a le resultat du calcul tats.

Laffectation dans la plupart des autres langages pourrait tre vue comme un calcul tats. Par exemple, quand on fait x = 5 dans un langage impratif, cela assignera gnralement la valeur 5 la variable x , et lexpression elle-mme aura aussi pour valeur 5 . Si vous imaginez cela fonctionnellement, vous pouvez le voir comme une fonction qui prend un tat (ici, toutes les variables qui ont t affectes prcdemment) et retourne un rsultat (ici 5 ) et un nouvel tat, qui contiendrait toutes les valeurs prcdentes des variables, ainsi que la variable frachement affecte.

Ce calcul tats, une fonction prenant un tat et retournant un rsultat et un tat, peut aussi tre vu comme une valeur dans un contexte. La valeur est le rsultat, alors que le contexte est quon doit fournir un tat initial pour pouvoir extraire la valeur, et quon retourne en plus du rsultat un nouvel tat.

Piles et rochers
Disons quon souhaite modliser la manipulation dune pile. Vous avez une pile de choses lune sur lautre, et vous pouvez soit ajouter quelque chose au sommet de la pile, soit enlever quelque chose du sommet de la pile. Quand on place quelque chose sur la pile, on dit quon lempile, et quand on enlve quelque chose on dit quon le dpile. Si vous voulez quelque chose qui est tout en bas de la pile, il faut dabord dpiler tout ce qui est au dessus. Nous utiliserons une liste pour notre pile et la tte de la liste sera le sommet de la pile. Pour nous aider dans notre tche, nous crerons deux fonctions : pop et push . pop prend une pile, dpile un lment, et retourne llment en rsultat ainsi que la nouvelle pile sans cet lment. push prend un lment et une pile et empile llment sur la pile. Elle retourne () en rsultat, ainsi quune nouvelle pile. Voici : type Stack = [Int] pop :: Stack -> (Int,Stack)

pop (x:xs) = (x,xs) push :: Int -> Stack -> ((),Stack) push a xs = ((),a:xs)

On utilise () comme rsultat lorsque lon empile parce quempiler un lment sur la pile na pas de rsultat intressant, son travail est simplement de changer la pile. Remarquez que si lon applique que le premier paramtre de push , on obtient un calcul tats. pop est dj un calcul tats de par son type. crivons un petit bout de code qui simule une pile en utilisant ces fonctions. On va prendre une pile, empiler 3 et dpiler deux lments, juste pour voir. Voici : stackManip :: Stack -> (Int, Stack) stackManip stack = let ((),newStack1) = push 3 stack (a ,newStack2) = pop newStack1 in pop newStack2

On prend une pile stack et on fait push 3 stack , ce qui retourne un tuple. La premire composante de cette paire est () et la seconde est la nouvelle pile, quon appelle newStack1 . Ensuite, on dpile un nombre de newStack1 , ce qui retourne un nombre a (qui est 3 ) et une nouvelle pile quon appelle newStack2 . Puis, on dpile un nombre de newStack2 et obtient un nombre b et une nouvelle pile newStack3 . Le tuple de ce nombre et cette pile est retourn. Essayons : ghci> stackManip [5,8,2,1] (5,[8,2,1])

Cool, le rsultat est 5 et la nouvelle pile est [8, 2, 1] . Remarquez comme stackManip est elle-mme un calcul tats. On a pris plusieurs calculs tats et on les a colls ensemble. Hmm, a sonne familier. Le code ci-dessus de stackManip est un peu fastidieux puisquon donne manuellement ltat chaque calcul tats, puis on rcupre un nouvel tat quon donne nouveau au prochain calcul. Ce serait mieux si, au lieu de donner les piles manuellement chaque fonction, on pouvait crire : stackManip = do push 3 a <- pop pop

Eh bien, avec la monade dtats cest exactement ce quon crira. Avec elle, on peut prendre des calculs tats comme ceux-ci et les utiliser sans se proccuper de la gestion de ltat manuellement.

La monade State
Le module Control.Monad.State fournit un newtype qui enveloppe des calculs tats. Voici sa dfinition : newtype State s a = State { runState :: s -> (a,s) }

Un State s a est un calcul tats qui manipule un tat de type s et retourne un rsultat de type a . Maintenant quon a vu ce que sont des calculs tats et comment ils pouvaient tre vus comme des valeurs avec des contextes, regardons leur instance de Monad : instance Monad (State s) where return x = State $ \s -> (x,s) (State h) >>= f = State $ \s -> let (a, newState) = h s (State g) = f a in g newState

Regardons dabord return . Le but de return est de prendre une valeur et den faire un calcul tats retournant toujours cette valeur. Cest pourquoi on cre une lambda \s -> (x, s) . On prsente toujours x en rsultat du calcul tats et ltat est inchang, parce que return place la valeur dans un contexte minimal. Donc return cre un calcul tats qui retourne une certaine valeur sans changer ltat. Quen est-il de >>= ? Eh bien, le rsultat obtenu en donnant une fonction un calcul tats par >>= doit tre un calcul tats, nest-ce pas ? On commence donc crire le newtype State et une lambda. Cette lambda doit tre notre nouveau calcul tats. Mais que doit-elle faire ? Eh bien, on doit extraire le rsultat du premier calcul tats dune manire ou dune autre. Puisquon se trouve dans un calcul tats, on peut donner au calcul tats h notre tat actuel s , ce qui retourne une paire dun rsultat et dun nouvel tat : (a, newState) . chaque fois quon a implment >>= , aprs avoir extrait le rsultat de la valeur monadique, on appliquait la fonction f dessus pour obtenir une nouvelle valeur monadique. Dans Writer , aprs avoir fait cela et

obtenu la nouvelle valeur monadique, on devait ne pas oublier de tenir compte du contexte en faisant mappend entre lancienne valeur monodale et la nouvelle. Ici, on fait f a et on obtient un nouveau calcul tats g . Maintenant quon a un calcul tats et un tat (qui sappelle newState ) on applique simplement le calcul tats g ltat newState . Le rsultat est un tuple contenant le rsultat final et ltat final ! Ainsi, avec >>= , on colle ensemble deux calculs tats, seulement le second est cach dans une fonction qui prend le rsultat du premier. Puisque pop et push sont dj des calculs tats, il est facile de les envelopper dans un State . Regardez : import Control.Monad.State pop :: State Stack Int pop = State $ \(x:xs) -> (x,xs) push :: Int -> State Stack () push a = State $ \xs -> ((),a:xs)

pop est dj un calcul tats et push prend un Int et retourne un calcul tats. Maintenant, on peut rcrire lexemple prcdent o lon empilait 3 sur la pile avant de dpiler deux nombres ainsi : import Control.Monad.State stackManip :: State Stack Int stackManip = do push 3 a <- pop pop

Voyez-vous comme on a coll ensemble un empilement et deux dpilements en un calcul tats ? Quand on sort ce calcul de son newtype , on obtient une fonction laquelle on peut fournir un tat initial : ghci> runState stackManip [5,8,2,1] (5,[8,2,1])

On navait pas eu besoin de lier le premier pop a vu quon nutilise pas ce a . On aurait pu crire : stackManip :: State Stack Int stackManip = do push 3 pop pop

Plutt cool. Mais et si lon voulait faire ceci : dpiler un nombre de la pile, puis si ce nombre est 5 , lempiler nouveau et sinon, empiler 3 et 8 plutt ? Voici le code : stackStuff :: State Stack () stackStuff = do a <- pop if a == 5 then push 5 else do push 3 push 8

Cest plutt simple. Lanons-la sur une pile vide. ghci> runState stackStuff [9,0,2,1,0] ((),[8,3,0,2,1,0])

Souvenez-vous, les expressions do rsultent en des valeurs monadiques, et avec la monade State , une expression do est donc une fonction tats. Puisque stackManip et stackStuff sont des calculs tats ordinaires, on peut les coller ensemble pour faire des calculs plus compliqus. moreStack :: State Stack () moreStack = do a <- stackManip if a == 100 then stackStuff else return ()

Si le rsultat de stackManip sur la pile actuelle est 100 , on fait stackStuff , sinon on ne fait rien. return () conserve ltat comme il est et ne fait rien. Le module Control.Monad.State fournit une classe de types appele MonadState qui contient deux fonctions assez utiles, jai nomm get et put . Pour State , la fonction get est implmente ainsi : get = State $ \s -> (s,s)

Elle prend simplement ltat courant et le prsente en rsultat. La fonction put prend un tat et cre une fonction tats qui remplace ltat courant par celui-ci : put newState = State $ \s -> ((),newState)

Avec ces deux fonctions, on peut voir la pile courante ou la remplacer par une toute nouvelle pile. Comme a : stackyStack :: State Stack () stackyStack = do stackNow <- get if stackNow == [1,2,3] then put [8,3,1] else put [9,2,1]

Il est intressant dexaminer le type quaurait >>= si elle tait restreinte aux valeurs State : (>>=) :: State s a -> (a -> State s b) -> State s b

Remarquez que le type de ltat s reste le mme, mais le type du rsultat peut changer de a en b . Cela signifie que lon peut coller ensemble des calculs tats dont les rsultats sont de diffrents types, mais le type des tats doit tre le mme. Pourquoi cela ? Eh bien, par exemple, pour Maybe , >>= a ce type : (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b

Il est logique que la monade elle-mme, Maybe , ne change pas. a naurait aucun sens dutiliser >>= entre deux monades diffrentes. Eh bien, pour la monade dtats, la monade est en fait State s , donc si ce s tait diffrent, on utiliserait >>= entre deux monades diffrentes.

Alatoire et monade dtats


Au dbut de la section, on a vu que gnrer des nombres alatoires pouvait parfois sembler bizarre puisque chaque fonction alatoire prenait un gnrateur et retourne un nombre alatoire ainsi quun nouveau gnrateur, qui devait ensuite tre utilis la place du prcdent pour gnrer de nouveaux nombres alatoires. La monade dtats rend cela bien plus facile. La fonction random de System.Random a pour type : random :: (RandomGen g, Random a) => g -> (a, g)

Signifiant quelle prend un gnrateur alatoire et produit un nombre alatoire et un nouveau gnrateur. On peut voir que cest un calcul tats, on peut donc lenvelopper dans le constructeur de newtype State et lutiliser comme une valeur monadique afin que la gestion de ltat soit faite pour nous : import System.Random import Control.Monad.State randomSt :: (RandomGen g, Random a) => State g a randomSt = State random

prsent, si lon souhaite lancer trois pices ( True pour pile, False pour face), on peut faire : import System.Random import Control.Monad.State threeCoins :: State StdGen (Bool,Bool,Bool) threeCoins = do a <- randomSt b <- randomSt c <- randomSt return (a,b,c)

threeCoins est un calcul tats et aprs avoir pris un gnrateur alatoire initial, il le passe au premier randomSt , qui produit un nombre et un nouveau

gnrateur, qui est pass au prochain et ainsi de suite. On utilise return (a, b, c) pour prsenter (a, b, c) comme le rsultat sans changer le gnrateur le plus rcent. Essayons : ghci> runState threeCoins (mkStdGen 33) ((True,False,True),680029187 2103410263)

Erreur, erreur, ma belle erreur


On sait prsent que Maybe est utilis pour ajouter un contexte dchec possible des valeurs. Une valeur peut tre Just something ou Nothing . Aussi utile que cela puisse tre, quand on a un Nothing , tout ce quon sait cest quil y a eu une sorte derreur, mais il ny a pas de moyen den savoir plus sur le type de lerreur et sa raison. Le type Either e a au contraire, nous permet dincorporer un contexte dchec ventuel nos valeurs tout en tant capable dattacher des valeurs aux checs, afin de dcrire ce qui sest mal pass et de fournir dautres informations intressantes concernant lchec. Une valeur Either e a peut tre ou bien une valeur Right , indiquant la bonne rponse et un succs, ou une valeur Left , indiquant lchec. Par exemple : ghci> :t Right 4 Right 4 :: (Num t) => Either a t ghci> :t Left "out of cheese error" Left "out of cheese error" :: Either [Char] b

Cest simplement un Maybe avanc, il est donc logique que ce soit une monade, parce quelle peut aussi tre vue comme une valeur avec un contexte additionnel dchec ventuel, seulement maintenant il y a une valeur attache cette erreur. Son instance de Monad est similaire celle de Maybe et peut tre trouve dans Control.Monad.Error : instance (Error e) => Monad (Either e) where return x = Right x Right x >>= f = f x Left err >>= f = Left err fail msg = Left (strMsg msg)

Comme toujours, return prend une valeur et la place dans un contexte par dfaut minimal. Elle enveloppe notre valeur dans le constructeur Right parce quon utilise Right pour reprsenter un calcul russi pour lequel un rsultat est prsent. Cest presque comme le return de Maybe . >>= examine deux cas possibles : un Left ou un Right . Dans le cas dun Right , la fonction f est applique la valeur lintrieur, comme pour Just . Dans le cas dune erreur, la valeur Left est conserve ainsi que son contenu qui dcrit lerreur. Linstance de Monad d Either e a a un pr-requis additionnel, qui est que le type de la valeur contenue dans Left , celui index par le paramtre de type e , doit tre une instance de la classe de types Error . La classe de types Error est pour les types dont les valeurs peuvent tre vues comme des messages derreur. Elle dfinit une fonction strMsg , qui prend une erreur sous la forme dune chane de caractres et retourne une telle valeur. Un bon exemple dinstance d Error est, eh bien, le type String ! Dans le cas de String , la fonction strMsg retourne simplement ce quelle a reu : ghci> :t strMsg strMsg :: (Error a) => String -> a ghci> strMsg "boom!" :: String "boom!"

Puisquon utilise gnralement des String pour dcrire nos erreurs quand on utilise Either , on na pas trop sen soucier. Quand un filtrage par motif choue dans la notation do , une valeur Left est utilise pour indiquer cet chec. Voici quelques exemples dutilisation : ghci> Left "boom" >>= \x -> return (x+1) Left "boom" ghci> Right 100 >>= \x -> Left "no way!" Left "no way!"

Quand on utilise >>= pour donner une valeur Left une fonction, la fonction est ignore et une valeur Left identique est retourne. Quand on donne une valeur Right une fonction, la fonction est applique sur ce qui est lintrieur du Right , mais dans ce cas, la fonction produit quand mme une valeur Left ! Quand on donne une valeur Right une fonction qui russit galement, on obtient une erreur de type curieuse ! Hmmm. ghci> Right 3 >>= \x -> return (x + 100)

<interactive>:1:0: Ambiguous type variable `a' in the constraints: `Error a' arising from a use of `it' at <interactive>:1:0-33 `Show a' arising from a use of `print' at <interactive>:1:0-33 Probable fix: add a type signature that fixes these type variable(s)

Haskell dit quil ne sait pas quel type choisir pour la partie e de notre valeur Either e a , bien quon nait seulement affich la partie Right . Ceci est d la contrainte Error e de linstance de Monad . Ainsi, si vous obtenez une telle erreur de type en utilisant la monade Either , ajoutez une signature de type explicite : ghci> Right 3 >>= \x -> return (x + 100) :: Either String Int Right 103

Parfait, cela fonctionne prsent ! part ce petit cueil, utiliser cette monade est trs similaire lutilisation de la monade Maybe . Dans le chapitre prcdent, on utilisait les aspect monadiques de Maybe pour simuler latterrissage doiseaux sur la perche dun funambule. En tant quexercice, vous pouvez rcrire cela avec la monade derreur afin que lorsque le funambule glisse et tombe, on sache combien il y avait doiseaux de chaque ct de la perche linstant de la chute.

Quelques fonctions monadiques utiles


Dans cette section, on va explorer quelques fonctions qui oprent sur des valeurs monadiques ou retournent des valeurs monadiques (ou les deux la fois !). De telles fonctions sont dites monadiques. Bien que certaines dentre elles seront toutes nouvelles, dautres ne seront que les quivalents monadiques de fonctions que lon connaissait dj, comme filter ou foldl . Voyons donc de qui il sagit !

liftM et ses amies


Alors que nous dbutions notre priple vers le sommet du Mont Monade, on a dabord vu des foncteurs, qui taient pour les choses sur lesquelles on pouvait mapper. Puis, on a appris quil existait des foncteurs amliors appels foncteurs applicatifs, qui nous permettaient dappliquer des fonctions normales sur plusieurs valeurs applicatives ainsi que de prendre une valeur normale et de la placer dans un contexte par dfaut. Finalement, on a introduit les monades comme des foncteurs applicatifs amliors, qui ajoutaient la possibilit de donner ces valeurs avec des contextes des fonctions normales. Ainsi, chaque monade est un foncteur applicatif, et chaque foncteur applicatif est un foncteur. La classe de types Applicative a une contrainte de classe telle que notre type doit tre une instance de Functor avant de pouvoir devenir une instance d Applicative . Mais bien que Monad devrait avoir la mme contrainte de classe pour Applicative , puisque chaque monade est un foncteur applicatif, elle ne la pas, parce que la classe de types Monad a t introduite en Haskell longtemps avant Applicative . Mais bien que chaque monade soit un foncteur, on na pas besoin que son type soit une instance de Functor grce la fonction liftM . liftM prend une fonction et une valeur monadique et mappe la fonction sur la valeur. Cest donc comme fmap ! Voici le type de liftM : liftM :: (Monad m) => (a -> b) -> m a -> m b

Et le type de fmap : fmap :: (Functor f) => (a -> b) -> f a -> f b

Si un type est la fois instance de Functor et de Monad et obit leurs lois respectives, alors ces deux fonctions doivent tre identiques (cest le cas pour toutes les monades quon a vues jusquici). Un peu comme pure et return font la mme chose, seulement que la premire a une contrainte de classe Applicative alors que lautre a une contrainte Monad . Testons liftM : ghci> liftM (*3) (Just 8) Just 24 ghci> fmap (*3) (Just 8) Just 24 ghci> runWriter $ liftM not $ Writer (True, "chickpeas") (False,"chickpeas") ghci> runWriter $ fmap not $ Writer (True, "chickpeas") (False,"chickpeas") ghci> runState (liftM (+100) pop) [1,2,3,4] (101,[2,3,4]) ghci> runState (fmap (+100) pop) [1,2,3,4]

(101,[2,3,4])

On sait dj comment fmap fonctionne avec les valeurs Maybe . Et liftM est identique. Pour les valeurs Writer , la fonction est mappe sur la premire composante du tuple, qui est le rsultat. Faire fmap ou liftM sur un calcul tats rsulte en un autre calcul tats, mais son rsultat final sera modifi par la fonction passe en argument. Si lon navait pas mapp (+100) sur pop , elle aurait retourn (1,[2,3,4]) . Voici comment liftM est implmente : liftM :: (Monad m) => (a -> b) -> m a -> m b liftM f m = m >>= (\x -> return (f x))

Ou, en notation do : liftM :: (Monad m) => (a -> b) -> m a -> m b liftM f m = do x <- m return (f x)

On donne la valeur monadique m la fonction, et on applique f son rsultat avant de le retourner dans un contexte par dfaut. Grce aux lois des monades, on a la garantie que le contexte est inchang, seule la valeur rsultante est altre. On voit que liftM est implmente sans faire rfrence la classe de types Functor . Cela signifie quon a pu implmenter fmap (ou liftM , peu importe son nom) en utilisant simplement ce que les monades nous offraient. Ainsi, on peut conclure que les monades sont plus fortes que les foncteurs normaux. La classe de types Applicative nous permet dappliquer des fonctions entre des valeurs dans des contextes comme si elles taient des valeurs normales. Comme cela : ghci> (+) <$> Just 3 <*> Just 5 Just 8 ghci> (+) <$> Just 3 <*> Nothing Nothing

Utiliser ce style applicatif rend les choses plutt faciles. <$> est juste fmap et <*> est une fonction de la classe de types Applicative qui a pour type : (<*>) :: (Applicative f) => f (a -> b) -> f a -> f b

Cest donc un peu comme fmap , seulement la fonction elle-mme est dans un contexte. Il faut dune certaine faon lextraire de ce contexte et la mapper sur la valeur f a , puis restaurer le contexte. Grce la curryfication par dfaut en Haskell, on peut utiliser la combinaison de <$> et <*> pour appliquer des fonctions qui prennent plusieurs paramtres entre plusieurs valeurs applicatives. Il savre que tout comme fmap , <*> peut aussi tre implmente uniquement avec ce que la classe de types Monad nous donne. La fonction ap est simplement <*> , mais avec une contrainte de classe Monad plutt qu Applicative . Voici sa dfinition : ap :: (Monad m) => m (a -> b) -> m a -> m b ap mf m = do f <- mf x <- m return (f x)

mf est une valeur monadique dont le rsultat est une fonction. Puisque la fonction est dans un contexte comme la valeur, on rcupre la fonction de son contexte et on lappelle f , puis on rcupre la valeur quon appelle x et finalement on applique la fonction avec la valeur et on prsente le rsultat. Voici un exemple rapide : ghci> Just (+3) <*> Just 4 Just 7 ghci> Just (+3) `ap` Just 4 Just 7 ghci> [(+1),(+2),(+3)] <*> [10,11] [11,12,12,13,13,14] ghci> [(+1),(+2),(+3)] `ap` [10,11] [11,12,12,13,13,14]

On voit prsent que les monades sont plus fortes que les foncteurs applicatifs, puisquon peut utiliser les fonctions de Monad pour implmenter celles d Applicative . En fait, trs souvent, lorsquun type savre tre une monade, les gens crivent dabord linstance de Monad , puis crent une instance d Applicative en disant que pure est return et <*> est ap . De faon similaire, si vous avec une instance de Monad , vous pouvez faire une instance de

Functor en disant que fmap est liftM . La fonction liftA2 est pratique pour appliquer une fonction entre deux valeurs applicatives. Elle est simplement dfinie comme : liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c liftA2 f x y = f <$> x <*> y

La fonction liftM2 fait la mme chose, mais avec une contrainte Monad . Il existe galement liftM3 , liftM4 et liftM5 . Nous avons vu que les monades taient plus puissantes que les foncteurs applicatifs et les foncteurs et que bien que toutes les monades soient des foncteurs applicatifs et des foncteurs, elles nont pas ncessairement dinstances de Functor et Applicative , et on a donc vu les fonctions quivalentes celles quon utilisait sur nos foncteurs et nos foncteurs applicatifs.

La fonction join
Voici de quoi faire travailler vos neurones : si le rsultat dune valeur monadique est une autre valeur monadique, autrement dit si une valeur monadique est imbrique dans une autre, peut-on les aplatir en une valeur monadique simple ? Par exemple, si lon a Just (Just 9) , peut-on en faire un Just 9 ? Il savre que toute valeur monadique imbrique peut tre aplatie et ceci est une proprit unique des monades. Pour cela, la fonction join existe. Son type est : join :: (Monad m) => m (m a) -> m a

Ainsi, elle prend une valeur monadique dans une valeur monadique et retourne simplement une valeur monadique, donc en quelque sorte elle laplatit. La voici en action sur diverses valeurs Maybe : ghci> join (Just (Just 9)) Just 9 ghci> join (Just Nothing) Nothing ghci> join Nothing Nothing

La premire ligne contient un calcul russi en rsultat dun calcul russi, donc ils sont joints en un gros calcul russi. La deuxime ligne contient un Nothing comme rsultat dans un Just . chaque fois quon a eu affaire des valeurs Maybe auparavant et quon voulait en combiner plusieurs, que ce soit avec <*> ou >>= , elles devaient toutes tre Just pour que le rsultat soit Just . Sil y avait un seul chec parmi elles, le rsultat tait un chec. Il en va de mme ici. la troisime ligne, on essaie daplatir ce qui est dj un chec, le rsultat reste un chec. Aplatir les listes est intuitif : ghci> join [[1,2,3],[4,5,6]] [1,2,3,4,5,6]

Comme vous le voyez, join est juste concat . Pour aplatir une valeur Writer donc le rsultat est une valeur Writer , on doit mappend les valeurs monodales. ghci> runWriter $ join (Writer (Writer (1,"aaa"),"bbb")) (1,"bbbaaa")

Le valeur monodale extrieure "bbb" vient dabord, puis "aaa" est juxtapose. Intuitivement, pour examiner la valeur dun Writer , il faut que sa valeur monodale soit mise au registre dabord, et seulement ensuite peut-on examiner ce quelle contient. Aplatir des valeurs Either est similaire au cas des valeurs Maybe : ghci> join (Right (Right 9)) :: Either String Int Right 9 ghci> join (Right (Left "error")) :: Either String Int Left "error" ghci> join (Left "error") :: Either String Int Left "error"

Si on applique join un calcul tats dont le rsultat est un calcul tats, le rsultat est un calcul tats qui lance dabord le calcul extrieur et ensuite le calcul intrieur. Regardez : ghci> runState (join (State $ \s -> (push 10,1:2:s))) [0,0,0] ((),[10,1,2,0,0,0])

La lambda ici prend un tat et place 2 et 1 dans la pile et prsente push 10 comme son rsultat. Quand cette chose est aplatie avec join et value, elle empile dabord 2 et 1 puis push 10 est excut, empilant un 10 en sommet de pile. Limplmentation de join est comme suit : join :: (Monad m) => m (m a) -> m a join mm = do m <- mm m

Puisque le rsultat de mm est une valeur monadique, on obtient ce rsultat et on le place ensuite sur sa propre ligne, comme une valeur monadique. Lastuce ici est que lorsquon fait m <- mm , le contexte de la monade en question est pris en compte. Cest pourquoi, par exemple, des valeurs Maybe rsultent en des Just seulement si les valeurs intrieures et extrieures sont toutes deux des valeurs Just . Voici quoi cela ressemblait si mm tait fix lavance la valeur Just (Just 8) : joinedMaybes :: Maybe Int joinedMaybes = do m <- Just (Just 8) m

Peut-tre que la chose la plus intressante propos de join est que, pour chaque monade, donner une valeur monadique une fonction avec >>= est quivalent mapper cette fonction sur la valeur puis utiliser join pour aplatir la valeur monadique imbrique rsultante ! En dautres termes, m >>= f est toujours gal join (fmap f m) ! Cest logique quand on y rflchit. Avec >>= , on donne une valeur monadique une fonction qui attend une valeur normale et retourne une valeur monadique. Si lon mappe cette fonction sur la valeur monadique, on a une valeur monadique lintrieur dune valeur monadique. Par exemple, si lon a Just 9 et la fonction \x -> Just (x + 1) . En mappant la fonction sur Just 9 , on obtient Just (Just 10)) . Le fait que m >>= f est toujours gal join (fmap f) est trs utile quand on cre nos propres instances de Monad pour certains types parce quil est souvent plus facile de voir comment on aplatirait une valeur monadique imbrique plutt que de trouver comment implmenter >>= .

filterM
La fonction filter est le pain quotidien de la programmation en Haskell ( map tant le beurre sur la tartine). Elle prend un prdicat et une liste filtrer et retourne une nouvelle liste dans laquelle tous les lments satisfaisant le prcidat ont t gards. Son type est : filter :: (a -> Bool) -> [a] -> [a]

Le prdicat prend un lment de la liste et retourne une valeur Bool . Et si la valeur Bool retourne tait une valeur monadique ? Ouah ! Cest--dire, et si elle venait avec son contexte ? Est-ce que cela marcherait ? Par exemple, si chaque valeur True ou False que le prdicat produisait tait accompagne dune valeur monodale, comme ["Accepted the number 5"] ou ["3 is too small"] ? On dirait que a pourrait marcher. Si ctait le cas, on sattendrait ce que la liste rsultante vienne galement avec un registre combinant tous les registres obtenus en route. Donc, si le Bool retourn par le prdicat vient avec un contexte, on sattendrait ce que la liste rsultante ait galement un contexte attach, autrement, le contexte de chaque Bool serait perdu. La fonction filterM de Control.Monad fait exactement ce que lon souhaite ! Son type est : filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]

Le prdicat retourne une valeur monadique dont le rsultat est un Bool , mais puisque cest une valeur monadique, son contexte peut-tre nimporte quoi, un possible chec, du non dterminisme, et encore plus ! Pour sassurer que le contexte est reflt dans le rsultat final, celui-ci est aussi une valeur monadique. Prenons une liste et gardons seulement les valeurs infrieures 4. Pour commencer, on va utiliser la fonction filter ordinaire : ghci> filter (\x -> x < 4) [9,1,5,2,10,3] [1,2,3]

Ctait plutt simple. Maintenant, crons un prdicat qui, en plus de retourner True ou False , fournisse galement un registre de ce quil a fait. Bien sr, on va utiliser la monade Writer cet effet : keepSmall :: Int -> Writer [String] Bool

keepSmall x | x < 4 = do tell ["Keeping " ++ show x] return True | otherwise = do tell [show x ++ " is too large, throwing it away"] return False

Plutt que de seulement retourner un Bool , cette fonction retourne un Writer [String] Bool . Cest un prdicat monadique. a sonne classe, non ? Si le nombre est plus petit que 4 , on indique quon le garde et on return True . prsent, passons-la filterM avec une liste. Puisque le prdicat retourne une valeur Writer , la liste rsultante sera galement une valeur Writer . ghci> fst $ runWriter $ filterM keepSmall [9,1,5,2,10,3] [1,2,3]

En examinant le rsultat de la valeur Writer rsultante, on voit que tout se droule bien. Maintenant, affichons le registre pour voir ce quil sest pass : ghci> mapM_ putStrLn $ snd $ runWriter $ filterM keepSmall [9,1,5,2,10,3] 9 is too large, throwing it away Keeping 1 5 is too large, throwing it away Keeping 2 10 is too large, throwing it away Keeping 3

Gnial. Donc simplement en fournissant un prdicat monadique filterM , nous avons pu filtrer une liste en tirant profit du contexte monadique utilis. Une astuce Haskell trs cool est dutiliser filterM pour obtenir lensemble des parties dune liste (en imaginant la liste comme un ensemble pour le moment). Lensemble des parties dun ensemble est lensemble des sous-ensembles de cet ensemble. Si lon a un ensemble comme [1, 2, 3] , lensemble de ses parties contient les ensembles suivants : [1,2,3] [1,2] [1,3] [1] [2,3] [2] [3] []

En dautres termes, obtenir lensemble des parties dun ensemble consiste trouver toutes les combinaisons possibles de garder ou jeter les lments de cet ensemble. [2, 3] est comme lensemble original, mais on a exclu le nombre 1 . Pour crer une fonction renvoyant lensemble des parties dune liste, on va sappuyer sur le non dterminisme. On prend une liste [1, 2, 3] et on regarde son premier lment, qui est 1 , et on se demande : devrait-on le garder ou le jeter ? Eh bien, on aimerait faire les deux en ralit. On va donc filtrer une liste laide dun prdicat qui gardera et jettera chaque lment de faon non dterministe. Voici notre fonction des sous-parties dun ensemble : powerset :: [a] -> [[a]] powerset xs = filterM (\x -> [True, False]) xs

Quoi, cest tout ? Yup. On choisit de jeter et de garder chaque lment, peu importe lequel cest. Nous avons un prdicat non dterministe, donc la liste rsultante sera aussi une valeur non dterministe, et donc une liste de listes. Essayons : ghci> powerset [1,2,3] [[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]

Il faut un peu y rflchir pour comprendre ce qui se passe, mais si vous considrez simplement les listes comme des valeurs non dterministes qui ne savent pas ce quelles valent et choisissent donc de tout valoir la fois, cest un peu plus simple.

foldM
Lquivalent monadique de foldl est foldM . Si vous vous souvenez de vos plis de la section sur les plis, vous savez que foldl prend une fonction binaire, un accumulateur initial et une liste plier, et plie la liste en partant de la gauche avec la fonction binaire pour nen faire plus quune valeur. foldM fait la mme chose, mais prend une fonction qui produit une valeur monadique pour plier la liste. Sans surprise, la valeur rsultante est galement monadique. Le type de foldl est :

foldl :: (a -> b -> a) -> a -> [b] -> a

Alors que foldM a pour type : foldM :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a

La valeur que la fonction binaire retourne est monadique, et donc le rsultat du pli entier est aussi monadique. Sommons une liste de nombres laide dun pli : ghci> foldl (\acc x -> acc + x) 0 [2,8,3,1] 14

Laccumulateur initial est 0 , puis 2 est ajout laccumulateur, rsultant en une valeur de 2 . 8 est ensuite ajout, rsultant en un accumulateur valant 10 et ainsi de suite jusqu atteindre la fin, laccumulateur final tant le rsultat. Et si lon voulait sommer une liste de nombres avec la condition supplmentaire que si lun des nombres de la liste est plus grande que 9 , tout choue ? Il semble logique dutiliser une fonction binaire qui vrifie si le nombre ajouter est plus grand que 9 , choue le cas chant, et continue son petit bonhomme de chemin si ce nest pas le cas. cause de cette possibilit dchec additionnelle, faisons retourner notre fonction un Maybe accumulateur plutt quun accumulateur normal. Voici la fonction binaire : binSmalls :: Int -> Int -> Maybe Int binSmalls acc x | x > 9 = Nothing | otherwise = Just (acc + x)

Puisque notre fonction binaire est maintenant une fonction monadique, on ne peut plus utiliser le foldl normal, mais on doit utiliser foldM . Voici : ghci> foldM binSmalls 0 [2,8,3,1] Just 14 ghci> foldM binSmalls 0 [2,11,3,1] Nothing

Excellent ! Puisquun des nombres de la liste est plus grand que 9 , le tout rsulte en Nothing . Plier avec une fonction binaire qui retourne une valeur Writer est aussi cool parce que vous pouvez enregistrer ce que vous voulez pendant que le pli suit son chemin.

Crer une calculatrice NPI scurise


Quand nous rsolvions le problme de limplmentation dune calculatrice NPI, nous avions remarqu quelle fonctionnait bien tant que lentre tait bien forme. Mais sil se passait quelque chose de travers, notre programme entier plantait. prsent que lon sait prendre un code existant et le rendre monadique, reprenons notre calculatrice NPI et ajoutons-lui une gestion derreur en profitant de la monade Maybe . Nous avions implment notre calculatrice NPI en prenant une chane de caractres comme "1 3 + 2 *" , en la dcoupant en mots pour obtenir quelque chose comme ["1","3","+","2","*"] , puis en pliant cette liste en commenant avec une pile vide et en utilisant une fonction binaire de pli qui empile les nombres ou manipule ceux au sommet de la pile pour les ajouter ou les diviser, etc. Ceci tait le corps de notre fonction principale : import Data.List solveRPN :: String -> Double solveRPN = head . foldl foldingFunction [] . words

On transformait lexpression en une liste de chanes de caractres, quon pliait avec notre fonction de pli, et il ne nous restait plus quun lment dans la pile, quon retournait comme rponse. La fonction de pli tait : foldingFunction foldingFunction foldingFunction foldingFunction foldingFunction :: [Double] -> String -> [Double] (x:y:ys) "*" = (x * y):ys (x:y:ys) "+" = (x + y):ys (x:y:ys) "-" = (y - x):ys xs numberString = read numberString:xs

Laccumulateur du pli tait une pile, quon reprsentait comme une liste de valeurs Double . Tandis que la fonction de pli traversait lexpression en NPI, si

llment en cours tait un oprateur, elle prenait deux lments en haut de la pile, appliquait loprateur sur ceux-ci et replaait le rsultat sur la pile. Si llment en cours tait une chane reprsentant un nombre, elle convertissait cette chane en un vrai nombre et retournait une nouvelle pile comme lancienne, mais avec ce nombre empil. Rendons dabord notre fonction de pli capable dchouer gracieusement. Son type va changer de ce quil tait en : foldingFunction :: [Double] -> String -> Maybe [Double]

Ainsi, soit elle retournera Just une nouvelle pile, soit elle chouera avec la valeur Nothing . La fonction reads est comme read , seulement elle retourne une liste avec un unique lment en cas de lecture russie. Si elle choue lire quelque chose, alors elle retourne une liste vide. part retourner la valeur quelle a lue, elle retourne galement le morceau de chane de caractres quelle na pas consomm. On va dire quil faut quelle consomme lentre en entier pour que cela marche, et on va crer une fonction readMaybe pour notre convenance. La voici : readMaybe :: (Read a) => String -> Maybe a readMaybe st = case reads st of [(x,"")] -> Just x _ -> Nothing

Testons : ghci> readMaybe "1" :: Maybe Int Just 1 ghci> readMaybe "GO TO HELL" :: Maybe Int Nothing

Ok, a a lair de marcher. Donc, transformons notre fonction de pli en une fonction monadique pouvant chouer : foldingFunction foldingFunction foldingFunction foldingFunction foldingFunction :: [Double] -> String -> Maybe [Double] (x:y:ys) "*" = return ((x * y):ys) (x:y:ys) "+" = return ((x + y):ys) (x:y:ys) "-" = return ((y - x):ys) xs numberString = liftM (:xs) (readMaybe numberString)

Les trois premiers cas sont comme les anciens, sauf que la nouvelle pile est enveloppe dans un Just (on a utilis return pour cela, mais on aurait tout aussi bien pu crire Just ). Dans le dernier cas, on fait readMaybe numberString et on mappe ensuite (:xs) dessus. Donc, si la pile xs est [1.0, 2.0] et si readMaybe numberString rsulte en Just 3.0 , le rsultat est Just [3.0, 1.0, 2.0] . Si readMaybe numberString rsulte en Nothing , alors le rsultat est Nothing . Essayons la fonction de pli seule : ghci> foldingFunction Just [6.0] ghci> foldingFunction Just [-1.0] ghci> foldingFunction Nothing ghci> foldingFunction Just [1.0] ghci> foldingFunction Nothing [3,2] "*" [3,2] "-" [] "*" [] "1" [] "1 wawawawa"

a a lair de marcher ! Il est lheure dintroduire notre nouvelle fonction solveRPN amliore. Mesdames, mesdemoiselles et messieurs ! import Data.List solveRPN :: String -> Maybe Double solveRPN st = do [result] <- foldM foldingFunction [] (words st) return result

Tout comme avant, on prend une chane de caractres et on en fait une liste de mots. Puis, on fait un pli, dmarrant avec une pile vide, seulement au lieu dun foldl normal, on fait un foldM . Le rsultat de ce foldM doit tre une valeur Maybe contenant une liste (notre pile finale) et cette liste ne doit contenir quune valeur. On utilise une expression do pour obtenir cette valeur et on lappelle result . Si foldM retourne Nothing , le tout vaudra Nothing , parce que cest comme cela que Maybe fonctionne. Remarquez aussi quon filtre par motif dans lexpression do , donc si la liste a plus dune valeur, ou bien aucune, le filtrage par motif choue et Nothing est produit. la dernire ligne, on fait simplement return result pour prsenter le rsultat du calcul NPI comme le rsultat de la valeur Maybe . Mettons-l lessai :

ghci> solveRPN Just 6.0 ghci> solveRPN Just 30.0 ghci> solveRPN Nothing ghci> solveRPN Nothing

"1 2 * 4 +" "1 2 * 4 + 5 *" "1 2 * 4" "1 8 wharglbllargh"

Le premier chec est d au fait que la pile finale na pas un seul lment, donc le filtrage par motif choue dans lexpression do . Le deuxime chec a lieu parce que readMaybe retourne Nothing .

Composer des fonctions monadiques


Lorsque nous tudiions les lois des monades, nous avions dit que la fonction <=< tait comme la composition, mais quau lieu de travailler sur des fonctions ordinaires comme a -> b , elle travaillait sur des fonctions comme a -> m b . Par exemple : ghci> let f = (+1) . (*100) ghci> f 4 401 ghci> let g = (\x -> return (x+1)) <=< (\x -> return (x*100)) ghci> Just 4 >>= g Just 401

Dans cet exemple, on a dabord compos deux fonctions ordinaires, appliqu la fonction rsultante 4 , et ensuite on a compos deux fonctions monadiques, et donn Just 4 la fonction rsultante laide de >>= . Si lon a tout un tas de fonctions dans une liste, on peut les composer en une unique norme fonction en utilisant id comme accumulateur initial et . comme fonction binaire. Voici un exemple : ghci> let f = foldr (.) id [(+1),(*100),(+1)] ghci> f 1 201

La fonction f prend un nombre et lui ajoute 1 , multiplie le rsultat par 100 et ajoute 1 a. On peut composer des fonctions monadiques de la mme faon, seulement au lieu de la composition normale on utilise <=< et au lieu d id on utilise return . On na mme pas remplacer foldr par foldM puisque la fonction <=< soccupe de grer la composition de manire monadique. Quand on shabituait la monade des listes dans le chapitre prcdent, on lutilisait pour trouver si un cavalier pouvait aller dune position dun chiquier une autre en exactement trois mouvements. On avait une fonction nomme moveKnight qui prenait la position du cavalier sur lchiquier et retournait tous les dplacements possibles au prochain tour. Puis, pour gnrer toutes les positions possibles aprs trois mouvements, on a cr la fonction suivante : in3 start = return start >>= moveKnight >>= moveKnight >>= moveKnight

Et pour vrifier sil pouvait aller de start end en trois mouvements, on faisait : canReachIn3 :: KnightPos -> KnightPos -> Bool canReachIn3 start end = end `elem` in3 start

En utilisant la composition de fonctions monadiques, on peut crer une fonction comme in3 , seulement quau lieu de gnrer toutes les positions possibles du cavalier aprs trois mouvements, on peut le faire pour un nombre de mouvements arbitraires. Si vous regardez in3 , vous voyez quon utilise moveKnight trois fois, et chaque fois, on utilise >>= pour lui donner toutes les positions prcdentes. Rendons cela plus gnral prsent. Voici comment procder : import Data.List inMany :: Int -> KnightPos -> [KnightPos] inMany x start = return start >>= foldr (<=<) return (replicate x moveKnight)

Dabord, on utilise replicate pour crer une liste contenant x copies de la fonction moveKnight . Puis, on compose monadiquement toutes ces fonctions en une seule, ce qui nous donne une fonction qui prend une position de dpart, et dplace le cavalier x fois de faon non dterministe. Il suffit alors de placer la position initiale dans une liste singleton avec return et de la donner notre fonction. On peut maintenant changer la fonction canReachIn3 pour tre galement plus gnrale :

canReachIn :: Int -> KnightPos -> KnightPos -> Bool canReachIn x start end = end `elem` inMany x start

Crer des monades

Dans cette section, on va regarder comment un type est cr, identifi comme une monade, et instanci en Monad . Gnralement, on ne cre pas une monade pour le plaisir de crer une monade. Gnralement, on cre plutt un type dont le but est de modliser un aspect du problme rsoudre, et plus tard si lon saperoit que ce type reprsente une valeur dans un contexte et peut agir comme une monade, on lui donne une instance de Monad . Comme on la vu, les listes sont utilises pour reprsenter des valeurs non dterministes. Une liste comme [3, 5, 9] peut tre vue comme une unique valeur non dterministe qui ne peut tout simplement pas dcider de ce quelle veut tre. Quand on donne une liste une fonction avec >>= , cela fait juste tous les choix possibles pour appliquer la fonction sur un lment de la liste, et le rsultat est une liste prsentant tous ces rsultats. Si lon regarde la liste [3, 5, 9] comme les nombres 3 , 5 et 9 la fois, on peut remarquer quil ny a pas dinformation quand la probabilit de chacun deux. Et si lon voulait modliser une valeur non dterministe comme [3, 5, 9] , mais quon souhaitait exprimer que 3 a 50% de chances davoir lieu, alors que 5 et 9 nont chacun que 25% de chances ? Essayons dy arriver ! Mettons que chaque lment de la liste vienne avec une valeur supplmentaire, une probabilit. Il peut sembler logique de le prsenter ainsi : [(3,0.5),(5,0.25),(9,0.25)]

En mathmatiques, on nutilise gnralement pas des pourcentages pour exprimer les probabilits, mais plutt des nombres rels entre 0 et 1. Un 0 signifie que quelque chose na aucune chance au monde davoir lieu, et un 1 signifie quelle aura lieu coup sr. Les nombres virgule flottante peuvent rapidement devenir bordliques parce quils ont tendance perdre en prcision, ainsi Haskell propose un type de donnes pour les nombres rationnels qui ne perdent pas en prcision. Ce type sappelle Rational et vit dans Data.Ratio . Pour crer un Rational , on lcrit comme si ctait une fraction. Le numrateur et le dnominateur sont spars par un % . Voici quelques exemples : ghci> 1%4 1 % 4 ghci> 1%2 + 1%2 1 % 1 ghci> 1%3 + 5%4 19 % 12

La premire ligne est juste un quart. la deuxime ligne, on additionne deux moitis, ce qui nous donne un tout, et la troisime ligne on additionne un tiers et cinq quarts et on obtient dix-neuf douzimes. Jetons ces nombres virgule flottante et utilisons plutt des Rational pour nos probabilits : ghci> [(3,1%2),(5,1%4),(9,1%4)] [(3,1 % 2),(5,1 % 4),(9,1 % 4)]

Ok, donc 3 a une chance sur deux davoir lieu, alors que 5 et 9 arrivent une fois sur quatre. Plutt propre. Nous avons pris des listes, et ajouter un contexte supplmentaire, afin quelles reprsentent des valeurs avec des contextes. Avant daller plus loin, enveloppons cela dans un newtype parce que mon petit doigt me dit quon va bientt crer des instances. import Data.Ratio newtype Prob a = Prob { getProb :: [(a,Rational)] } deriving Show

Bien. Est-ce un foncteur ? Eh bien, la liste tant un foncteur, ceci devrait probablement tre galement un foncteur, parce quon a juste rajout quelque chose la liste. Lorsquon mappe une fonction sur une liste, on lapplique chaque lment. Ici, on va lappliquer chaque lment galement, et on laissera les probabilits comme elles taient. Crons une instance :

instance Functor Prob where fmap f (Prob xs) = Prob $ map (\(x,p) -> (f x,p)) xs

On sort la paire du newtype en filtrant par motif, on applique la fonction f aux valeurs en gardant les probabilits telles quelles, et on rencapsule le tout. Voyons si cela marche : ghci> fmap negate (Prob [(3,1%2),(5,1%4),(9,1%4)]) Prob {getProb = [(-3,1 % 2),(-5,1 % 4),(-9,1 % 4)]}

Autre chose noter, les probabilits devraient toujours avoir pour somme 1 . Si ce sont bien toutes les choses qui peuvent avoir lieu, il serait illogique que la somme de leur probabilit ne fasse pas 1 . Une pice qui a 75% de chances de faire pile et 50% de chances de faire face ne semble pouvoir marcher que dans un autre trange univers. Maintenant, la grande question, est-ce une monade ? tant donn que la liste est une monade, on dirait que a devrait aussi tre une monade. Pensons dabord return . Comment marche-t-elle sur les listes ? Elle prend une valeur, et la place dans une liste singleton. Quen est-il ici ? Eh bien, puisque cest cens tre un contexte minimal par dfaut, cela devrait aussi tre une liste singleton. Quen est-il de la probabilit ? Eh bien, return x est suppose crer une valeur monadique qui prsente toujours x en rsultat, il serait donc illogique que la probabilit soit 0 . Si elle doit toujours la prsenter en rsultat, la probabilit devrait tre 1 ! Quen est-il de >>= ? a semble un peu compliqu, profitons donc du fait que m >>= f est toujours gal join (fmap f m) pour les monades, et pensons plutt la faon dont on aplatirait une liste de probabilits de listes de probabilits. Comme exemple, considrons une liste o il y a exactement 25% de chances que 'a' ou 'b' ait lieu. a et b ont la mme probabilit. galement, il y a 75% de chances que c ou d ait lieu. 'c' et 'd' ont galement la mme probabilit. Voici une image de la liste de probabilits qui modlise ce scnario : Quelles sont les chances que chacune de ces lettres ait lieu ? Si lon devait redessiner ceci avec quatre botes, chacune avec une probabilit, quelles seraient ces probabilits ? Pour les trouver, il suffit de multiplier chaque probabilit par la probabilit quelle contient. 'a' aurait lieu une fois sur huit, et de mme pour 'b' , parce que si lon multiplie un demi par un quart, on obtient un huitime. 'c' aurait lieu trois fois sur huit parce que trois quarts multiplis par un demi donnent trois huitimes. 'd' aurait aussi lieu trois fois sur huit. Si lon somme toutes les probabilits, la somme vaut toujours un. Voici cette situation exprime comme une liste de probabilits : thisSituation :: Prob (Prob Char) thisSituation = Prob [( Prob [('a',1%2),('b',1%2)] , 1%4 ) ,( Prob [('c',1%2),('d',1%2)] , 3%4) ]

Remarquez que son type est Prob (Prob Char) . Maintenant quon a trouv comment aplatir une liste de probabilits imbrique, il nous suffit dcrire le code correspondant, et on peut alors crire >>= comme join (fmap f m) et avoir notre monade ! Voici flatten , quon nomme ainsi parce que le nom join est dj pris : flatten :: Prob (Prob a) -> Prob a flatten (Prob xs) = Prob $ concat $ map multAll xs where multAll (Prob innerxs,p) = map (\(x,r) -> (x,p*r)) innerxs

La fonction multAll prend un tuple form dune liste de probabilits et dune probabilit p et multiplie toutes les probabilits lintrieur de cette premire par p , retournant une liste de paires dlments et de probabilits. On mappe multAll sur chaque paire de notre liste de probabilits imbrique et on aplatit simplement la liste imbrique rsultante. On a prsent tout ce dont on a besoin pour crire une instance de Monad ! instance Monad Prob where return x = Prob [(x,1%1)] m >>= f = flatten (fmap f m) fail _ = Prob []

Puisquon a dj fait tout le travail, linstance est trs simple. On a aussi dfini la fonction fail , qui est la mme que pour les listes, donc en cas dchec dun filtrage par motif dans une expression do , un chec a lieu dans le contexte dune liste de

probabilits. Il est galement important de vrifier si les lois des monades tiennent pour linstance quon vient de crer. La premire dit que return x >>= f devrait tre gal f x . Une preuve rigoureuse serait fastidieuse, mais on peut voir que si lon place une valeur dans un contexte par dfaut avec return et quon fmap une fonction par dessus, puis quon aplatit la liste de probabilits rsultante, chaque probabilit rsultant de la fonction serait multiplie par la probabilit 1%1 cre par return , donc le contexte serait inaffect. Le raisonnement montrant que m >>= return est gal m est similaire. La troisime loi dit que f <=< (g <=< h) devrait tre gal (f <=< g) <=< h . Puisque cette loi tient pour la monade des listes, qui est la base de la monade des probabilits, et parce que la multiplication est associative, cette loi tient donc galement pour la monade des probabilits. 1%2 * (1%3 * 1%5) est gal (1%2 * 1%3) * 1%5 . prsent quon a une monade, que peut-on en faire ? Eh bien, elle peut nous aider faire des calculs avec des probabilits. On peut traiter des vnements probabilistes comme des valeurs dans des contextes, et la monade de probabilit sassurera que les probabilits sont bien refltes dans le rsultat final. Mettons quon ait deux pices normales et une pice pipe qui donne pile un nombre ahurissant de neuf fois sur dix, et face seulement une fois sur dix. Si lon jette les trois pices la fois, quelles sont les chances quelles atterrissent toutes sur pile ? Dabord, crons des valeurs de probabilits pour un lancer de pice normale et un lancer de pice pipe : data Coin = Heads | Tails deriving (Show, Eq) coin :: Prob Coin coin = Prob [(Heads,1%2),(Tails,1%2)] loadedCoin :: Prob Coin loadedCoin = Prob [(Heads,1%10),(Tails,9%10)]

Et finalement, le lancer de pices en action : import Data.List (all) flipThree :: Prob Bool flipThree = do a <- coin b <- coin c <- loadedCoin return (all (==Tails) [a,b,c])

En lessayant, on voit que les chances que les trois pices atterrissent sur pile ne sont pas trs bonnes, en dpit davoir trich avec notre pice pipe : ghci> getProb flipThree [(False,1 % 40),(False,9 % 40),(False,1 % 40),(False,9 % 40), (False,1 % 40),(False,9 % 40),(False,1 % 40),(True,9 % 40)]

Toutes les trois atterriront sur pile neuf fois sur quarante, ce qui fait moins de 25% de chances. On voit que notre monade ne sait pas joindre tous les rsultats False o les pices ne tombent pas toutes sur pile en un seul rsultat. Ce nest pas un gros problme, puisqucrire une fonction qui regroupe tous ses rsultats en un seul est plutt facile et est laiss comme exercice pour le lecteur (vous !). Dans cette section, on est parti dune question (et si les listes transportaient galement une information de probabilit ?) pour crer un type, reconnatre une monade et finalement crer une instance et faire quelque chose avec elle. Je trouve a plutt attrayant ! ce stade, on devrait avoir une assez bonne comprhension de ce que sont les monades.

Pour une poigne de monades

Table des matires

Zippeurs

Zippeurs
Et pour quelques monades de plus Table des matires Alors que la puret dHaskell amne tout un tas de bienfaits, elle nous oblige attaquer certains problmes sous un autre angle que celui quon aurait pris dans des langages impurs. cause de la transparence rfrentielle, une valeur est aussi bonne quune autre en Haskell si elles reprsentent toutes les deux la mme chose. Donc si on a un arbre plein de cinq (tapez-men cinq ?) et quon veut en changer un en un six, il nous faut un moyen de savoir exactement quel cinq dans notre arbre on veut changer. Il faut savoir o il est dans larbre. Dans les langages impurs, on aurait pu noter ladresse mmoire o est situ le cinq, et changer la valeur cette adresse. Mais en Haskell, un cinq est un cinq comme les autres, et on ne peut donc pas le discriminer en fonction de sa position en mmoire. On ne peut pas non plus vraiment changer quoi que ce soit, et quand on dit quon change un arbre, on veut en fait dire quon prend un arbre et quon en retourne un autre, similaire loriginal mais un peu diffrent. Une possibilit consiste se rappeler du chemin de la racine de larbre llment quon souhaite changer. On pourrait dire : prends cet arbre, va gauche, va droite, et encore gauche, et change llment qui est cet endroit. Bien que cela marche, a peut tre inefficace. Si lon veut plus tard changer un lment qui est juste ct de celui quon vient de changer, on doit traverser larbre nouveau depuis la racine jusqu cet lment ! Dans ce chapitre, nous allons voir quon peut prendre une structure de donnes et se focaliser sur une partie de celle-ci de faon ce que modifier ses lments soit facile, et ce que se balader autour soit efficace. Joli !

Une petite balade


Comme on la appris en cours de biologie, il y a beaucoup de sortes diffrentes darbres, alors choisissons une graine quon utilisera pour planter le ntre. La voici : data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show)

Notre arbre est donc ou bien vide, ou bien un nud qui a un lment et deux sous-arbres. Voici un bon exemple dun tel arbre, que je donne, vous lecteur, gratuitement ! freeTree :: Tree Char freeTree = Node 'P' (Node 'O' (Node 'L' (Node (Node ) (Node 'Y' (Node (Node ) ) (Node 'L' (Node 'W' (Node (Node ) (Node 'A' (Node (Node ) )

'N' Empty Empty) 'T' Empty Empty)

'S' Empty Empty) 'A' Empty Empty)

'C' Empty Empty) 'R' Empty Empty)

'A' Empty Empty) 'C' Empty Empty)

Et voici ce mme arbre reprsent graphiquement :

Remarquez-vous le W dans cet arbre ? Disons quon souhaite le changer en un P . Comment ferions-nous cela ? Eh bien, une faon de faire consisterait filtrer par motif sur notre arbre jusqu ce quon trouve llment situ en allant dabord droite puis gauche, et quon change cet lment. Voici le code faisant cela : changeToP :: Tree Char -> Tree Char changeToP (Node x l (Node y (Node _ m n) r)) = Node x l (Node y (Node 'P' m n) r)

Beurk ! Non seulement cest plutt laid, mais cest aussi un peu droutant. Quest-ce quil se passe ici ? Eh bien, on filtre par motif sur notre arbre et on appelle sa racine x (il devient le 'P' de la racine) et son sous-arbre gauche l . Plutt que de donner un nom au sous-arbre droit, on le filtre nouveau par motif. On continue ce filtrage jusqu atteindre le sous-arbre dont notre 'W' est la racine. Une fois ceci fait, on recre larbre, seulement le sous-arbre qui contenait le 'W' a maintenant pour racine 'P' . Y a-t-il un meilleur moyen de faire ceci ? Pourquoi ne pas crer une fonction qui prenne un arbre ainsi quune liste de directions ? Les directions seront soit L soit R , reprsentant respectivement la gauche et la droite, et on changera llment sur lequel on tombe en suivant ces directions. Voici : data Direction = L | R deriving (Show) type Directions = [Direction] changeToP changeToP changeToP changeToP :: Directions -> Tree Char (L:ds) (Node x l r) = Node (R:ds) (Node x l r) = Node [] (Node _ l r) = Node 'P' -> Tree Char x (changeToP ds l) r x l (changeToP ds r) l r

Si le premier lment de notre liste de directions est un L , on construit un nouvel arbre comme loriginal, mais son sous-arbre gauche a un lment modifi en 'P' . Quand on appelle rcursivement changeToP , on ne donne que la queue de la liste des directions, puisquon est dj all une fois gauche. On fait la mme chose dans le cas de R . Si la liste des directions est vide, cela signifie quon est arriv destination, on retourne donc un arbre qui est comme celui en entre, mais avec 'P' sa racine. Pour viter dafficher larbre entier, crons une fonction qui prend une liste de directions et nous dit quel est llment la destination : elemAt elemAt elemAt elemAt :: Directions -> Tree a -> a (L:ds) (Node _ l _) = elemAt ds l (R:ds) (Node _ _ r) = elemAt ds r [] (Node x _ _) = x

La fonction est en fait assez similaire changeToP , seulement au lieu de se souvenir des choses en chemin et de reconstruire larbre, elle ignore simplement tout sauf sa destination. Ici on change le 'W' en 'P' et on vrifie si le changement a bien eu lieu dans le nouvel arbre : ghci> let newTree = changeToP [R,L] freeTree ghci> elemAt [R,L] newTree 'P'

Bien, a a lair de marcher. Dans ces fonctions, la liste des directions agit comme une sorte de point focal, parce quil dsigne exactement un sous-arbre de notre arbre. Une liste de directions comme [R] se focalise sur le sous-arbre droit de la racine, par exemple. Une liste de directions vide se focalise sur larbre entier. Bien que cette technique ait lair cool, elle peut tre assez inefficace, notamment si lon veut changer des lments rptition. Disons quon ait un arbre norme et une trs longue liste de directions qui pointe vers un lment tout en bas de cet arbre. On utilise la liste de directions pour se balader dans larbre et changer

llment en bas. Si lon souhaite changer un autre lment proche de cet lment, on doit repartir de la racine et retraverser tout larbre depuis le dbut nouveau ! La barbe !

Une trane de miettes


Ok, donc pour se concentrer sur un sous-arbre, on veut quelque chose de mieux quune liste de directions quon suit toujours depuis la racine de notre arbre. Est-ce que cela aiderait si lon commenait la racine de larbre, et quon se dplaait vers la gauche ou la droite, une tape la fois, en laissant dune certaine faon des miettes de pain sur notre passage ? Cest--dire, quand on va gauche, on se souvient quon est all gauche, et quand on va droite, on se souvient quon est all droite. Bien, on peut essayer. Pour reprsenter nos miettes de pain, on va aussi utiliser une liste de Direction (soit L , soit R ), seulement au lieu de lappeler Directions , on lappellera Breadcrumbs , puisque nos directions seront prsent renverses puisquon jette les miettes au fur et mesure quon avance dans larbre : type Breadcrumbs = [Direction]

Voici une fonction qui prend un arbre et des miettes et se dplace dans le sous-arbre gauche tout en ajoutant L dans la liste qui reprsente nos miettes de pain : goLeft :: (Tree a, Breadcrumbs) -> (Tree a, Breadcrumbs) goLeft (Node _ l _, bs) = (l, L:bs)

On ignore llment la racine ainsi que le sous-arbre droit, et on retourne juste le sous-arbre gauche ainsi que lancienne trane de miettes, avec L plac sa tte. Voici une fonction pour aller droite : goRight :: (Tree a, Breadcrumbs) -> (Tree a, Breadcrumbs) goRight (Node _ _ r, bs) = (r, R:bs)

Elle fonctionne de faon similaire. Utilisons ces fonctions pour prendre notre freeTree et aller droite puis gauche : ghci> goLeft (goRight (freeTree, [])) (Node 'W' (Node 'C' Empty Empty) (Node 'R' Empty Empty),[L,R])

Ok, donc maintenant on a un arbre qui a 'W' sa racine, 'C' la racine de son sous-arbre gauche, et 'R' la racine de son sous-arbre droit. Les miettes de pain sont [L, R] parce quon est dabord all droite, puis gauche. Pour rendre plus clair le dplacement dans notre arbre, on peut rutiliser la fonction -: quon avait dfinie ainsi : x -: f = f x

Ce qui nous permet dappliquer des fonctions des valeurs en crivant dabord la valeur, puis -: et enfin la fonction. Ainsi, au lieu dcrire goRight (freeTree, []) , on peut crire (freeTree, []) -: goRight . En utilisant ceci, on peut rcrire le code ci-dessus de faon faire mieux apparatre quon va en premier droite, et ensuite gauche : ghci> (freeTree, []) -: goRight -: goLeft (Node 'W' (Node 'C' Empty Empty) (Node 'R' Empty Empty),[L,R])

Retourner en haut
Et si lon veut remonter dans larbre prsent ? Grce notre trane de miettes, on sait que notre arbre actuel est le sous-arbre gauche de son parent, et que celui-ci est le sous-arbre droit de son parent, mais cest tout. Elles ne nous en disent pas assez sur le parent du sous-arbre pour quon puisse remonter dans larbre. Il semblerait quen plus de la direction prise, notre miette de pain devrait aussi contenir toutes les donnes ncessaires pour remonter dans larbre. Dans ce cas, il nous faudrait llment de larbre parent ainsi que le sous-arbre droit. En gnal, une miette de pain doit contenir toutes les donnes ncessaires pour reconstruire son nud parent. Elle doit donc contenir toute linformation sur les chemins que lon na pas suivis, ainsi que la direction que lon a prise, mais elle na pas besoin de contenir le sous-arbre sur lequel on se focalise, puisquon a dj ce sous-arbre dans la premire composante de notre tuple, donc sil tait aussi dans la miette, il y aurait une duplication de linformation. Modifions notre miette de pain afin quelle contienne aussi linformation sur tout ce quon a ignor prcdemment quand on a choisi daller gauche ou droite. Au lieu de Direction , dfinissons un nouveau type de donnes :

data Crumb a = LeftCrumb a (Tree a) | RightCrumb a (Tree a) deriving (Show)

Maintenant, au lieu davoir juste un L , on a un LeftCrumb qui contient galement llment dans le nud parent, ainsi que son sous-arbre droit quon a choisi de ne pas visiter. Au lieu de R , on a un RightCrumb , qui contient aussi llment du nud parent et son sous-arbre gauche quon na pas visit. Ces miettes de pain contiennent prsent toutes les donnes ncessaires pour recrer larbre quon a parcouru. Ainsi, au lieu dtre juste des miettes de pain normales, ce sont plutt des petits disques durs quon parsme en chemin, parce quils contiennent beaucoup plus dinformations que seulement la direction prise. En gros, chaque miette de pain est maintenant comme un nud darbre mais avec un trou dun ct. Lorsquon se dplace en profondeur dans larbre, la miette transporte toute linformation du nud duquel on est parti, sauf le sous-arbre sur lequel on se focalise. Elle doit aussi savoir de quel ct se trouve le trou. Dans le cas dun LeftCrumb , on sait quon est all gauche, donc le sous-arbre manquant est le sous-arbre gauche. Changeons galement notre synonyme de type Breadcrumbs pour reflter le changement : type Breadcrumbs a = [Crumb a]

prsent, on doit modifier les fonctions goLeft et goRight afin quelles stockent linformation sur les chemins que lon na pas pris dans nos miettes, au lieu de tout ignorer comme elles le faisaient auparavant. Voici goLeft : goLeft :: (Tree a, Breadcrumbs a) -> (Tree a, Breadcrumbs a) goLeft (Node x l r, bs) = (l, LeftCrumb x r:bs)

Vous pouvez voir que a ressemble beaucoup notre ancienne goLeft , seulement au lieu dajouter seulement un L en tte de notre liste de miettes, on ajoute un LeftCrumb qui indique quon est all gauche, et on munit ce LeftCrumb de llment du nud quon quitte (le x ) et de son sous-arbre droit quon a choisi de ne pas visiter. Remarquez que cette fonction suppose que larbre sur lequel on se focalise actuellement nest pas Empty . Un arbre vide na pas de sous-arbres, donc si lon essaie daller gauche dans un arbre vide, une erreur aura lieu parce que le filtrage par motif sur Node chouera, et parce quon na pas mis de motif pour Empty . goRight est similaire : goRight :: (Tree a, Breadcrumbs a) -> (Tree a, Breadcrumbs a) goRight (Node x l r, bs) = (r, RightCrumb x l:bs)

Prcdemment, nous tions capable daller gauche ou droite. Ce quon a gagn, cest la possibilit de rebrousser chemin, en se souvenant de ce quoi ressemblaient les nuds parents et les chemins quon na pas visits. Voici la fonction goUp : goUp :: (Tree a, Breadcrumbs a) -> (Tree a, Breadcrumbs a) goUp (t, LeftCrumb x r:bs) = (Node x t r, bs) goUp (t, RightCrumb x l:bs) = (Node x l t, bs)

On se focalise sur larbre t , et on vrifie ce qutait le dernier Crumb . Si ctait un LeftCrumb , alors on construit un nouvel arbre o notre arbre t est le sous-arbre gauche, et on utilise linformation propos du sous-arbre droit quon na pas visit et de llment pour reconstruire le reste du Node . Puisquon a en quelque sorte rebrouss chemin et ramass la dernire miette pour rcrer le nud parent, la nouvelle liste de miettes ne contient pas cette miette. Remarquez que cette fonction cause une erreur si lon est dj la racine dun arbre et quon essaie de remonter. Plus tard, on utilisera la monade Maybe pour reprsenter un chec possible lorsquon change de point focal. Avec une paire de Tree a et de Breadcrumbs a , on a toute linformation ncessaire pour reconstruire un arbre entier, tout en tant focalis sur un de ses sous-arbres. Ce mcanisme nous permet de facilement nous dplacer gauche, droite, et vers le haut. Une telle paire contenant une partie focalise dune structure de donnes, et ses alentours, est appele un zippeur, parce que changer de point focal vers le haut et vers le bas fait

penser une braguette qui monte et descend. Il est donc pratique de crer un synonyme de types : type Zipper a = (Tree a, Breadcrumbs a)

Je prfrerais appeler le synonyme de types Focus pour indiquer clairement quon se focalise sur une partie de la structure de donnes, mais le terme zippeur est globalement accept pour dcrire un tel mcanisme, on sen tiendra donc Zipper .

Manipuler des arbres sous focalisation


prsent quon sait se dplacer de haut en bas, crons une fonction qui modifie llment la racine de larbre sur lequel on est actuellement focalis : modify :: (a -> a) -> Zipper a -> Zipper a modify f (Node x l r, bs) = (Node (f x) l r, bs) modify f (Empty, bs) = (Empty, bs)

Si lon se focalise sur un nud, on modifie son lment racine avec la fonction f . Si on se focalise sur un arbre vide, on ne fait rien. Dsormais, on peut dmarrer avec un arbre, se dplacer o lon veut, et modifier llment cet endroit, tout en restant focalis sur cet lment de faon pouvoir facilement se dplacer de haut en bas par la suite. Un exemple : ghci> let newFocus = modify (\_ -> 'P') (goRight (goLeft (freeTree,[])))

On va gauche, puis droite, et on modifie llment racine en le remplaant par un 'P' . Cest encore plus lisible avec -: : ghci> let newFocus = (freeTree,[]) -: goLeft -: goRight -: modify (\_ -> 'P')

On peut ensuite remonter si lon veut, et remplacer un lment par un mystrieux 'X' : ghci> let newFocus2 = modify (\_ -> 'X') (goUp newFocus)

Ou avec -: : ghci> let newFocus2 = newFocus -: goUp -: modify (\_ -> 'X')

Remonter est facile puisque les miettes quon a laisses forment la structure de donnes que lon na pas visite, seulement tout est renvers, un peu comme une chaussette lenvers. Cest pourquoi, lorsquon veut remonter dun cran, on na pas besoin de repartir de la racine et de redescendre presque tout en bas, il nous suffit de prendre le sommet de notre arbre invers, en le remettant dans le bon sens et sous notre focalisation. Chaque nud a deux sous-arbres, mme si ces sous-arbres sont vides. Ainsi, si lon se focalise sur un arbre vide, on peut le remplacer par un sous-arbre non vide, en lui attachant un arbre comme feuille. Le code pour faire cela est simple : attach :: Tree a -> Zipper a -> Zipper a attach t (_, bs) = (t, bs)

On prend un arbre et un zippeur, et on retourne un nouveau zippeur, dont le point focal est remplac par larbre en question. Non seulement peut-on tendre des arbres ainsi, en remplaant des sous-arbres vides par dautres arbres, mais on peut aussi compltement remplacer des sous-arbres existants. Attachons un arbre tout gauche de notre freeTree : ghci> let farLeft = (freeTree,[]) -: goLeft -: goLeft -: goLeft -: goLeft ghci> let newFocus = farLeft -: attach (Node 'Z' Empty Empty)

newFocus est prsent focalis sur larbre quon vient juste dattacher, et le reste de larbre est stock renvers dans les miettes de pain. Si lon utilisait goUp pour remonter jusqu la racine de larbre, il serait exactement comme freeTree , mais avec un 'Z' en plus tout gauche.

Im going straight to the top, oh yeah, up where the air is fresh and clean!
Crer une fonction qui remonte tout en haut de notre arbre, peu importe lendroit sur lequel on sest focalis, est assez facile. La voil : topMost :: Zipper a -> Zipper a topMost (t,[]) = (t,[]) topMost z = topMost (goUp z)

Si notre trane de miettes est vide, cela signifie quon est arriv la racine de larbre, on retourne donc le point focal actuel. Sinon, on remonte dun cran pour obtenir le point focal du nud parent, et on appelle rcursivement topMost sur ce point focal. prsent, on peut se balader dans notre arbre, aller gauche, droite, en haut, appliquer modify et attach tout en se dplaant, et quand on a fini de faire nos modifications, on peut utiliser topMost pour nous focaliser nouveau sur la racine de larbre, et voir tous les changements effectus depuis cette meilleure perspective.

Se focaliser sur des listes


Les zippeurs peuvent tre utiliss avec quasiment toutes les structures de donnes, il nest donc pas surprenant quon puisse les utiliser pour se focaliser sur les sous-listes dune liste. Aprs tout, les listes sont un peu comme des arbres, seulement l o le nud dun arbre a un lment (ou pas) et plusieurs sous-arbres, un nud dune liste na quun lment et quune sous-liste. Quand on avait implment nos propres listes, on avait dfini le type de donnes ainsi : data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord)

Contrastez ceci avec la dfinition de notre arbre binaire, et vous verrez facilement que les listes peuvent tre considres comme des arbres pour lesquelles chaque nud na quun sous-arbre. Une liste comme [1, 2, 3] peut tre crite 1:2:3:[] . Elle consiste en une tte de liste, qui est 1 , et une queue, qui est 2:3:[] . son tour, 2:3:[] a aussi une tte, qui est 2 , et une queue, qui est 3:[] . Avec 3:[] , la tte est 3 et la queue est la liste vide [] . Crons un zippeur de listes. Pour changer le point focal dune sous-liste dune liste, on peut aller soit en avant, soit en arrire (alors que pour les arbres, on pouvait aller en haut, gauche ou droite). La partie focalise sera une sous-liste, avec une trane de miettes quon aura laisse en avanant. En quoi consisterait donc une miette de liste ? Quand on travaillait avec des arbres binaires, on disait quune miette devait contenir llment racine du nud parent, ainsi que tous les sous-arbres quon navait pas visits. Elle devait aussi se souvenir si lon tait all gauche ou droite. Ainsi, elle doit contenir toute linformation du nud parent, lexception du sous-arbre quon choisit de visiter. Les listes sont plus simples que des arbres, donc on na pas besoin de se souvenir si lon est all gauche ou droite, parce quil ny a quun chemin pour aller plus profondment dans la liste. Et puisquil ny a quun sous-arbre par nud, on na pas besoin de se souvenir de chemins quon naurait pas pris. Il semblerait que tout ce dont on ait besoin de se souvenir est llment prcdent. Si lon a une liste comme [3, 4, 5] , et quon sait que llment prcdent tait 2 , on peut rebrousser chemin en plaant simplement cet lment en tte de liste, obtenant [2, 3, 4, 5] . Puisquune miette de pain est juste un lment dans ce cas, on na pas vraiment besoin de la placer dans un type de donnes, comme on lavait fait avec Crumb pour notre zippeur darbres : type ListZipper a = ([a],[a])

La premire liste reprsente la liste sur laquelle on se focalise, et la seconde est la liste des miettes. Crons des fonctions pour avancer et reculer dans des listes : goForward :: ListZipper a -> ListZipper a goForward (x:xs, bs) = (xs, x:bs) goBack :: ListZipper a -> ListZipper a goBack (xs, b:bs) = (b:xs, bs)

Quand on avance, on se focalise sur la queue de la liste actuelle et on laisse la tte comme miette. Quand on rebrousse chemin, on prend la dernire miette, et on la replace en tte de liste. Voici les deux fonctions en action : ghci> let xs = [1,2,3,4] ghci> goForward (xs,[]) ([2,3,4],[1]) ghci> goForward ([2,3,4],[1]) ([3,4],[2,1]) ghci> goForward ([3,4],[2,1]) ([4],[3,2,1]) ghci> goBack ([4],[3,2,1]) ([3,4],[2,1])

On voit que les miettes de pain dans le cas des listes ne sont rien de plus quune partie de la liste renverse. Llment dont on part devient la tte des miettes, il

est donc facile de revenir en arrire en reprenant la tte des miettes et en en faisant la tte de notre point focal. On voit galement mieux pourquoi on appelle a un zippeur, parce que cela ressemble vraiment une braguette montant et descendant. Si vous tiez en train de crer un diteur de texte, vous pourriez utiliser une liste de chanes de caractres pour reprsenter les lignes du fichier de texte ouvert, et utiliser un zippeur pour savoir quelle ligne le curseur se trouve actuellement. En utilisant un zippeur, il serait galement facile dinsrer de nouvelles lignes nimporte o dans le texte ou deffacer des lignes existantes.

Un systme de fichiers lmentaire


Maintenant quon sait comment les zippeurs fonctionnent, utilisons des arbres pour reprsenter un systme de fichiers trs simple, et crons un zippeur pour ce systme de fichiers, qui nous permettra de nous dplacer travers les dossiers, comme on le fait gnralement quand on parcourt un systme de fichiers. Une vue simpliste du systme de fichiers usuel consiste le voir comme compos principalement de fichiers et de dossiers. Les fichiers sont des units de donnes qui ont un nom, alors que les dossiers sont utiliss pour organiser ces fichiers et peuvent contenir dautres dossiers. Disons donc quun lment dun systme de fichiers est soit un fichier, avec son nom et ses donnes, soit un dossier, qui a un nom et tout un tas dautres lments pouvant tre eux-mmes des fichiers ou des dossiers. Voici le type de donnes quon utilisera et quelques synonymes de types pour savoir qui est quoi : type Name = String type Data = String data FSItem = File Name Data | Folder Name [FSItem] deriving (Show)

Un fichier vient avec deux chanes de caractres, qui reprsentent son nom et les donnes quil contient. Un dossier vient avec une chane de caractres, qui est son nom, et une liste dlments. Si cette liste est vide, on a un dossier vide. Voici un dossier avec des fichiers et des sous-dossiers : myDisk :: FSItem myDisk = Folder "root" [ File "goat_yelling_like_man.wmv" "baaaaaa" , File "pope_time.avi" "god bless" , Folder "pics" [ File "ape_throwing_up.jpg" "bleargh" , File "watermelon_smash.gif" "smash!!" , File "skull_man(scary).bmp" "Yikes!" ] , File "dijon_poupon.doc" "best mustard" , Folder "programs" [ File "fartwizard.exe" "10gotofart" , File "owl_bandit.dmg" "mov eax, h00t" , File "not_a_virus.exe" "really not a virus" , Folder "source code" [ File "best_hs_prog.hs" "main = print (fix error)" , File "random.hs" "main = print 4" ] ] ]

Cest exactement ce que contient mon disque dur linstant.

Un zippeur pour notre systme de fichiers


Maintenant quon a un systme de fichiers, tout ce dont on a besoin est dun zippeur afin de zippeur et zoomer dans celui-ci, et dajouter, modifier ou supprimer des fichiers ou des dossiers. Tout comme avec les arbres binaires et les listes, on va semer des miettes qui contiennent linformation ncessaire concernant les choses quon na pas choisi de visiter. Comme on la dit, une miette doit contenir tout, lexception du sous-arbre sur lequel on se focalise. Elle doit aussi savoir o se situe le trou, afin de pouvoir replacer notre point focal au bon endroit quand on dcidera de rebrousser chemin. Dans ce cas, une miette de pain doit tre un dossier dans lequel il manque le dossier quon a choisi dexplorer. Et pourquoi pas un fichier plutt ? Eh bien, parce quune fois quon se focalise sur un fichier, on ne peut pas descendre plus profond dans le systme de fichiers, donc une miette qui dirait quon vient dun fichier ne serait pas logique. Un fichier est en quelque sorte un arbre vide. Si lon se focalise sur le dossier "root" , puis quon choisit de se focaliser sur le fichier "dijon_poupon.doc" , quoi devrait ressembler la miette quon laissera ? Eh bien, elle devrait contenir le nom du dossier parent ainsi que les lments qui venaient avant le fichier sur lequel on se focalise et les lments qui viennent aprs. Tout ce dont on a besoin est donc dun Name et de deux listes dlments. En gardant spare la liste des lments venant avant et celle des lments venant aprs, on sait exactement o placer llment sur lequel on tait focalis quand on retourne en arrire. Ainsi,

on sait o se trouve le trou. Voici notre type miette pour le systme de fichiers : data FSCrumb = FSCrumb Name [FSItem] [FSItem] deriving (Show)

Et voici un synonyme de type pour notre zippeur : type FSZipper = (FSItem, [FSCrumb])

Remonter dun cran dans la hirarchie est trs simple. On prend simplement la dernire miette et on assemble le nouveau point focal partir du prcdent et de la miette. Ainsi : fsUp :: FSZipper -> FSZipper fsUp (item, FSCrumb name ls rs:bs) = (Folder name (ls ++ [item] ++ rs), bs)

Puisque notre miette savait le nom du dossier parent, ainsi que les lments qui venaient avant llment sur lequel on stait focalis (cest le ls ) ainsi que ceux venant aprs (cest le rs ), remonter dun cran tait simple. Quen est-il davancer plus profondment dans le systme de fichiers ? Si nous sommes dans "root" et quon veut se focaliser sur "dijon_poupon.doc" , la miette quon laissera devra inclure le nom "root" ainsi que les lments prcdant "dijon_poupon.doc" et ceux qui viennent aprs. Voici une fonction qui, tant donn un nom, se focalise sur un fichier ou un dossier situ dans le dossier courant : import Data.List (break) fsTo :: Name -> FSZipper -> FSZipper fsTo name (Folder folderName items, bs) = let (ls, item:rs) = break (nameIs name) items in (item, FSCrumb folderName ls rs:bs) nameIs :: Name -> FSItem -> Bool nameIs name (Folder folderName _) = name == folderName nameIs name (File fileName _) = name == fileName

fsTo prend un Name et un FSZipper et retourne un nouveau FSZipper qui se focalise sur le fichier ou dossier portant ce nom. Ce fichier doit tre dans le dossier courant. Cette fonction ne va pas le chercher dans tous les endroits possibles, elle regarde seulement dans le dossier courant. Tout dabord, on utilise break pour dtruire la liste des lments dun dossier en ceux qui prcdent le fichier quon cherche et ceux qui le suivent. Si vous vous en souvenez, break prend un prdicat et une liste et retourne une paire de listes. La premire liste de la paire contient les premiers lments pour lesquels le prdicat a retourn False . Puis, ds que le prdicat retourne True pour un lement, elle place cet lment et tout le reste de la liste dans la deuxime composante de la paire. On a cr une fonction auxiliaire nomme nameIs qui prend un nom et un lment dun systme de fichiers et retourne True si le nom correspond. prsent, ls est une liste contenant les lments qui prcdent llment quon cherchait, item est llment en question, et rs est la liste des lments venant aprs lui dans ce dossier. Muni de ceci, il ne nous reste plus qu prsenter llment trouv par break comme le point focal et construire la miette approprie. Remarquez que si le nom quon cherche ne se trouve pas dans le dossier, le motif item:rs essaiera de filtrer une liste vide, et on aura une erreur. Si notre point focal nest pas un dossier mais un fichier, on aura une erreur et le programme plantera galement. On peut maintenant se dplacer de haut en bas dans notre systme de fichiers. Partons de la racine et dirigeons-nous vers le fichier "skull_man(scary).bmp" : ghci> let newFocus = (myDisk,[]) -: fsTo "pics" -: fsTo "skull_man(scary).bmp"

newFocus est prsent un zippeur qui est focalis sur le fichier "skull_man(scary).bmp" . Rcuprons le premier lment du zippeur (le point focal lui-mme) et vrifions si cest bien vrai : ghci> fst newFocus File "skull_man(scary).bmp" "Yikes!"

Remontons dun cran pour nous focaliser sur son voisin "watermelon_smash.gif" :

ghci> let newFocus2 = newFocus -: fsUp -: fsTo "watermelon_smash.gif" ghci> fst newFocus2 File "watermelon_smash.gif" "smash!!"

Manipuler notre systme de fichiers


prsent quon sait naviguer dans notre systme de fichiers, le manipuler est trs simple. Voici une fonction qui renomme le fichier ou dossier courant : fsRename :: Name -> FSZipper -> FSZipper fsRename newName (Folder name items, bs) = (Folder newName items, bs) fsRename newName (File name dat, bs) = (File newName dat, bs)

On peut maintenant renommer "pics" en "cspi" : ghci> let newFocus = (myDisk,[]) -: fsTo "pics" -: fsRename "cspi" -: fsUp

On est descendu jusquau dossier "pics" , on la renomm, et on est remont. Pourquoi pas une fonction qui prend un nouvel lment, et lajoute au dossier courant ? Admirez : fsNewFile :: FSItem -> FSZipper -> FSZipper fsNewFile item (Folder folderName items, bs) = (Folder folderName (item:items), bs)

Ctait du gteau. Remarquez que ceci planterait si lon essayait dajouter un lment alors que notre point focal tait un fichier plutt quun dossier. Ajoutons un fichier au dossier "pics" et retournons ensuite la racine : ghci> let newFocus = (myDisk,[]) -: fsTo "pics" -: fsNewFile (File "heh.jpg" "lol") -: fsUp

Ce qui est vraiment cool, cest que lorsquon modifie notre systme de fichiers, on ne le modifie pas vraiment en lieu et place, mais on retourne plutt un nouveau systme de fichiers. Ainsi, on a la fois accs lancien systme de fichiers (ici, myDisk ) et au nouveau (la premire composante de newFocus ). Ainsi, en utilisant des zippeurs, on obtient du versionnage sans effort, ce qui signifie quon peut toujours se rfrer aux anciennes versions de nos structures de donnes mme aprs les avoir modifies, pour ainsi dire. Ce nest pas unique aux zippeurs, cest simplement une proprit dHaskell de au fait que ses structures de donnes sont immuables. Cependant, avec les zippeurs, on a la capacit de se dplacer efficacement dans des structures de donnes, et cest l que la persistance des structures de donnes dHaskell commence dvoiler son potentiel.

Attention la marche
Jusquici, lorsquon traversait nos structures de donnes, quelles soient des arbres binaires, des listes ou des systmes de fichiers, on ne se proccupait pas trop davancer un pas trop loin et de tomber dans le vide. Par exemple, notre fonction goLeft prend un zippeur et un arbre binaire et dplace le point focal vers son sous-arbre gauche : goLeft :: Zipper a -> Zipper a goLeft (Node x l r, bs) = (l, LeftCrumb x r:bs)

Et si larbre duquel on avanait tait vide ? Cest--dire, et si ce ntait pas un Node , mais un Empty ? Dans ce cas, on obtiendrait une erreur lexcution parce que le filtrage par motif chouerait et parce quon navait pas mis de motif grant le cas dun arbre vide, qui ne contient pas de sous-arbres. Jusquici, on a seulement suppos que lon nessaierait jamais de nous focaliser sur le sous-arbre gauche dun arbre vide, puisque celui-ci nexiste pas. Mais, aller dans le sous-arbre gauche dun arbre vide est illogique, et jusqualors, on a ignor ce fait paresseusement. Et si lon tait la racine dun arbre, sans miettes, et quon essayait de rebrousser chemin ? La mme chose aurait lieu. Il semblerait que quand on utilise les zippeurs, chaque pas pourrait tre notre dernier pas (une musique menaante se fait entendre). En dautres termes, tout mouvement peut russir, mais peut aussi chouer. Cela ne vous rappelle rien ? Bien sr, les monades ! Plus spcifiquement, la monade Maybe qui ajoute un contexte dchec potentiel nos valeurs. Utilisons donc la monade Maybe pour ajouter un contexte possible dchec nos mouvements. On va prendre les fonctions qui marchent sur notre zippeur darbres binaires et les rendre monadiques. Dabord, occupons-nous des checs possibles de goLeft et goRight . Jusquici, lchec de fonctions pouvant chouer tait toujours reflt dans leur rsultat, il en va de mme ici. Voici goLeft et goRight pouvant chouer :

goLeft :: Zipper a -> Maybe (Zipper a) goLeft (Node x l r, bs) = Just (l, LeftCrumb x r:bs) goLeft (Empty, _) = Nothing goRight :: Zipper a -> Maybe (Zipper a) goRight (Node x l r, bs) = Just (r, RightCrumb x l:bs) goRight (Empty, _) = Nothing

Cool, maintenant, si lon essaie davancer vers la gauche dun arbre vide, on obtient Nothing ! ghci> goLeft (Empty, []) Nothing ghci> goLeft (Node 'A' Empty Empty, []) Just (Empty,[LeftCrumb 'A' Empty])

a a lair bon ! Et pour rebrousser chemin ? Le problme avait lieu lorsquon essayait de remonter alors quon navait plus de miettes, ce qui signifiait quon tait dj la racine de notre arbre. Voici la fonction goUp qui lance une erreur si lon ne reste pas dans les limites de notre arbre : goUp :: Zipper a -> Zipper a goUp (t, LeftCrumb x r:bs) = (Node x t r, bs) goUp (t, RightCrumb x l:bs) = (Node x l t, bs)

Modifions-l pour chouer gracieusement : goUp goUp goUp goUp :: Zipper a -> Maybe (Zipper a) (t, LeftCrumb x r:bs) = Just (Node x t r, bs) (t, RightCrumb x l:bs) = Just (Node x l t, bs) (_, []) = Nothing

Si lon a des miettes, tout va bien et on retourne un nouveau point focal russi, mais si ce nest pas le cas, on retourne un chec. Auparavant, ces fonctions prenaient des zippeurs et retournaient des zippeurs, ce qui signifiait quon pouvait les chaner ainsi pour se balader : gchi> let newFocus = (freeTree,[]) -: goLeft -: goRight

Mais prsent, au lieu de retourner un Zipper a , elles retournent un Maybe (Zipper a) , et chaner les fonctions ainsi ne fonctionnera plus. On avait eu un problme similaire quand on soccupait de notre funambule dans le chapitre sur les monades. Lui aussi avanait un pas la fois et chaque pas pouvait chouer parce quun tas doiseau pouvait atterrir sur un ct de sa perche dquilibre et le faire tomber. Maintenant, nous sommes larroseur arros, parce que cest nous qui essayons de nous dplacer, et on traverse le labyrinthe quon a nous-mme cr. Heureusement, on peut apprendre de lexprience du funambule et faire ce quil a fait, cest--dire changer lapplication de fonctions normale contre >>= , qui prend une valeur dans un contexte (dans notre cas, un Maybe (Zipper a) , qui a un contexte dchec potentiel) et la donne une fonction en sassurant que le contexte est bien gr. Ainsi, tout comme avec notre funambule, on va changer nos -: contre des >>= . Trs bien, on peut nouveau chaner nos fonctions ! Observez : ghci> let coolTree = Node 1 Empty (Node 3 Empty Empty) ghci> return (coolTree,[]) >>= goRight Just (Node 3 Empty Empty,[RightCrumb 1 Empty]) ghci> return (coolTree,[]) >>= goRight >>= goRight Just (Empty,[RightCrumb 3 Empty,RightCrumb 1 Empty]) ghci> return (coolTree,[]) >>= goRight >>= goRight >>= goRight Nothing

On a utilis return pour placer un zippeur dans un Just , puis utilis >>= pour donner cela notre fonction goRight . Dabord, on cre un arbre qui a un sousarbre gauche vide, et un sous-arbre droit avec deux sous-arbres. Quand on essaie daller droite une fois, cest un succs, parce que lopration est logique. Aller deux fois droite est aussi correct, on se retrouve avec un sous-arbre vide en point focal. Mais aller droite trois fois na pas de sens, parce quon ne peut pas aller droite dans un sous-arbre vide, cest pourquoi le rsultat est Nothing . On a dsormais muni nos arbres dun filet de scurit qui nous attraperait si lon devait tomber. Ouah, cette mtaphore tait parfaite. Notre systme de fichiers a aussi beaucoup de cas pouvant chouer, comme lorsque lon essaie de se focaliser sur un fichier inexistant. Comme exercice, vous pouvez munir notre systme de fichiers de fonctions qui chouent gracieusement en utilisant la monade Maybe .

Et pour quelques monades de plus

Table des matires

Vous aimerez peut-être aussi