Aller au menu - Aller au contenu

Icône Découvrez le langage avec ghci

Avatar
Mise à jour : 01/08/2010
449 visites depuis 7 jours, dont 34 sur ce chapitre classé 248/786
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 :
Code : Console
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>

Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

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 :
Code : Console
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 :
Code : Console
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 :
Code : Console
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 . :
Code : Console
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.
Code : Console
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 :
Code : Console
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 :
Code : Console
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.
Code : Console
Prelude> 2 * 3.141592653589793 * 1
6.283185307179586

Puis on va recommencer pour des cercles de rayons 2,3,4,5,...
Code : Console
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 :
Code : Console
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).
Code : Console
Prelude> 3 * (3 + 1)
12

Maintenant, on prend x = 2\pi.
Code : Console
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 :
Code : Console
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:
Code : Console
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 :
Code : Console
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 :
Code : Console
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 :
Code : Console
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 ++ :
Code : Console
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.
Code : Console
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.
Code : Console
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.
Code : Console
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.
Code : Console
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.
Code : Console
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.
Code : Console
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 :
Code : Console
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 :
Code : Console
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.
Code : Console
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 :
Code : Console
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 :
Code : Console
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 :
Code : Console
Prelude> product [1..20]
2432902008176640000


Des listes infinies


Que se passe-t-il si on écrit [1..] ? Si on essaye, on obtient
Code : Console
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.
Code : Console
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é.
Code : Console
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 :
Code : Console
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 :
Code : Console
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.
Chapitre précédent Sommaire Chapitre suivant

Partager

6 commentaires pour "Découvrez le langage avec ghci"
Note moyenne : 3.61 / 4 (33 votes)
Pseudo Commentaire
Hors ligne Atoll # Posté le 31/01/2010 à 01:36:49
Groupe : Bannis

Très bon tuto !
J'ai eu l'occasion d'apprendre ce B-A-ba pour l'ocaml ( bon c'est presque pareil l'haskell :p ) dans diverses sites, mais là c'est clair, complet tout en restant concis.
Je suis contre l'introduction de déclaration de fonction simple. Le chapitre est bien chargé comme ça. chaque chose en son temps :)
Hors ligne anonyme # Posté le 11/09/2010 à 22:19:51

Salut,

C'est bien mais tu oublies de préciser qu'il peut aussi exprimer les notations scientifiques et exprimer les nombres en base hexadécimales en décimal :

Code : Console
Prelude> 21e4
210000.0

Prelude> 1.434e4
14340.0

Prelude> 0x539
1337


Où "e" signifie *10 suivi de sa puissance et le préfixe "0x" signifie qu'on va mettre un hexa. ^^
Hors ligne gnomnain # Posté le 11/09/2010 à 22:46:11
Blblbl !
Avatar

Avis : Très bon Groupe : Anciens

Ouais, je sais, je n'ai volontairement pas parlé de ces deux notations pour ne pas alourdir le tuto (il y a aussi l'octal, et quelques règles bizarres : (+) 0o1239 donne 0o123 + 9 ). Je pourrais peut-être les ajouter en notes ?
D'ailleurs, si on regarde la syntaxe haskell il y a quelques autres trucs dont je n'ai pas parlé, notamment pour les chaines de caractères : j'ai pas parlé de comment on met des caractères unicodes (\x2603 pour un bonhomme de neige ☃) ou des string gaps (la chaine notée
Code : Haskell
1
"hello\      \world"
donne en fait "helloworld" )

Image utilisateur
Haskell - Learn You a Haskell - Real World Haskell - xmonad - OCaml
Apprenez Haskell ! - #ircduzero
<colbseton> Serialk: tu cherches vraiment des liens logiques dans tout ce que je raconte ?
 
Hors ligne anonyme # Posté le 11/09/2010 à 23:40:37

Citation : gnomnain
Je pourrais peut-être les ajouter en notes ?

Tu veux dire en annexe ?
Hors ligne gnomnain # Posté le 11/09/2010 à 23:46:14
Blblbl !
Avatar

Avis : Très bon Groupe : Anciens

Je sais pas vraiment, je verrais ça quand je refondrais ce chapitre (ça devrait arriver un jour).

Image utilisateur
Haskell - Learn You a Haskell - Real World Haskell - xmonad - OCaml
Apprenez Haskell ! - #ircduzero
<colbseton> Serialk: tu cherches vraiment des liens logiques dans tout ce que je raconte ?
 

Voir tous les commentaires