Améliorons le plus ou moins
Pour commencer, nous allons améliorer le plus ou moins. Ce sera l'occasion de voir comment générer des nombres aléatoires, manipuler les fichiers, et compiler un programme.
Générer un nombre
Il n'y a pas de fonction pure pour générer un nombre aléatoire en Haskell : par principe, une telle fonction devrait renvoyer une même valeur pour les mêmes arguments, et ce n'est pas ce que l'on veut. Cependant, il est tout de même possible de générer des nombres aléatoires, en utilisant les fonctions du
module System.Random. La première chose à avoir est une source de nombre aléatoires. En réalité, on ne peut pas générer de nombres vraiment aléatoires avec un ordinateur : le résultat des opérations est parfaitement déterminé. Mais il existe des techniques pour générer, à partir d'une valeur de départ, des suites de nombres qui ont l'air aléatoires.
Il y a deux possibilités : vous pouvez décider de passer explicitement l'état du générateur à chaque fonction, et compter sur ces fonctions pour retourner l'état suivant du générateur. Par exemple, pour tirer deux nombres aléatoires à la suite, le code ressemblerait à ceci :
Code : Haskell | random2 :: (RandomGen g) => g -> (Int,Int,g)
random2 gen = let (a,gen2) = random gen in
let (b,gen3) = random gen2 in
(a,b,gen3)
|
Cette fonction prend donc en argument un générateur aléatoire, et doit renvoyer un générateur aléatoire pour être utilisé par la fonction suivante. Le code n'est pas très beau, et en plus vous risquez de vous tromper et d'utiliser deux fois random avec le même état, ce qui fait que vous obtiendrez deux fois le même nombre (puisque la valeur de retour ne dépend que des arguments).
Soit vous décidez d'utiliser la solution simple, et vous utilisez les fonctions de génération des nombres aléatoires retournant une valeur dans la monade IO. Ces fonctions stockent en réalité un état global du générateur de nombres aléatoires, mais il est caché et vous n'avez pas à vous en soucier. Pour générer un nombre aléatoire, utilisez simplement la fonction randomRIO en lui indiquant l'intervalle qui vous intéresse. Vous pouvez aussi utiliser la fonction randomIO, mais elle ne prend pas d'intervalle en argument et risque de vous donner des nombres beaucoup trop grands pour ce que vous voulez en faire. Vous devrez parfois aussi indiquer le type de retour que vous souhaitez (ce n'est pas nécessaire ici, car le type des arguments de randomRIO permet de déterminer son type de retour).
Code : Haskell | jouer xmin xmax = do
x <- randomRIO (xmin,xmax)
plusOuMoins x xmin xmax 0
|
Voilà, votre plus ou moins est capable de générer des nombres aléatoires.
Compiler votre programme
Maintenant que vos programmes sont capables de faire quelque chose en dehors de retourner une valeur, il est temps d'apprendre à les compiler. Il n'y a pas grand-chose de compliqué. D'abord, vous devez mettre une fonction main quelque part (par exemple, notre fichier PlusOuMoins.hs) :
Code : Haskell 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | import System.Random
plusOuMoins x xmin xmax ncoups = do
putStrLn $ "Entrez un nombre entre " ++ show xmin ++ " et " ++ show xmax
y <- readLn
case compare x y of
LT -> do
putStrLn "Plus petit !"
plusOuMoins x xmin (y-1) (ncoups + 1)
GT -> do
putStrLn "Plus grand !"
plusOuMoins x (y+1) xmax (ncoups + 1)
EQ -> do
putStrLn $ "Bravo, vous avez trouvé le nombre en " ++ show (ncoups + 1) ++ " essais"
return (ncoups + 1)
jouer :: Int -> Int -> IO Int
jouer xmin xmax = do
x <- randomRIO (xmin,xmax)
plusOuMoins x xmin xmax 0
main = jouer 1 100
|
Ensuite, pour compiler ce programme, il suffit de lancer la commande
ghc --make PlusOuMoins.hs. Si vous n'obtenez pas d'erreurs, vous devriez trouver dans le dossier un exécutable appelé
PlusOuMoins. Vous n'avez plus qu'à le lancer pour jouer !
Plus de fonctions d'entrées-sorties
Combiner des actions IO
Imaginons que vous voulez coder un programme qui prend du texte en entrée, et à chaque ligne de texte, renvoie cette ligne en majuscule. Il y a dans le module Data.Char une fonction toUpper qui met un caractère en majuscule. Le premier coder qui pourrait vous venir à l'esprit est celui-ci :
Code : Haskell | import Data.Char
main = do
l <- getLine
putStrLn $ map toUpper l
main
|
Vous remarquez qu'on utilise un appel récursif à la fin de la fonction, pour que l'action se répète. Il y a une fonction pour ça, dans le module Control.Monad : c'est la fonction
forever. Vous pouvez donc réécrire votre programme comme ceci :
Code : Haskell | import Data.Char
import Control.Monad
main = forever $ do
l <- getLine
putStrLn $ map toUpper l
|
Son type est
forever :: IO a -> IO b (en effet, cette fonction ne devrait jamais se terminer, donc la valeur de retour n'est pas utilisée). Son type réel est un peu plus général, mais ce type devrait vous suffire pour recoder forever vous-même :
Code : Haskell | forever a = do
a
forever a
|
Si vous voulez utiliser votre programme sans le compiler d'abord, vous pouvez utiliser le programme runhaskell : par exemple, si vous avez enregistré votre programme dans le fichier up.hs, utilisez simplement la commande runhaskell up.hs
Comme les fonctions, les actions IO ne subissent pas de traitement particulier : il est possible de les prendre comme arguments, et de les combiner pour donner d'autres actions. D'ailleurs, Control.Monad comprend plein d'autres fonctions pour combiner des actions.
Par exemple, les fonctions
when et
unless permettent d'exécuter une action IO conditionnellement :
when condition action exécute l'action si la condition est vraie, et
return () sinon, et
unless fait l'inverse.
Par exemple, si vous voulez créer un programme qui n'affiche que les lignes qui ne commencent pas par un espace :
Code : Haskell | import Data.List
import Control.Monad
main = forever $ do
l <- getLine
unless (" " `isPrefixOf` l) $ putStrLn l
|
La fonction sequence est aussi plutôt utile : son type est
sequence :: [IO a] -> IO [a]. Elle exécute donc toutes les actions d'une liste à la suite, et donne le résultat. On peut donc l'utiliser avec toutes les fonctions qui donnent des listes, comme map. Par exemple, vous pouvez faire un programme qui compte jusqu'à 10 en utilisant map :
Code : Haskell | compter n = sequence $ map (putStrLn . show) [1..n]
|
Mais pour ce genre d'utilisation, vous pouvez utiliser directement la fonction mapM :
Code : Haskell | compter n = mapM (putStrLn . show) [1..n]
|
La fonction mapM_ peut aussi servir si vous n'avez pas besoin du résultat des fonctions (ce qui est le cas dans notre exemple) : elle retourne une valeur de type IO () (donc ignore les résultats) :
Code : Haskell | compter n = mapM_ (putStrLn . show) [1...n]
|
Vous pourrez aussi parfois voir les fonctions forM et forM_ : il s'agit de mapM et mapM_ avec leurs arguments inversés.
Entrées et sorties standard
Il y a quelques fonctions utiles pour interagir avec l'utilisateur que vous n'avez pas encore vues.
La fonction putStr fait la même chose que putStrLn mais n'insère pas de retour à la ligne automatiquement. Cela peut être utile pour faire un prompt pour demander des informations :
Code : Haskell | allo = do
putStr "Dites quelque chose: "
l <- getLine
putStr "Vous avez dit : "
putStrLn l
|
Cependant, ce code ne marche pas comme vous l'attendez : dans ghci, vous verrez bien "Dites quelque chose:", mais si vous lancez ce script avec runhaskell, il attendra une entrée, puis il affichera le message demandant l'entrée. Cela ne vient pas encore d'un problème avec l'ordre d'exécution, mais du fait que par défaut, la sortie n'est affichée qu'à chaque caractère de retour à la ligne. Si cela pose un problème, vous pouvez désactiver ce comportement de plusieurs façons. La première, c'est d'utiliser la fonction hFlush du module System.IO : quand vous voulez que la sortie soit affichée immédiatement, ajoutez simplement
hFlush stdout. Sinon, vous pouvez désactiver complètement la mise en cache en exécutant l'action
hSetBuffering stdout NoBuffering.
Vous pouvez rencontrer le même problème avec la fonction
getChar : cette fonction attend un caractère de l'utilisateur. L'entrée standard aussi n'est lue que lorsque l'utilisateur appuie sur entrée. Pour régler ce problème, vous pouvez si besoin utiliser
hSetBuffering stdin NoBuffering. Ensuite, vous pouvez créer un programme qui réagit dès que l'utilisateur appuie sur une touche. Par exemple, ce bout de programme permet de répondre par o ou n à une question sans avoir à appuyer sur entrée après :
Code : Haskell 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | import System.IO
main = do
hSetBuffering stdout NoBuffering
hSetBuffering stdin NoBuffering
r <- ouiNon
putStrLn $ show r
ouiNon = do
putStr "Oui ou non? "
c <- getChar
putChar '\n'
case c of
'y' -> return True
'n' -> return False
_ -> ouiNon
|
La fonction putChar utilisée ici permet d'afficher un caractère. Une autre fonction très pratique est la fonction print : vous écrivez souvent
putStrLn (show a) ? Remplacez ce code tout simplement par
print a.
Si votre programme lit toute l'entrée d'un coup, la fonction getContents peut vous être utile : elle lit toute l'entrée d'un seul coup. Par exemple, on peut facilement créer un programme qui compte les lignes d'un fichier avec getContents :
Code : Haskell | main = do
l <- getContents
print $ length (lines l)
|
Ce genre de programme peut être utile si on lui passe la sortie d'un autre programme : par exemple, au lieu de compter les lignes d'un fichier avec la commande
wc, vous pouvez faire
cat fichier | runhaskell wc.hs (en tout cas, ça marche sous un environement type Unix). Faisons un autre test : on va créer un programme qui affiche le nombre de caractères de chaque ligne.
Code : Haskell | main = do
l <- getContents
mapM_ (print . length) (lines l)
|
On teste ce programme avec un exemple (les chiffres sont ce qui est renvoyé par le programme) :
Code : Console | Hello, world!
13
ABC
3
haskell c'est bien
18 |
Surprise : au lieu d'attendre la fin de l'entrée pour donner le résultat, notre programme affiche le nombre de caractères après chaque ligne. En fait, la fonction getContents est
paresseuse : au lieu de lire tout le contenu, puis de le renvoyer, elle crée une liste, et à chaque fois qu'un caractère de cette liste est demandé, elle le lit sur l'entrée standard. C'est très pratique pour un certain nombre de programmes, où on aimerait bien afficher le résultat dès que possible. Par exemple, quand on utilise des
pipes pour connecter les entrées et les sorties de plusieurs programmes, on aime bien que chaque programme affiche le résultat en fonction de ce qu'il a déjà reçu, au lieu d'attendre la fin de l'entrée pour tout afficher.
Dans le même esprit, il y a la fonction interact : elle prend une fonction de type String -> String, et renvoie une action IO (). On peut donc interagir en même temps avec l'entrée et la sortie, et coder notre programme de cette façon :
Code : Haskell | main = interact (unlines . map (show . length) . lines)
|
C'est plutôt court ! En fait, ce qu'on fait, c'est qu'on prend ce qui arrive en entrée, on le découpe en lignes, on compte le nombre de caractères de chaque ligne, qu'on transforme immédiatement en chaîne de caractères, et on regroupe le tout avec unlines. Avec interact, lines et unlines, il est possible de coder très rapidement des programmes qui traitent l'entrée ligne par ligne. Si vous avez du mal avec les compositions de fonctions en chaîne, vous pouvez découper un peu plus, en donnant un nom à notre fonction de traitement d'une ligne.
Par contre, il est moins facile d'utiliser ces fonctions pour des programmes qui doivent interagir plus directement avec l'utilisateur : on sait que les informations seront demandées dans l'ordre, affichées dans l'ordre, mais il est difficile de déterminer dans quel ordre exact seront faites les entrées par rapport aux sorties.
Fichiers, dossiers et ligne de commande
Manipuler des fichiers
Il est aussi possible de manipuler les fichiers. Toutes les fonctions permettant de manipuler les fichiers se trouvent dans le module
System.IO. Pour ouvrir un fichier, on utilise la fonction
openFile :: FilePath -> IOMode -> IO Handle. Le type FilePath est juste un autre nom pour le type String : c'est donc le nom du fichier. Le type IOMode sert à indiquer ce qu'on souhaite faire avec le fichier. Il est défini par
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode. Il définit la façon dont on veut interagir avec le fichier : le lire uniquement (ReadMode), écrire dedans (WriteMode), écrire à la fin du fichier (AppendMode), ou lire et écrire (ReadWriteMode). Ensuite, on obtient un
Handle, qui représente le fichier ouvert.
Ensuite, on peut lire et modifier le fichier avec des opérations comme hGetContents, hPutChar, hPutStr, hGetLine, ou hGetChar. Ces fonctions marchent presque comme leurs versions sans h devant, sauf qu'elles prennent un paramètre supplémentaire : un Handle qui correspond au fichier que l'on veut modifier. Enfin, après avoir terminé avec un fichier, n'oubliez pas de le fermer avec la fonction hClose.
Par exemple, ce programme lit un fichier, rajoute un numéro de ligne devant chaque ligne et écrit les lignes obtenues dans un deuxième fichier :
Code : Haskell 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | import System.IO
numeroter inp outp n = do
t <- hIsEOF inp
if t
then return ()
else do
x <- hGetLine inp
hPutStrLn outp $ show n ++ ": " ++ x
numeroter inp outp (n+1)
main = do
inp <- openFile "test" ReadMode
outp <- openFile "test.num" WriteMode
numeroter inp outp 1
hClose inp
hClose outp
|
En plus des fonctions mentionnées plus haut, on a utilisé la fonction
hIsEOF : elle permet de tester si on est arrivé à la fin du fichier ou s'il reste du contenu à lire. Au lieu d'utiliser openFile et hClose, vous pouvez utiliser la fonction withFile. L'avantage de cette fonction est que le fichier est automatiquement fermé, quoi qu'il arrive. Son type est
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r : elle prend une fonction qui utilise le fichier ouvert, et lui passe le descripteur de fichier, puis ferme le fichier avant de retourner le résultat. Par exemple, on peut réécrire notre fonction main de cette façon :
Code : Haskell | main = do
withFile "test" ReadMode
(\inp -> withFile "test.num" WriteMode
(\outp -> numeroter inp outp 1))
|
Enfin, vous aimerez probablement utiliser les fonctions readFile, writeFile et appendFile : la fonction readFile retourne le contenu d'un fichier, la fonction writeFile écrit le contenu qu'on lui donne dans un fichier (et écrase le contenu si le fichier existait déjà), et la fonction appendFile ajoute quelque chose à la fin d'un fichier, en le créant si nécessaire. Par exemple, on peut numéroter les lignes comme ceci :
Code : Haskell | main = do
x <- readFile "test"
writeFile "test.num" $ unlines . zipWith (\n l -> show n ++ ": " ++ l) [1..] . lines $ x
|
Comme avec getContents, le fichier est lu de façon paresseuse. Ce code fonctionne un peu comme les programmes qui utilisent interact : on coupe l'entrée (ici, le contenu d'un fichier) en lignes, puis on traite le contenu comme une liste de lignes, et enfin on le regroupe avant de l'afficher.
Si votre programme a besoin d'afficher des résultats à l'écran ou de les écrire dans un fichier suivant le choix de l'utilisateur, pas besoin de faire une fonction pour chaque cas : l'entrée et la sortie standard peuvent aussi être traitées comme un fichier. Utilisez simplement stdin et stdout comme Handle (respectivement pour l'entrée et la sortie)
Arguments de la ligne de commande
Si vous développez des programmes qui s'utilisent en ligne de commande, vous aurez besoin de gérer les arguments donnés à votre programme. Pour cela, il y a deux actions IO intéressantes dans le module System.Environment. La première est
getProgName :: IO String, elle renvoie le nom du programme (argv[0] en C). La deuxième est
getArgs :: IO [String], qui renvoie la liste des arguments.
Testons ces deux fonctions :
Code : Haskell | import System.Environment
import Control.Monad
main = do
p <- getProgName
putStrLn $ "Nom du programme: " ++ p
a <- getArgs
mapM_ putStrLn a
|
Vous pouvez tester ce programme après l'avoir compilé :
Code : Console | $ ./args arg1 arg2 arg3
Nom du programme: args
arg1
arg2
arg3 |
Vous pouvez aussi passer des arguments au programme avec runhaskell : par exemple, dans notre cas, on utiliserait la commande
runhaskell args.hs arg1 arg2 arg3, et le nom du programme serait
args.hs.