• Aucun résultat trouvé

Jusqu’ici, nous avions toujours chargé nos fonctions dans GHCi pour les tester et jouer avec elles. On a aussi exploré les fonctions de la bibliothèque standard de cette façon. Mais à présent, après environ huit chapitres, on va finalement écrire notre premier vrai programme Haskell ! Yay ! Et pour sûr, on va se faire ce bon vieux

"hello, world" "hello, world" .

Hey ! Pour ce chapitre, je vais supposer que vous disposez d’un environnement à la Unix pour apprendre

Haskell. Si vous êtes sous Windows, je suggère d’utiliser Cygwin, un environnement à la Linux pour Windows, autrement dit, juste ce qu’il vous faut.

Pour commencer, tapez ceci dans votre éditeur de texte favori :

main = putStrLn "hello, world"

On vient juste de définir un nom mainmain , qui consiste à appeler putStrLnputStrLn avec le paramètre "hello, world""hello, world" . Ça semble plutôt banal, mais ça ne l’est pas, comme on va le voir bientôt. Sauvegardez ce fichier sous helloworld.hs .helloworld.hs

Et maintenant, on va faire quelque chose sans précédent. On va réellement compiler notre programme ! Je suis tellement ému ! Ouvrez votre terminal et naviguez jusqu’au répertoire où helloworld.hshelloworld.hs est placé et faites :

$ ghc --make helloworld

[1 of 1] Compiling Main ( helloworld.hs, helloworld.o ) Linking helloworld ...

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

hello, world

Et voilà, notre premier programme compilé qui affichait quelque chose dans le terminal. Comme c’est extraordinaire(ment ennuyeux) ! Examinons ce qu’on vient d’écrire. D’abord, regardons le type de la fonction putStrLn .putStrLn

ghci> :t putStrLn

putStrLn :: String -> IO ()

ghci> :t putStrLn "hello, world"

putStrLn "hello, world" :: IO ()

On peut lire le type de putStrLn ainsi : putStrLnputStrLn putStrLn prend une chaîne de caractères et retourne une action I/O qui a pour type de retour ()() (c’est-à-dire le tuple vide, aussi connu comme unit). Une action I/O est quelque chose qui, lorsqu’elle sera exécutée, va effectuer une action avec des effets de bord (généralement, lire en entrée ou afficher à l’écran) et contiendra une valeur de retour. Afficher quelque chose à l’écran n’a pas vraiment de valeur de retour significative, alors une valeur factice () est utilisée.()

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

Donc, quand est-ce que cette action sera exécutée ? Eh bien, c’est ici que le main entre en jeu. Une action I/O sera exécutée lorsqu’on lui donnemain le nom main et qu’on lance le programme ainsi créé.main

Que votre programme entier ne soit qu’une action I/O semble un peu limitant. C’est pourquoi on peut utiliser la notation do pour coller ensemble plusieurs actions I/O en une seule. Regardez l’exemple suivant :

main = do

putStrLn "Hello, what's your name?"

name <- getLine

putStrLn ("Hey " ++ name ++ ", you rock!")

Ah, intéressant, une nouvelle syntaxe ! Et celle-ci se lit presque comme un programme impératif. Si vous compilez cela et l’essayez, cela se comportera certainement conformément à ce que vous attendez. Remarquez qu’on a dit do, puis on a aligné une série d’étapes, comme en programmation impérative. Chacune de ces étapes est une action I/O. En les mettant ensemble avec la syntaxe do, on les a collées en une seule action I/O. L’action obtenue a pour type IO () , parce que c’est le type de la dernière action à l’intérieur du collage.IO ()

À cause de ça, main a toujours la signature de type main :: IO somethingmain main :: IO something , où somethingsomething est un type concret. Par convention, on ne spécifie généralement pas la déclaration de type de mainmain .

Une chose intéressante qu’on n’a pas rencontrée avant est à la troisième ligne, qui dit name <- getLinename <- getLine . On dirait qu’elle lit une ligne depuis l’entrée et la stocke dans une variable namename . Est-ce le cas ? Examinons le type de getLinegetLine .

ghci> :t getLine

getLine :: IO String

Aha, o-kay. getLinegetLine est une action I/O qui contient un résultat de type StringString . Ça tombe sous le sens, parce qu’elle attendra que l’utilisateur tape quelque chose dans son terminal, et ensuite, cette frappe sera représentée comme une chaîne de caractères. Mais qu’est-ce qui se passe dans name <- getLine alors ?name <- getLine Vous pouvez lire ceci ainsi : effectue l’action I/O getLinegetLine puis lie la valeur résultante au nom namename .

getLine

getLine a pour type IO StringIO String , donc namename aura pour type StringString . Vous pouvez imaginer une action I/O comme une boîte avec des petits pieds qui sortirait dans le monde réel et irait faire quelque chose là-bas

(comme des graffitis sur les murs) et reviendrait peut-être avec une valeur. Une fois qu’elle a attrapé une valeur pour vous, le seul moyen d’ouvrir la boîte pour en récupérer le contenu est d’utiliser la construction <-<- . Et si l’on sort une valeur d’une action I/O, on ne peut le faire qu’à l’intérieur d’une autre action I/O. C’est ainsi qu’Haskell parvient à séparer proprement les parties pure et impure du code. getLine est en un sens impure, parce que sa valeur de retour n’est pasgetLine garantie d’être la même lorsqu’on l’exécute deux fois. C’est pourquoi elle est en quelque sorte tachée par le constructeur de types IOIO , et on ne peut récupérer cette donnée que dans du code I/O. Et puisque le code I/O est taché aussi, tout calcul qui dépend d’une valeur tachée I/O renverra un résultat taché.

Quand je dis taché, je ne veux pas dire taché de façon à ce que l’on ne puisse plus jamais utiliser le résultat contenu dans l’action I/O dans un code pur. Non, on dé-tache temporairement la donnée dans l’action I/O lorsqu’on la lie à un nom. Quand on fait name <- getLinename <- getLine , namename est une chaîne de caractères normale, parce qu’elle représente ce qui est dans la boîte. On peut avoir une fonction très compliquée qui, mettons, prend votre nom (une chaîne de caractères normale) et un paramètre, 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 tellFortunetellFortune (ou n’importe quelle fonction à laquelle on passe namename ) n’a pas besoin de savoir quoi que ce soit à propos d’I/O, c’est une simple fonction String -> String !String -> String

Regardez ce bout de code. Est-il valide ?

nameTag = "Hello, my name is " ++ getLine

Si vous avez répondu 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, c’est parce que ++ requiert que ses deux paramètres soient des listes du même type. Le premier paramètre a pour type String++ String (ou [Char] si vous voulez), alors que getLine[Char] getLine a pour type IO StringIO String . On ne peut pas concaténer une chaîne de caractères et une action I/O. On doit d’abord récupérer le résultat de l’action I/O pour obtenir une valeur de type String , et le seul moyen de faire ceci c’est de faireString

name <- getLine

name <- getLine dans une autre action I/O. Si l’on veut traiter des données impures, il faut le faire dans un environnement impur. Ainsi, la trace de l’impureté se propage comme le fléau des morts-vivants, et il est dans notre meilleur intérêt de restreindre les parties I/O de notre code autant que faire se peut.

Chaque action I/O effectuée encapsule son résultat en son sein. C’est pourquoi notre précédent programme aurait pu s’écrire ainsi :

main = do

foo <- putStrLn "Hello, what's your name?"

name <- getLine

putStrLn ("Hey " ++ name ++ ", you rock!")

Cependant, foo aurait juste pour valeur ()foo () , donc écrire ça serait un peu discutable. Remarquez qu’on n’a pas lié le dernier putStrLnputStrLn . C’est parce que, dans un bloc do, la dernière action ne peut pas être liée à un nom contrairement aux précédentes. Nous verrons pourquoi c’est le cas un peu plus tard quand nous nous aventurerons dans le monde des monades. Pour l’instant, vous pouvez imaginer que le bloc do extrait automatiquement la valeur de la dernière action et la lie à son propre résultat.

À part la dernière ligne, toute ligne d’un bloc do qui ne lie pas peut être réécrite avec une liaison. Ainsi, putStrLn "BLAH"putStrLn "BLAH" peut être réécrit comme _ <- putStrLn "BLAH" . Mais ça ne sert à rien, donc on enlève le <-_ <- putStrLn "BLAH" <- pour les actions I/O dont le résultat ne nous importe pas, comme

putStrLn something putStrLn something .

Les débutants pensent parfois que faire

name = getLine

va lire l’entrée et lier la valeur de cela à name . Eh bien non, tout ce que cela fait, c’est de donner un autre nom à l’action I/O getLinename getLine , ici, namename . Rappelez-vous, pour obtenir une valeur d’une action I/O, vous devez le faire de l’intérieur d’une autre action I/O et la liant à un nom via <-<- . Les actions I/O ne seront exécutées que si elles ont pour nom main ou lorsqu’elles sont dans une grosse action I/O composée par un bloc do enmain train d’être exécutée. 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 façon, elles ne seront exécutées que si elles finissent par se retrouver dans main .main

Oh, j’oubliais, il y a un autre cas où une action I/O est exécutée. C’est lorsque l’on tape cette action I/O dans GHCi et qu’on presse Entrée.

ghci> putStrLn "HEEY"

HEEY

Même lorsque l’on tape juste des nombres ou qu’on appelle une fonction dans GHCi et qu’on tape Entrée, il va l’évaluer (autant que nécessaire) et appeler showshow sur le résultat, puis afficher cette chaîne de caractères sur le terminal en appelant implicitement putStrLnputStrLn .

Vous vous souvenez des liaisons let ? Si ce n’est pas le cas, rafraîchissez-vous l’esprit en lisant cette section. Elles sont de la forme let bindings in expression

let bindings in expression , où bindingsbindings sont les noms à donner aux expressions et expressionexpression est l’expression évaluée qui peut voir ces liaisons. On a aussi dit que dans les listes en compréhension, la partie in n’était pas nécessaire. Eh bien, on peut aussi les utiliser dans les blocs do comme on le faisait dans les listes en compréhension. 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 alignées. Notez également comme le let est aligné avec les actions I/O, et les noms du let sont alignés les uns aux autres. C’est important, parce que l’indentation importe en Haskell. Ici, on a fait map toUpper firstNamemap toUpper firstName , qui transforme quelque chose comme "John" en une chaîne de caractères bien plus cool comme "JOHN""John" "JOHN" . On a lié cette chaîne de caractères en capitales à un nom, puis utilisé ce nom dans une chaîne de caractères qu’on a affichée sur le terminal plus tard.

Vous vous demandez peut-être quand utiliser <- et quand utiliser des liaisons let ? Eh bien, souvenez-vous, <-<- <- sert (pour l’instant) à effectuer une action I/O et lier son résultat à un nom. map toUpper firstNamemap toUpper firstName , cependant, n’est pas une action I/O. C’est une expression pure en Haskell. Donc, utilisez <-<- quand vous voulez lier le résultat d’une 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é à l’action I/O getLinelet firstName = getLine getLine un nouveau nom, sans avoir exécuté cette action.

À présent, on va faire un programme qui lit continuellement une ligne et l’affiche avec les mots renversés. Le programme s’arrêtera 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 qu’il fait, essayez de l’exécuter avant qu’on s’intéresse au code.

Astuce : Pour lancer un programme, vous pouvez soit le compiler puis lancer l’exécutable produit en faisant ghc --make helloworld puisghc --make helloworld ./helloworld

./helloworld , ou bien vous pouvez utiliser la commande runhaskellrunhaskell ainsi : runhaskell helloworld.hsrunhaskell helloworld.hs et votre programme sera exécuté à la volée.

Tout d’abord, regardons la fonction reverseWordsreverseWords . C’est une fonction normale qui prend une chaîne de caractères comme "hey there man"

"hey there man" et appelle wordswords pour produire une liste de mots comme ["hey", "there", "man"]["hey", "there", "man"] . Puis, on mappe reversereverse sur la liste, résultant en ["yeh", "ereht", "nam"] , puis on regroupe cette liste en une chaîne en utilisant unwords["yeh", "ereht", "nam"] unwords et le résultat final est

"yeh ereht nam"

"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))

reverseWords st = unwords (map reverse (words st)) .

Quid de main ? D’abord, on récupère une ligne du terminal avec getLinemain getLine et on nomme cette ligne lineline . 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 exécute une action I/O, et lorsqu’elle est vide, l’action I/O sous le else est exécutée à la place. C’est 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 .if condition then I/O action else I/O action Regardons d’abord du côté de la clause else. Puisqu’on doit avoir une action I/O après le else, on crée 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 c’est plus moche. Peu importe, dans le bloc do, on appelle reverseWords

reverseWords sur la ligne obtenue de getLinegetLine , puis on l’affiche au terminal. Après cela, on exécute mainmain . C’est un appel récursif, et c’est bon, parce que main est bien une action I/O. En un sens, on est de retour au début du programme.main

Maintenant, que se passe-t-il lorsque null line est vrai ? Dans ce cas, ce qui suit le then est exécuté. Si on regarde, on voit qu’il y anull line then return ()

then return () . Si vous avez utilisé des langages impératifs comme C, Java ou Python, vous vous dites certainement que vous savez déjà ce que returnreturn fait, et il se peut que vous ayez déjà sauté ce long paragraphe. Eh bien, voilà le détail qui tue : le returnreturn de Haskell n’a vraiment

rien à voir avec le returnreturn de la plupart des autres langages ! Il a le même nom, ce qui embrouille beaucoup de monde, mais en réalité, il est bien différent. Dans les langages impératifs, return termine généralement l’exécution de la méthode ou sous-routine, en rapportant une valeur àreturn son appelant. En Haskell (plus spécifiquement, dans les action I/O), il crée une action I/O à partir d’une valeur pure. Si vous repensez à l’analogie de la boîte faite précédemment, il prend une valeur et la met dans une boîte. L’action I/O résultante ne fait en réalité rien, mais encapsule juste cette valeur comme son résultat. Ainsi, dans un contexte d’I/O, return "HAHA"return "HAHA" aura pour type IO StringIO 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 nécessaire ? Eh bien, il nous fallait une action I/O à exécuterIO dans le cas où la ligne en entrée était vide. C’est pourquoi on a créé une fausse action I/O qui ne fait rien, en écrivant return () .return ()

Utiliser return ne cause pas la fin de l’exécution du bloc do I/O ou quoi que ce soit du genre. Par exemple, ce programme va gentiment s’exécuterreturn jusqu’au bout de la dernière ligne :

main = do return () return "HAHAHA"

line <- getLine

return "BLAH BLAH BLAH"

return 4

putStrLn line

Tout ce que ces return font, c’est créer des actions I/O qui ne font rien de spécial à part encapsuler un résultat, et les résultats sont ici jetésreturn puisqu’on ne les lie pas à des noms. On peut utiliser return en combinaison avec <-return <- pour lier des choses à des noms.

main = do

a <- return "hell"

b <- return "yeah!"

putStrLn $ a ++ " " ++ b

Comme vous voyez, returnreturn est un peu l’opposé de <-<- . Alors que returnreturn prend une valeur et l’enveloppe dans une boîte, <-<- prend une boîte, l’exécute, et en extrait la valeur pour la lier à un nom. Faire cela est un peu redondant, puisque l’on dispose des liaisons let dans les blocs do, donc on préfère :

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 l’on a besoin de créer une action I/O qui ne fait rien, ou bien parcereturn qu’on ne veut pas que l’action créée par le bloc ait la valeur de la dernière action qui la compose, auquel cas on place un return tout à la fin avecreturn

le résultat qu’on veut obtenir de cette action composée.

Un bloc do peut aussi avoir une seule action I/O. Dans ce cas, c’est équivalent à écrire seulement l’action. Certains vont préférer écrire then do return ()

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

putStr est un peu comme putStrLnputStrLn puisqu’il prend une chaîne de caractères en paramètre et retourne une action I/O qui affiche cette chaîne sur le terminal, seulement putStr ne va pas à la ligne après avoir affiché la chaîne, alors que putStrLnputStr 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 résultat encapsulé est unit. Une valeur inutile, donc ça ne sert à rien de la lier.putStr :: String -> IO () putChar

putChar prend un caractère et retourne une action I/O qui l’affiche sur le terminal.

main = do putChar 't' putChar 'e' putChar 'h' $ runhaskell putchar_test.hs teh putStr

putStr est en fait défini récursivement à l’aide de putCharputChar . Le cas de base de putStrputStr est la chaîne vide, donc si on affiche une chaîne vide, elle retourne juste une action I/O qui ne fait rien à l’aide de return ()return () . Si la chaîne n’est pas vide, elle affiche son premier caractère avec

putChar

putChar , puis affiche le reste de la chaîne avec putStrputStr .

putStr :: String -> IO ()

putStr [] = return ()

putStr (x:xs) = do putChar x putStr xs

Voyez comme on peut utiliser la récursivité dans les I/O, comme dans du code pur. Comme pour un code pur, on définit un cas de base, et on réfléchit à ce que le résultat doit être. C’est une action qui affiche le première caractère puis affiche le reste de la chaîne.