Dans ce chapitre, vous allez découvrir les bases de la syntaxe du langage. Pour ce faire, nous utiliserons le mode interactif de GHC, ghci.
Pour lancer ghci, ouvrez une console et tapez "ghci". Vous devriez voir apparaître un message qui ressemble à celui-ci :
1 2 3 4 5 | GHCi, version 6.10.2: http://www.haskell.org/ghc/ :? for help Loading package ghc-prim ... linking ... done. Loading package integer ... linking ... done. Loading package base ... linking ... done. Prelude> |
Une calculatrice
Premiers calculs
La ligne se terminant par > (ici Prelude>
) signale que ghci attend une instruction.
Opérations de base
Tapez alors votre première expression :
1 2 | Prelude> 3 + 5 8 |
ghci a évalué l'expression, et affiche son résultat
Vous pouvez essayer d'autres expressions avec des entiers, ou des nombres à virgule :
1 2 3 4 5 6 7 8 9 10 | Prelude> 12*30 360 Prelude> 1 1 Prelude> 12-3 9 Prelude> 2^12 4096 Prelude> 4/2 2.0 |
Le symbole ^ permet d'élever un nombre à une puissance entière. Utilisez ** pour une puissance non-entière.
Les priorités normales des opérations mathématiques sont respectées. On peut aussi mettre des parenthèses :
1 2 3 4 | Prelude> 3+2*5+7 20 Prelude> (3+2)*(5+7) 60 |
Nombres négatifs, nombres réels, et grands nombres
Les nombres à virgule se notent avec un . :
1 2 3 4 | Prelude>3.7 * 4.4 16.28 Prelude> 1/2 0.5 |
On note les nombres négatifs en mettant un - devant. Cependant, il y a quelques subtilités.
1 2 3 4 5 6 7 8 | Prelude> -3 -3 Prelude> -3 + 4 1 Prelude> 4 + -3 <interactive>:1:0: Precedence parsing error cannot mix `+' [infixl 6] and prefix `-' [infixl 6] in the same infix expression |
Le problème, c'est que - peut aussi servir pour faire des soustractions, et dans un cas comme ça, ghci n'arrive pas à déterminer si - est utilisé pour noter un nombre négatif ou pour noter une soustraction. La solution, c'est de mettre -3 entre parenthèses, comme ceci :
1 2 | Prelude> 4 + (-3) 1 |
Ces parenthèses seront obligatoires dans la plupart des cas, si vous obtenez des erreurs bizarres, c'est peut-être que vous les avez oubliées.
Et finalement, une fonctionnalité intéressante : Haskell supporte les entiers aussi grands que l'on veut. On peut donc écrire quelque chose comme :
1 2 | Prelude> 2^4096 10443888814131525[...]4190336 |
J'ai coupé les chiffres du résultat, mais vous pouvez essayer ça chez vous.
Des noms pour les résultats intermédiaires
Motivation : éviter les répétitions
On va maintenant calculer le périmètre d'un cercle. Pour ceux qui ne le savent pas, il est égal à $2{\pi}r$, où r est le rayon du cercle. Et $\pi$ vaut environ 3.141592653589793. On va donc calculer le périmètre pour un cercle de rayon r égal à 1.
1 2 | Prelude> 2 * 3.141592653589793 * 1 6.283185307179586 |
Puis on va recommencer pour des cercles de rayons 2,3,4,5,…
1 2 3 4 | Prelude> 2 * 3.141592653589793 * 2 12.566370614359172 Prelude> 2 * 3.141592653589793 * 3 18.84955592153876 |
Et ainsi de suite. Mais l'ordinateur n'est pas une simple calculatrice et on peut faire bien mieux. De plus, les programmeurs détestent la répétition, pour tout un tas de raisons (dont la paresse). Ici, ce qu'on n'arrête pas de répéter, c'est la valeur de $\pi$. Il faudrait donc un moyen de dire «à chaque fois que je dis pi, remplace par 3.141592653589793».
Pour cela on va utiliser ce qu'on appelle des variables.
Dans ghci, on les déclare comme ceci : let nom = valeur
. Ensuite, pour l'utiliser, il suffit d'écrire son nom, et il sera remplacé par sa valeur.
Par exemple :
1 2 3 4 5 | Prelude> let pi = 3.141592653589793 Prelude> 2 * pi * 1 6.283185307179586 Prelude> 2 * 3.141592653589793 *1 6.283185307179586 |
On retrouve bien le même résultat.
Les noms de variables sont sensibles à la casse : valeurpi et valeurPi sont des variables différentes. Un nom de variable doit commencer par une lettre minuscule.
Ne pas répéter les calculs
L'autre intérêt des variables, c'est de ne pas avoir à répéter les calculs. Imaginons que j'ai un nombre $x$, et que je veux calculer $x(x+1)$.
1 2 | Prelude> 3 * (3 + 1) 12 |
Maintenant, on prend $x = 2\pi$.
1 2 | Prelude> (2*pi)*((2*pi)+1 45.76160291153702 |
En regardant cette ligne, on se rend compte que 2*pi
est calculé deux fois. Évidemment, ce n'est pas un calcul très long. Mais si on faisait la même chose avec un calcul qui prend 2 heures, vous seriez contents de ne pas devoir attendre 2 heures de trop.
La solution est de déclarer une variable : le compilateur pourrait simplement remplacer le nom d'une variable par son expression (le calcul qui donne la valeur de la variable), mais on se rend compte que ce n'est pas le plus efficace si la variable est utilisée plusieurs fois : il faudrait refaire le calcul à chaque utilisation. C'est pourquoi il fait en sorte que la valeur de la variable ne soit calculée qu'une seule fois. Ce code donne le même résultat, mais évite les calculs superflus :
1 2 3 | Prelude> let x = 2*pi Prelude> x*(x+1) 45.76160291153702 |
Don't panic
Si vous êtes arrivés jusqu'ici, vous avez peut-être essayé un code comme celui-ci :
1 2 3 4 5 6 7 8 | Prelude> let r=5 Prelude> 2*pi*r <interactive>:1:5: Couldn't match expected type `Double' against inferred type `Integer' In the second argument of `(*)', namely `r' In the expression: 2 * pi * r In the definition of `it': it = 2 * pi * r |
Ou celui-ci si vous utilisez la définition de pi intégrée au Prelude :
1 2 3 4 5 6 7 8 9 | Prelude> let r=5 Prelude> 2*pi*r <interactive>:1:2: No instance for (Floating Integer) arising from a use of `pi' at <interactive>:1:2-3 Possible fix: add an instance declaration for (Floating Integer) In the second argument of `(*)', namely `pi' In the first argument of `(*)', namely `2 * pi' In the expression: 2 * pi * r |
Ces deux messages effrayants (toutes les erreurs de ghc sont comme ça, il faudra vous y habituer) indiquent une erreur de type. Pourtant, vous multipliez un nombre par un nombre, et si vous remplacez les variables par leur valeur, vous n'obtenez pas d'erreur. Quel est le problème alors ?
Il vient de l'interaction entre une limitation du langage introduite pour des raisons de performances et ghci. En gros, quand on entre un nombre, ghc peut le comprendre comme un nombre décimal ou un nombre entier. Quand on entre une expression comme pi * 5
, ghci comprend que 5 doit être vu comme un nombre décimal, mais quand on écrit let r = 5
, le compilateur ne sait pas encore comment r va être utilisé, et décide donc par défaut que r sera un entier. Le problème est qu'on ne peut multiplier que des nombres du même type, et qu'il n'y a pas de conversions implicite entre les types de nombres. On peut régler le problème en forçant r à être un nombre décimal :
1 | Prelude> let r=5.0
|
Le problème est légèrement plus compliqué, comme vous le verrez dans le chapitre sur les types.
Utiliser des fonctions
Appeler une fonction
Un exemple
Pour prendre la valeur absolue d'un nombre, il n'y a pas d'opérateur. Par contre, dans le Prelude (c'est le module chargé par défaut en Haskell, qui contient les fonctions et les types de base), il y a une fonction abs
qui prend un argument et renvoie sa valeur absolue. Pour appeler une fonction à un argument, il suffit d'écrire le nom de la fonction, puis l'argument, le tout séparé par un espace.
Par exemple, abs 5
donne 5, abs (-5)
donne 5, et abs (1-3)
donne 2.
max
est une fonction à deux arguments qui renvoie le plus grand de ses deux arguments (min renvoie le plus petit). On l'appelle comme ceci : max 5 3
donne 5.
La syntaxe générale pour appeler une fonction est la suivante : fonction argument_1 argument_2 ... argument_n
.
Une expression comme abs 15 + 1
est interprétée comme (abs 15) + 1
. Si vous voulez calculer abs 16
, il faut mettre des parenthèses autour de l'argument, comme ceci : abs (15 + 1)
(cela se produit avec tous les opérateurs).
Les opérateurs sont aussi des fonctions (et inversement)
La fonction mod
permet de trouver le reste de la division euclidienne d'un entier par un autre. Par exemple, mod 42 5
donne 2. On pourra préférer une notation infixe, comme 42 mod 5
. Cependant, si on fait ça, le compilateur va penser qu'on veut appliquer la fonction 42 à mod et 5, et comme 42 n'est pas une fonction mais un nombre, ça ne va pas marcher. Mais le langage fournit un mécanisme pour régler ce problème : pour utiliser une fonction en notation infixe (comme un opérateur), il suffit d'entourer son nom avec des accents graves (`, AltGr + 7 sur un clavier azerty). Le code suivant donne bien ce que l'on cherche : 42 `mod` 5.
Une autre fonction que vous aimeriez peut-être utiliser en notation infixe est la fonction div
, qui donne le quotient de la division euclidienne.
Si les fonctions sont des opérateurs, les opérateurs sont aussi… des fonctions ! En effet, pour utiliser un opérateur en position préfixe, il suffit d'entourer son nom avec des parenthèses. Par exemple, ces deux codes sont équivalents : 1 + 2
et (+) 1 2
. Les opérateurs ne sont donc pas des objets à part, impossibles à manipuler et à transformer, mais bien des fonctions comme les autres. On peut donc en définir (vous verrez comment au prochain chapitre), et les manipuler exactement de la même manière que les fonctions, ce qui nous facilitera grandement la vie quand nous utiliserons des fonctions d'ordre supérieur, qui prennent d'autres fonctions comme arguments.
Paires
Les paires sont une façon de stocker deux valeurs en même temps. Il n'y a pas grand-chose à savoir : pour noter les paires, on écrit entre parenthèses les deux valeurs séparées par une virgule, comme ceci : (5,12)
. Les deux éléments de la paire peuvent être de types différents. Ils peuvent même être des paires ! Par exemple, (5,(6,7))
est aussi une paire, dont le premier élément est 5 et dont le second élément est une paire.
Les fonctions fst et snd permettent d'obtenir respectivement les premiers et deuxièmes éléments d'une paire. Un exemple :
1 2 3 4 5 | Prelude> let paire = (5,(6,7)) Prelude> snd paire (6,7) Prelude> fst (snd paire) + snd (snd paire) 13 |
Listes, chaînes de caractères
Listes
Les listes permettent de stocker un certain nombre d'éléments du même type. Par exemple, [1,2,3,4,5]
est une liste. Comme vous le voyez, les listes sont notées entre crochets, et les éléments sont séparés par des virgules. Par contre, [1,2,(3,4)]
n'est pas une liste valide parce que les éléments n'ont pas le même type. Un cas particulier de liste est []
, qui représente la liste vide.
Opérations sur les listes
Il existe beaucoup de fonctions pour manipuler les listes et toutes les présenter serait beaucoup trop long. Je ne montrerai que les plus importantes.
Pour prendre deux listes et les mettre bout à bout, on utilise l'opérateur de concaténation ++
:
1 2 3 4 5 6 7 8 9 10 | Prelude> [1,2,3] ++ [4,5,6] [1,2,3,4,5,6] Prelude> [1,2] ++ [(3,4)] <interactive>:1:5: Couldn't match expected type `Integer' against inferred type `(Integer, Integer)' Expected type: [Integer] Inferred type: [(Integer, Integer)] In the second argument of `(++)', namely `b' In the expression: a ++ b |
On obtient une erreur dans le deuxième cas, puisque la liste obtenue aurait des éléments de types différents. Attention, ++
n'est pas très efficace : plus la première liste est longue, plus ++
prend de temps. Cela prend beaucoup de temps de rajouter un élément à la fin d'une longue liste. Au contraire, si on ajoute un élément au début de la liste, quelle que soit la liste, l'opération est instantanée. On pourrait écrire [0]++[1,2,3]
, mais il existe un opérateur exprès pour ça : :
. Cet opérateur, parfois appelé cons permet d'ajouter un élément au début de la liste. C'est l'opération de base permettant de construire une liste, toutes les autres opérations qui créent une liste l'utilisent. cons ne copie pas la liste devant laquelle on rajoute un élément, mais puisqu'on ne peut pas la modifier, vous ne vous en rendrez pas compte. Cependant, c'est ça qui lui permet d'être si rapide.
1 2 3 4 | Prelude> 0:[1,2,3] [0,1,2,3] Prelude> 0:1:2:3:[] [0,1,2,3] |
Le deuxième exemple montre que l'on peut toujours écrire une liste à partir de : et de la liste vide. D'ailleurs, noter une liste entre crochets, comme [1,2,3]
, est seulement un raccourci pour cette notation.
head
et tail
sont les opérations inverses de cons : head
donne le premier élément d'une liste, et tail
la liste à laquelle on a retiré ce premier élément. Comme cons, ces opérations sont instantanées et ne demandent pas de copier la liste.
1 2 3 4 5 6 7 8 9 10 11 | Prelude> let xs = [0,1,2,3] Prelude> head xs 0 Prelude> tail xs [1,2,3] Prelude> head xs:tail xs [0,1,2,3] Prelude> head [] *** Exception: Prelude.head: empty list Prelude> tail [] *** Exception: Prelude.tail: empty list |
Comme vous le voyez, head
et tail
renvoient une erreur quand la liste est vide, puisqu'une liste vide n'a pas de premier élément.
Si on veut prendre un élément particulier d'une liste, on peut utiliser l'opérateur !!
. liste !! n
donne l'élément de rang n de la liste (les éléments sont numérotés à partir de 0). Si la liste n'a pas d'élément de rang n, on obtient une erreur.
1 2 3 4 | Prelude> [1,2,3] !! 0 1 Prelude> [1,2,3] !! 3 *** Exception: Prelude.(!!): index too large |
Les fonctions take
et drop
permettent respectivement de prendre les n premiers éléments de la liste, et la liste à laquelle on a enlevé les n premiers éléments. Ces fonctions ne renvoient pas d'erreur quand n est trop grand.
1 2 3 4 5 6 7 8 9 | Prelude> let xs = [1,2,3,4,5] Prelude> take 2 xs [1,2] Prelude> drop 2 xs [3,4,5] Prelude> take 100 xs [1,2,3,4,5] Prelude> drop 100 xs [] |
La fonction elem
permet de tester si un élément est dans une liste ou non. Elle renvoie True si c'est le cas, False sinon. On l'utilise souvent en notation infixe.
1 2 3 4 | Prelude> 1 `elem` [0,1,2] True Prelude> 42 `elem` [1,3,3,7] False |
Avec reverse
, il est possible de renverser l'ordre d'une liste.
1 2 | Prelude> reverse [1,2,3] [3,2,1] |
length
renvoie la longueur d'une liste. Les fonctions minimum et maximum renvoient, sans surprise, le minimum et le maximum des éléments d'une liste (à condition qu'on puisse les ordonner). Enfin sum
et product
renvoient respectivement la somme et le produit des éléments d'une liste de nombres. Quelques exemples :
1 2 3 4 5 6 7 8 9 10 11 12 13 | Prelude> let liste = [1,42,47,85,62,31,12,93] Prelude> length liste 8 Prelude> length [] 0 Prelude> maximum liste 93 Prelude> minimum liste 1 Prelude> sum liste 373 Prelude> product liste 359901496080 |
Il est aussi possible de créer des listes de listes. Les listes peuvent avoir des longueurs différentes, mais doivent toutes contenir des éléments du même type. Par exemple, [[],[]]
est une liste de liste valide, mais [[5,6],[[]]]
ne marche pas : le premier élément est une liste d'entiers et le deuxième est une liste de listes. On peut transformer une liste de listes en liste tout court avec la fonction concat
:
1 2 | Prelude> concat [[1,2,3],[4,5,6],[7,8,9]] [1,2,3,4,5,6,7,8,9] |
Noter une séquence
Dans les exemples précédents, toutes les listes de nombres ont été entrées à la main. Mais si on voulait la liste des nombres de 1 à 100 ? On pourrait les entrer à la main, mais ce serait bien trop long. Heureusement, Haskell offre une syntaxe spéciale pour les suites arithmétiques.
Pour afficher tous les entiers entre deux entiers donnés, il suffit d'écrire entre crochets le premier nombre, puis le dernier nombre et de mettre deux points entre les deux.
1 2 3 4 5 | Prelude> [0..10] [0,1,2,3,4,5,6,7,8,9,10] Prelude> let n = 42 Prelude [n..n+5] [42,43,44,45,46,47] |
On peut écrire n'importe quelle suite arithmétique en donnant les deux premiers nombres, puis le dernier. On peut aussi utiliser cette notation quand on veut que les nombres soient dans l'ordre décroissant :
1 2 3 4 5 6 | Prelude>[0,2..10] [0,2,4,6,8,10] Prelude> [10..0] [] Prelude> [10,9..0] [10,9,8,7,6,5,4,3,2,1,0] |
Cependant, cela ne marche qu'avec les suites arithmétiques. Il y a aussi quelques problèmes avec les nombres à virgules, donc il vaut mieux éviter de les utiliser avec cette notation. Ces problèmes ne sont pas causés par le langage en lui-même, mais par la façon dont les nombres à virgule sont représentés en mémoire. Par exemple :
1 2 | Prelude> [0.1,0.3..1] [0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999] |
On peut bien sûr combiner cette notation avec toutes les fonctions sur les listes. Par exemple, pour calculer $20!$ (le produit de tous les nombres de 1 à 20), il suffit d'utiliser product
:
1 2 | Prelude> product [1..20] 2432902008176640000 |
Des listes infinies
Que se passe-t-il si on écrit [1..]
? Si on essaye, on obtient
1 2 | Prelude> [1..] [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,Interrupted. |
J'ai appuyé sur Ctrl + C pour l'arrêter, sinon il allait remplir mon écran de chiffres, mais si je l'avais laissé tourner, il aurait affiché la liste des tous les nombres à partir de 1. On peut donc obtenir des listes infinies en Haskell. On peut les manipuler presque comme des listes normales : on peut prendre certains de leurs éléments, ajouter un élément devant, … En général, on prend la précaution de mettre quelque chose comme take 10
avant d'afficher la liste pour éviter les catastrophes.
1 2 3 4 | Prelude> take 10 [1..] [1,2,3,4,5,6,7,8,9,10] Prelude> take 10 (0:[1..]) [0,1,2,3,4,5,6,7,8,9] |
Si on peut faire des listes infinies, c'est grâce à l'évaluation paresseuse : un élément de la liste n'est calculé que lorsqu'il est réellement demandé. Cependant, certaines fonctions comme reverse
, minimum
et maximum
, sum
et product
ne se terminent pas sur les listes infinies, car elles ont besoin de lire la liste en entier pour pouvoir répondre.
Quelques autres fonctions permettent de manipuler les listes infinies : cycle répète une liste une infinité de fois, repeat
répète seulement un élément. La fonction replicate
fait la même chose que repeat
, sauf qu'elle prend un argument qui indique combien de fois l'élément doit être répété.
1 2 3 4 5 6 | Prelude> take 20 (cycle [0..2]) [0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1] Prelude> take 20 (repeat 0) [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] Prelude> replicate 5 0 [0,0,0,0,0] |
Chaînes de caractères
Notation
On note une chaîne de caractères entre guillemets doubles. Par exemple, "Hello World"
(essayez d'entrer ça dans ghci). Pour échapper les caractères gênants (comme "
et \
), on utilise un \
. Exemple : "Plein de \" et de \\"
.
Ce sont des listes !
En réalité, les chaînes de caractères sont juste des listes de caractères. Un caractère se note entre apostrophes, par exemple 'a'
. On peut aussi utiliser des séquences d'échappement quand on note des caractères : '\n'
représente un retour à la ligne.
Cela veut dire que l'on peut utiliser toutes les opérations disponibles sur les listes sur des chaînes de caractères :
1 2 3 4 5 6 7 8 | Prelude> 'a':"bcd" "abcd" Prelude> "Hello " ++ "World" "Hello World" Prelude> reverse "Hello World" "dlroW olleH" Prelude> 'a' `elem` "Hello World" False |
On peut aussi noter des suites de caractères de la même façon que des suites de nombres :
1 2 3 4 5 6 | Prelude> ['a'..'z'] "abcdefghijklmnopqrstuvwxyz" Prelude> ['a','c'..'z'] "acegikmoqsuwy" Prelude> ['z','y'..'a'] "zyxwvutsrqponmlkjihgfedcba" |
Dans le prochain chapitre, vous allez apprendre à définir des fonctions et comment écrire du code dans un fichier pour l'utiliser dans ghci. Vous verrez aussi beaucoup de points de syntaxe qui n'ont pas été abordés dans ce chapitre.