Cette deuxième partie va vous apprendre à définir des fonctions un peu plus intéressantes.
if/then/else
Une construction utile est
if.
if renvoie le résultat d'une expression ou d'une autre suivant qu'une condition est vraie ou fausse.
Elle s'écrit comme ceci :
if condition then expression 1 else expression 2
.
condition est une expression qui donne un booléen, c'est-à-dire vrai ou faux.
Si la condition vaut True, expression 1 est renvoyée, sinon expression 2 est renvoyée. En pratique, seule l'expression renvoyée est calculée.
Pour utiliser if, il est donc essentiel de savoir manipuler les booléens. Un booléen a deux valeurs possibles :
True (vrai) et
False (faux). Les noms sont sensibles à la casse, donc n'oubliez pas la majuscule.
Opérateurs de comparaison
Dans une condition, ce qui nous intéressera en général, c'est de comparer des objets. Pour cela, il existe des opérateurs de comparaison, qui prennent deux arguments et renvoient un booléen :
| Opérateur |
Renvoie True si... |
| == |
les deux arguments sont égaux |
| /= |
les deux arguments sont différents |
| < |
le premier argument est inférieur au deuxième |
| > |
le premier argument est supérieur au deuxième |
| <= |
le premier argument est inférieur ou égal au deuxième |
| >= |
le premier argument est supérieur ou égal au deuxième |
La ligne la plus importante à retenir est celle en gras : on écrit
/=, et non pas
!=.
Certaines choses ne sont pas comparables, par exemple les fonctions. On ne peut comparer des paires que si elles sont composées d'éléments comparables.
Testons ces opérations sur quelques valeurs :
Code : Console | Prelude> 42 == 1337
False
Prelude> 4 < 5
True
Prelude> (2*7+6) >= (7*7-23)
False
Prelude> (1,7,3) == (4,2)
<interactive>:1:11:
Couldn't match expected type `(t, t1, t2)'
against inferred type `(t3, t4)'
In the second argument of `(==)', namely `(4, 2)'
In the expression: (1, 7, 3) == (4, 2)
In the definition of `it': it = (1, 7, 3) == (4, 2) |
Comme vous le voyez, on ne peut comparer (même pour l'égalité) que des valeurs du même type.
Combiner des booléens
Quand ces conditions ne sont pas suffisantes, on peut les combiner. Pour cela, on dispose de trois fonctions déjà définies.
La fonction
not prend un argument et l'inverse simplement :
not False
donne
True et
not True
donne
False.
Si on veut que deux conditions soient vraies, on peut utiliser l'opérateur
et, noté
&&. Cet opérateur ne renvoie
True que si ses deux arguments sont égaux à
True. Par exemple,
True && False
donne
False, et
True && True
donne
True.
Enfin, l'opérateur
|| (
ou) permet de tester si au moins une des deux conditions est vraie. Donc,
False || False
renvoie
False, et
True || False
renvoie
True.
Pour montrer comment fonctionnent ces trois fonctions, on va coder un exemple qui utilise les trois. Le but est de code une fonction "ou exclusif" (ou xor) qui prend deux arguments et renvoie
True si un seul de ses arguments vaut
True,
False sinon.
Vous pouvez essayer de trouver comment le faire vous-même. Si vous ne trouvez pas (ou si vous voulez vérifier votre solution), regardez la solution.
Secret (cliquez pour afficher)
On va appeler les deux arguments x et y. On se rend compte que xor x y
vaut True seulement si deux conditions sont respectées : x ou y doit valoir True (donc x || y
doit donner True). De plus, x et y ne doivent pas être tous les deux vrais : x && y
doit donner False, donc not (x && y)
doit donner True.
Finalement, on aboutit à ceci :
xor x y = (x || y) && not (x && y)
Utiliser if
Vous pouvez tester
if dans ghci :
Code : Console | Prelude> let x = 7
Prelude> if x > 5 then 42 else 0
42
Prelude> let x = 2
Prelude> if x > 5 then 42 else 0
0 |
La partie else est obligatoire. Si vous avez fait du C ou du php, if ressemble beaucoup à l'opérateur ternaire (qui revoie une valeur)
Les deux branches doivent renvoyer des valeurs du même type, sinon ça ne compilera pas !
Maintenant, une astuce utile. Prenons les fonctions suivantes
Code : Haskell | nul x = if x == 0 then True else False
nonNul x = if x == 0 then False else True
|
On peut faire plus court : quand notre
if renvoie des booléens, on peut enlever le
if, comme ceci :
Code : Haskell | nul x = x == 0
nonNul x = not (x==0)
|
On va utiliser
if pour écrire une fonction qui prend un entier et renvoie
"Negatif"
s'il est strictement inférieur à 0,
"Positif"
sinon.
Code : Haskell | signe x = if x >= 0 then "Positif" else "Negatif"
|
On pourrait écrire ce code sur plusieurs lignes :
Code : Haskell | signe x = if x >= 0
then "Positif"
else "Negatif"
|
Mais on ne peut pas écrire ça :
Code : Haskell | signe x = if x >= 0
then "Positif"
else "Negatif"
|
En effet, l'indentation est importante en Haskell : ce qui est à l'intérieur de la fonction doit être plus indenté que le début de la déclaration de la fonction.
Filtrage de motif
case of
L'autre structure conditionnelle importante est
case of.
Observons là sur un exemple simple :
Code : Haskell | enLettres x = case x of
0 -> "Zero"
1 -> "Un"
2 -> "Deux"
_ -> "Trop grand!"
|
Cette construction peut vous faire penser à un
switch en C.
On écrit
case variable of
, et en dessous une série de motifs ainsi que ce qu'il faut renvoyer quand variable correspond à un de ces motifs. Donc x est comparé aux motifs dans l'ordre, et on obtient le résultat de l'expression associée au premier motif qui correspond. Si aucun motif ne correspond, on obtient une erreur.
Dans cet exemple, on a deux types de motifs : une valeur (0, 1, 2) et
_ qui est un motif qui correspond à n'importe quelle valeur.
Code : Haskell | enLettres x = case x of
_ -> "Trop grand!"
0 -> "Zero"
1 -> "Un"
2 -> "Deux"
|
Puisque les motifs sont testés dans l'ordre, si on changeait l'ordre des motifs, on obtiendrait des résultats différents. Ici,
enLettres renverra toujours
"Trop grand!"
.
On peut aussi écrire des motifs plus compliqués :
Code : Haskell | ouEstZero x = case x of
(0,0) -> "Gauche et droite"
(0,_) -> "Gauche"
(_,0) -> "Droite"
_ -> "Nul part"
|
Ici, on voit une nouvelle façon de construire des motifs : on peut utiliser _ à l'intérieur de structures plus compliquées, pour dire qu'on ne se soucie pas d'une partie de cette structure. Donc le motif
(0,_)
correspond à toutes les paires donc le premier élément est 0.
On peut aussi utiliser le filtrage de motif pour décomposer une paire.
Code : Haskell | sommePaire t = case t of
(x,y) -> x+y
|
Quand on met un nom de variable dans un motif, cela ne signifie pas que cette partie du motif doit être égale à la variable. Un nom de variable se comporte plutôt comme un _, c'est-à-dire qu'il correspond à tout, mais en plus, dans l'expression à droite du motif, cette variable vaudra ce qu'il y avait à sa place dans le motif.
Par exemple, si on filtre la valeur
(0,7)
avec le motif et le résultat
(0,x) -> x+1
, on aura
x=7
donc on obtiendra 8.
On peut combiner toutes ces idées pour créer des fonctions plus compliquées. Cette fonction renvoie le premier élément non nul d'une paire, ou 0.
Code : Haskell | premierNonNul t = case t of
(0,0) -> 0
(0,y) -> y
(x,0) -> x
(x,y) -> x
|
On remarque que certains motifs se recoupent.
Par exemple, les cas (0,0) -> 0 et (0,y) -> y peuvent se réécrire avec un seul motif (0,y) -> y
De même, on peut remplacer les cas (x,0) -> x et (x,y) -> x par un seul cas, (x,_) -> x
On obtient un code avec seulement deux cas :
Code : Haskell | premierNonNul t = case t of
(0,y) -> y
(x,_) -> x
|
On ne peut pas mettre deux fois la même variable dans un motif (donc il est impossible de faire un motif
(x,x)
). Dans chaque cas, les valeurs renvoyées doivent être du même type.
Style déclaratif
Le filtrage de motif est un outil puissant, et on se rend compte qu'on fait très souvent un filtrage sur les arguments de la fonction.
Quand on doit prendre en compte la valeur de plusieurs arguments, le filtrage finit par donner des choses assez peu claires. Ici, on prend comme exemple une version de
premierNonNul qui prend deux arguments au lieu de prendre une paire de nombres :
Code : Haskell | premierNonNul x y = case (x,y) of
(0,y) -> y
(x,_) -> x
|
On doit construire une paire avec les deux arguments, ce qui finit par donner des codes pas très naturels.
Code : Haskell | premierNonNul 0 y = y
premierNonNul x _ = x
|
On préfère en général écrire le filtrage de cette façon, quand c'est possible.
Il est aussi possible de remplacer dans certains cas if par des gardes :
Code : Haskell | signePremier (x,_)
| x > 0 = "Positif"
| x < 0 = "Negatif"
| otherwise = "Nul"
|
Les gardes permettent d'exécuter du code différent suivant des conditions : si le motif correspond, l'expression correspondant à la première garde qui renvoie True est exécutée. La garde
otherwise permet de prendre en compte tous les cas pas encore traités (en réalité,
otherwise est une constante qui vaut
True). Il ne faut pas mettre de signe égal entre le motif et les gardes, sous peine de récolter une erreur de syntaxe.
n-uplets
Vous avez déjà vu les paires. Mais en fait, ce ne sont qu'un exemple d'un type de données plus général : les n-uplets. Les paires sont des n-uplets à 2 éléments, mais on peut écrire des n-uplets avec plus d'éléments.
Par exemple
(1,2,3,True). On utilise la même notation pour le filtrage de motif sur les n-uplets que pour les paires.
Cependant,
fst (1,2,3,True)
donne une erreur de type : les fonctions sur les n-uplets ne fonctionnent que pour des n-uplets de taille fixée. Mais vous pouvez, comme exercice, coder les fonctions
fst3,
snd3 et
thr3 qui permettent d'obtenir respectivement le premier, deuxième et troisième élément d'un triplet en utilisant le filtrage de motif.
Solution :
Secret (cliquez pour afficher)Code : Haskell | fst3 (a,_,_) = a
snd3 (_,b,_) = b
thr3 (_,_,c) = c
|
Si vous lisez des articles en anglais sur Haskell, les n-uplets sont appelés
tuples.
Définir des valeurs intermédiaires
Parfois il peut être utile dans une fonction de définir des valeurs intermédiaires.
Par exemple, on veut créer une fonction qui donne le nombre de racines réelles d'un polynôme du second degré (de la forme

). On sait que le discriminant est donné par

, et que s'il est positif, il y a deux racines réelles, s'il est nul, il y en a une, et s'il est négatif, il n'y en a pas.
Donc on peut penser notre fonction comme ceci : on calcule d'abord le discriminant, puis on regarde son signe pour donner le nombre de racines.
Pour faire cela, on a besoin de définir une variable locale à notre fonction. Il y a deux façons de faire ça.
let ... in ...
La première méthode est d'utiliser
let
. On l'utilise ainsi :
let variable = valeur in expression
.
Par exemple, on pourrait coder notre fonction
nombreDeRacines ainsi :
Code : Haskell | nombreDeRacines a b c = let delta = b^2 - 4*a*c in
if delta > 0 then 2
else if delta == 0 then 1
else 0
|
where
On peut aussi déclarer une variable locale avec
where
.
Par exemple :
Code : Haskell | nombreDeRacines' a b c = if delta > 0 then 2
else if delta == 0 then 1
else 0
where delta = b^2 - 4*a*c
|
On peut aussi déclarer plusieurs variables avec un seul where, comme dans cet exemple qui ne fait rien d'utile :
Code : Haskell | diffSommeProd a b = produit - somme
where produit = a*b
somme = a+b
|
where est sensible à l'indentation ! Il doit toujours être plus indenté que le début de la déclaration de la fonction.
Un peu d'exercice ?
Il est temps de mettre en pratique ce que vous avez appris. Ces exercices ne sont pas corrigés, mais vous pouvez tester votre code : s'il marche, c'est bon signe. Une bonne habitude à prendre est d'essayer toujours de trouver les cas qui font que le code ne marche pas.
- Des fonctions myMin et myMax qui prennent chacune deux arguments et renvoient respectivement le minimum et le maximum des deux arguments
- À partir de ces fonctions, codez une fonction qui donne le minimum ou le maximum de 4 nombres
- En utilisant myMin et myMax, codez une fonction bornerDans qui prend trois arguments et renvoie le troisième argument s'il est dans l'intervalle formé par les deux premiers, ou renvoie la borne de l'intervalle la plus proche.
Exemples:
Code : Haskell | bornerDans 5 7 6 = 6 -- dans l'intervalle
bornerDans 5 7 4 = 5 -- trop petit
bornerDans 5 7 9 = 7 -- trop grand
|
- Codez une fonction qui prend trois arguments et dit si le troisième argument est dans l'intervalle fermé formé par les deux premiers arguments (on considèrera que le premier argument est inférieur ou égal au deuxième)
- En n'utilisant qu'une seule comparaison, codez une fonction qui prend une paire de nombre et renvoie cette paire triée
- Codez une fonction qui prend deux vecteurs représentés par des paires de nombres, et donne la somme de ces deux vecteurs
- Codez une fonction qui prend un vecteur et renvoie sa norme
- Codez une fonction qui prend un nombre et un vecteur, et renvoie le produit du vecteur par ce nombre
- Codez une fonction qui prend deux vecteurs et renvoie le produit scalaire de ces deux vecteurs