• Aucun résultat trouvé

getChar est une action I/O qui lit un caractère du terminal. getLine est une action I/O qui lit une ligne du terminal. Ces deux sont plutôt simples, et la plupart des langages de programmation ont des fonctions ou des instructions semblables à ces actions I/O. Mais à présent, rencontrons getContents. getContents est une action I/O qui lit toute l’entrée standard jusqu’à rencontrer un caractère de fin de fichier. Son type est getContents :: IO String. Ce qui est cool avec

getContents , c’est qu’elle effectue une entrée paresseuse. Quand on fait foo <- getContents , elle ne va pas lire toute l’entrée d’un coup, la stocker en

mémoire, puis la lier à foo. Non, elle est paresseuse ! Elle dira : “Ouais ouais, je lirai l’entrée du terminal plus tard, quand tu en auras besoin !”.

getContents est très utile quand on veut connecter la sortie d’un programme à l’entrée de notre programme. Si vous ne savez pas comment cette connexion (à base de tubes) fonctionne dans les systèmes type Unix, voici un petit aperçu. Créons un fichier texte qui contient ce haiku :

I'm a lil' teapot

What's with that airplane food, huh? It's so small, tasteless

Ouais, le haiku est pourri, mais bon ? Si quelqu’un connaît un tutoriel de haikus, je suis preneur.

Bien, souvenez-vous de ce programme qu’on a écrit en introduisant la fonction forever. Il demandait à l’utilisateur une ligne, et lui retournait en MAJUSCULES, puis recommençait, indéfiniment. 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. À présent, 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 d’un programme (dans notre cas c’était cat) à l’entrée d’un autre (ici capslocker) est fait à l’aide du caractère |. Ce qu’on a fait ici est à peu près équivalent à lancer capslocker, puis taper notre haiku dans le terminal, avant d’envoyer le caractère de fin de fichier (généralement en tapant Ctrl-D). C’est comme lancer cat haiku.txt et dire : “Attends, n’affiche pas ça dans le terminal, va le dire à capslocker plutôt !”.

Donc ce qu’on fait principalement avec cette utilisation de forever c’est prendre l’entrée et la transformer en une sortie. C’est 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 l’action I/O getContents et on nomme la chaîne produite contents. Puis on mappe toUpper sur cette chaîne, et on l’affiche sur le terminal. Gardez en tête que puisque les chaînes de caractères sont simplement des listes, qui sont paresseuses, et que getContents est aussi paresseuse en entrée-sortie, cela ne va pas essayer de lire tout le contenu et de le stocker en mémoire avant d’afficher la version en majuscules. Plutôt, cela va afficher la version en majuscule au fur et à mesure de la lecture, parce que cela ne lira une ligne de l’entrée que lorsque ce sera nécessaire.

$ 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 l’on essaie de lancer capslocker et de taper les lignes nous-même ? $ ./capslocker

hey ho HEY HO

lets go LETS GO

On a quitté le programme en tapant Ctrl-D. Plutôt joli ! Comme vous voyez, cela affiche nos caractères en majuscules ligne après ligne. Quand le résultat de getContents est lié à contents , ce n’est pas représenté en mémoire comme une vraie chaîne de caractères, mais plutôt comme une promesse de produire cette chaîne de caractères en temps voulu. Quand on mappe toUpper sur contents, c’est aussi une promesse de mapper la fonction sur le contenu une fois qu’il sera disponible. Et finalement, quand putStr est exécuté, il dit à la promesse précédente : “Hé, j’ai besoin des lignes en majuscules ! ”. Celle-ci n’a pas encore les lignes, alors elle demande à contents : “Hé, pourquoi tu n’irais pas chercher ces lignes dans le terminal à présent ? ”. C’est à ce moment que

getContents va vraiment lire le terminal et donner une ligne au code qui a demandé d’avoir quelque chose de tangible. Ce code mappe alors toUpper sur la ligne et la donne à putStr, qui l’affiche. Puis putStr dit : “Hé, j’ai besoin de la ligne suivante, allez ! ” et tout ceci se répète jusqu’à ce qu’il n’y ait plus d’entrée, comme l’indique le caractère de fin de fichier.

Faisons un programme qui prend une entrée et affiche seulement les lignes qui font moins de 10 caractères 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 entrée, et afficher quelque chose en fonction de l’entrée, on peut l’implémenter en lisant le contenu d’entrée, puis en exécutant une fonction pure sur ce contenu, et en affichant le résultat que la fonction a renvoyé. La fonction shortLinesOnly fonctionne ainsi : elle prend une chaîne de caractères comme "short\nlooooooooooooooong\nshort again". Cette chaîne a trois lignes, dont deux courtes et une au milieu plus longue. La fonction lance lines sur cette chaîne, ce qui la convertit en

["short", "looooooooooooooong", "short again"] , qu’on lie au nom allLines . Cette liste de chaînes est ensuite filtrée pour ne garder que les lignes de moins de 10 caractères, produisant ["short", "short again"]. Et finalement, unlines joint cette liste en une seule chaîne, 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 à l’entrée de shotlinesonly et en sortie, on obtient les lignes courtes.

Ce motif qui récupère une chaîne en entrée, la transforme avec une fonction, puis écrit le résultat est tellement commun qu’il existe une fonction qui rend cela encore plus simple, appelée interact. interact prend une fonction de type String -> String en paramètre et retourne une action I/O qui va lire une entrée, lancer cette fonction dessus, et afficher le résultat. Modifions notre programme en conséquence :

main = interact shortLinesOnly

shortLinesOnly :: String -> String

shortLinesOnly input =

let allLines = lines input

shortLines = filter (\line -> length line < 10) allLines result = unlines shortLines

Juste pour montrer que ceci peut être fait en beaucoup moins de code (bien que ce soit moins lisible) et pour démontrer notre maîtrise de la composition de fonctions, on va retravailler cela.

main = interact $ unlines . filter ((<10) . length) . lines

Wow, on a réduit cela à juste une ligne, c’est plutôt cool !

interact peut être utilisé pour faire des programmes à qui l’on connecte un contenu en entrée et qui affichent un résultat en conséquence, ou bien pour faire des programmes qui semblent attendre une entrée de l’utilisateur, et rendent un résultat basé sur la ligne entrée, puis prend une autre ligne, etc. Il n’y a en fait pas de réelle distinction entre les deux, ça dépend juste de la façon dont l’utilisateur 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 à l’utilisateur si c’est un palindrome, puis lancer main à nouveau. Mais il est plus simple d’utiliser interact. Quand vous utilisez interact, pensez à tout ce que vous avez besoin de faire pour transformer une entrée en la sortie désirée. Dans notre cas, on doit remplacer chaque ligne de l’entrée 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

Plutôt direct. D’abord, 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 chaîne de caractères. À présent, on peut faire :

main = interact respondPalindromes

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

Bien qu’on ait fait un programme qui transforme une grosse chaîne de caractères en une autre, ça se passe comme si on avait fait un programme qui faisait cela ligne par ligne. C’est parce qu’Haskell est paresseux et veut afficher la première ligne de la chaîne de caractères en sortie, mais ne peux pas parce qu’il ne l’a pas encore. Dès qu’on lui donne une ligne en entrée, il va l’afficher en sortie. On termine le programme en envoyant un caractère de fin de fichier.

On peut aussi utiliser ce programme en connectant simplement un fichier en entrée. Disons qu’on ait ce fichier :

dogaroo radar rotor madam

et qu’on le sauvegarde comme words.txt. Voici ce qu’on obtient en le connectant en entrée de notre programme : $ cat words.txt | runhaskell palindromes.hs

not a palindrome

palindrome palindrome palindrome

Encore une fois, on obtient la même sortie que si l’on avait tapé les mots dans le programme nous-même. Seulement, on ne voit pas l’entrée affichée puisqu’elle est venue du programme et qu’on ne l’a pas tapée ici.

Vous voyez probablement comment les entrées-sorties paresseuses fonctionnent à présent, et comment on peut en tirer parti. Vous pouvez penser simplement à ce que doit être la sortie en fonction de l’entrée, et écrire une fonction qui effectue cette transformation. En entrée-sortie paresseuse, rien n’est consommé en entrée tant que ce n’est pas absolument nécessaire, par exemple parce que l’on souhaite afficher un résultat qui dépend de cette entrée.

Jusqu’ici, nous avons travaillé avec les entrées-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 déjà fait ça. Une manière de penser au terminal est de se dire que c’est un fichier (un peu spécial). On peut appeler le fichier de ce qu’on tape dans le terminal stdin, et le fichier qui s’affiche dans notre terminal stdout, pour entrée standard et sortie standard, respectivement. En gardant cela à l’esprit, on va voir qu’écrire ou lire dans un fichier est très similaire à écrire ou lire dans l’entrée ou la sortie standard.

On va commencer avec un programme très simple qui ouvre un fichier nommé girlfriend.txt, qui contient un vers du tube d’Avril Lavigne numéro 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 lançant, on obtient le résultat 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 première ligne contient juste quatre exclamations, pour attirer notre attention. Dans la deuxième ligne, Avril nous indique qu’elle n’apprécie pas notre partenaire romantique actuelle. La troisième ligne sert à mettre en emphase cette désapprobation, alors que la quatrième ligne suggère que nous devrions chercher une nouvelle petite amie.

Hum, regardons plutôt le programme ligne par ligne ! Notre programme consiste en plusieurs actions I/O combinées ensemble par un bloc do. Dans la première ligne du bloc do, on remarque une fonction nouvelle nommée 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 résultat la poignée vers le fichier associé.

FilePath est juste un synonyme du type String, simplement défini comme :

type FilePath = String IOMode est défini ainsi :

data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode

Tout comme notre type qui représente sept valeurs pour les jours de la semaine, ce type est une énumération qui

représente ce qu‘on veut faire avec le fichier ouvert. Très simple. Notez bien que le type est IOMode et non pas IO Mode. Ce dernier serait le type d’une action I/O qui contient une valeur d’un type Mode, alors qu’ IOMode est juste une simple énumération.

Finalement, la fonction retourne une action I/O qui ouvre le fichier spécifié dans le mode indiqué. Si l’on lie cette action à un nom, on obtient un Handle. Une valeur de type Handle représente notre fichier ouvert. On utilise cette poignée pour savoir de quel fichier on parle. Il serait stupide d’ouvrir un fichier mais ne pas lier cette poignée à un nom, puisqu’on ne pourrait alors pas savoir quel fichier on a ouvert. Dans notre cas, on lie la poignée au nom handle.

À la prochaine ligne, on voit une fonction nommée hGetContents. Elle prend un Handle, afin de savoir de quel fichier on veut récupérer le contenu, et retourne une IO String - une action I/O qui contient en résultat le contenu du fichier.

Cette fonction est proche de getContents. La seule différence, c’est que getContents lit automatiquement depuis l’entrée standard (votre terminal), alors que hGetContents prend une poignée pour savoir quel fichier elle doit lire. Pour le reste, elles font la même chose. Et tout comme getContents, hGetContents ne va pas lire tout le fichier et le stocker en mémoire, mais lire ce qui sera nécessaire pour progresser. C’est très cool parce qu’on peut traiter contents comme le contenu de tout le fichier, alors qu’il n’est pas réellement chargé en entier dans la mémoire. Donc, même pour un énorme fichier, faire hGetContents ne va pas étouffer notre mémoire, parce que le fichier est lu seulement quand c’est nécessaire.

Notez bien la différence entre la poignée, utilisée pour identifier le fichier, et le contenu de ce fichier, liés respectivement dans ce programme aux noms handle et contents . La poignée est juste quelque chose qui indique quel est notre fichier. Si vous imaginez que votre système de fichiers est un énorme livre, dont les fichiers sont des chapitres, la poignée 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 poignée et retourne une action I/O qui ferme le fichier. Il faut fermer le fichier vous-même après l’avoir 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 poignée 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 résultat retourné par cette action I/O est le même que le résultat retourné par la fonction qu’on lui fournit. Ça peut vous sembler compliqué, mais c’est en fait très simple, surtout avec des lambdas, voici le précédent exemple réécrit avec withFile :

import System.IO

main = do

withFile "girlfriend.txt" ReadMode (\handle -> do

contents <- hGetContents handle putStr contents)

Comme vous le voyez, c’est très similaire au code précédent. (\handle -> ...) est une fonction qui prend une poignée et retourne une action I/O, et on le fait généralement comme ça avec une lambda. La raison pour laquelle withFile doit prendre une fonction qui retourne une action I/O, plutôt que de prendre directement une action I/O, est que l’action I/O ne saurait pas sur quel fichier elle doit agir autrement. Ainsi, withFile ouvre le fichier et passe la poignée à la fonction qu’on lui a donnée. Elle récupère ainsi une action I/O, et crée à son tour une action I/O qui est comme la précédente 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 résultat sera une action I/O, donc on peut commencer par écrire un do. D’abord, on ouvre le fichier pour récupérer une poignée. Puis, on applique notre fonction sur cette poignée pour obtenir une action I/O qui fait son travail sur ce fichier. On lie le résultat de cette action à result, on ferme la poignée, et on fait return result. En

retournant le résultat encapsulé dans l’action I/O obtenue par f, notre action I/O composée encapsule le même résultat que celui de f handle. Ainsi, si f handle retourne une action I/O qui lit un nombre de lignes de l’entrée standard, les écrit dans un fichier, et encapsule pour résultat le nombre de lignes qu’elle a lues, alors en l’utilisant avec withFile', l’action I/O résultante aurait également pour résultat 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 poignée en paramètre et opèrent sur le fichier correspondant plutôt que l’entrée ou la sortie standard. Par exemple :

putStrLn est une fonction qui prend une chaîne de caractères et retourne une action I/O qui affiche cette chaîne dans le terminal avec un retour à la ligne à la fin. hPutStrLn prend une poignée et une chaîne et retourne une action I/O qui écrit cette chaîne dans le fichier associé à la poignée, suivie d’un retour à la ligne. Dans la même veine, hGetLine prend une poignée et retourne une action I/O qui lit une ligne de ce fichier.

Charger des fichiers et traiter leur contenu comme des chaînes de caractères est tellement commun qu’on 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 sûr) et lie son contenu à une chaîne de caractères. C’est généralement