Licence CC BY-SA

Définir des fonctions

Dans ce chapitre, vous n'allez plus uniquement utiliser ghci : la première partie va vous montrer comment écrire du code dans un fichier, et le charger dans ghci pour tester les fonctions définies dedans. Dans les deux parties suivantes, vous allez découvrir comment utiliser les conditions, mais aussi une technique très utilisée en Haskell : le filtrage de motif.

Déclarations dans un fichier

Variables dans un fichier et chargement

Créez un fichier nommé declaration.hs avec pour contenu :

1
reponse = 42

On va maintenant le charger. Pour cela, ouvrez une console, naviguez jusqu'au répertoire où se trouve votre fichier et lancez ghci.

1
2
3
4
5
Prelude>:l declaration.hs
[1 of 1] Compiling Main             ( declaration.hs, interpreted )
Ok, modules loaded: Main.
*Main> reponse
42

On a chargé le fichier avec la commande :l. ghci indique qu'il a réussi à charger le fichier. Maintenant, la variable reponse vaut 42.

Vous l'aurez compris, on déclare une variable comme ceci : nomVariable = valeur Comme quand on définit des variables dans ghci, on peut réutiliser le résultat des calculs précédents.

1
2
foo = 42
foo = 1337

Si on définit deux fois une variable, le compilateur se plaint :

1
2
3
4
5
6
7
8
Prelude> :l multi.hs
[1 of 1] Compiling Main             ( multi.hs, interpreted )

multi.hs:2:0:
    Multiple declarations of `Main.foo'
    Declared at: multi.hs:1:0
                 multi.hs:2:0
Failed, modules loaded: none.

ghci indique ici qu'il n'a pas pu charger le fichier car foo est défini deux fois: à la ligne 1 et à la ligne 2.

Définir des fonctions simples

On peut aussi déclarer des fonctions.

Un exemple vaut mieux qu'un long discours :

1
perimetreCercle r = 2 * pi * r

Cette ligne de code définit une fonction perimetreCercle, qui prend un argument r, et renvoie 2*pi*r.

Vous pouvez charger ce fichier dans ghci pour tester la fonction :

1
2
3
4
5
Prelude> :l fonction
Prelude> perimetreCercle 5
31.41592653589793
Prelude> 2*pi*5
31.41592653589793

Pour appeler la fonction, on utilise la même syntaxe que pour les fonctions prédéfinies. Ce qui se passe, c'est que le corps de la fonction est exécuté, avec dans la variable r qui correspond à l'argument la valeur de l'argument donnée quand on appelle la fonction. C'est pour ça que, à la place de perimetreCercle 5, on aurait très bien pu écrire 2*pi*5.

Si vous avez déjà programmé dans un langage comme le C, vous remarquerez que la définition d'une fonction en Haskell ressemble plus à la définition d'une fonction en maths qu'à la définition d'une fonction en C. $f(x)=2*\pi*x$

1
2
3
double perimetreCercle(double r) {
    return 2*pi*r;
}

À part les indications de type, la différence principale, c'est qu'une fonction C est une suite d'instructions, alors qu'une fonction Haskell est une expression (un calcul, un appel de fonction, …), et donc qu'il n'y a pas d'équivalent de return.

D'ailleurs, en Haskell, mettre plusieurs instructions dans une fonction n'aurait aucun sens, puisque les instructions d'avant n'auraient aucune influence sur l'exécution du programme (le seul moyen d'influencer l'exécution du programme serait par des effets de bords, comme la modification d'une variable globale, mais ceux-ci sont interdits en Haskell).

On peut aussi définir des fonctions prenant plusieurs arguments :

1
perimetreRectangle longueur largeur = 2.0*(longueur+largeur)

Cette fonction calcule le périmètre d'un rectangle : vous pouvez la tester dans ghci. On peut aussi réutiliser les fonctions déjà définies. Par exemple, sachant qu'un carré est un rectangle dont les côtés ont même longueur, comment calculeriez-vous le prérimètre d'un carré ?

1
2
perimetreRectangle longueur largeur = 2.0*(longueur+largeur)
perimetreCarre cote = perimetreRectangle cote cote

On définit le périmètre d'un carré de côté c comme le périmètre d'un rectangle de longueur c et de largeur c.

Pour recharger un fichier après l'avoir modifié, dans ghci, utilisez la commande :r

Maintenant, que se passerait-il si on chargeait ce code, où l'ordre des définitions est inversé ?

1
2
perimetreCarre cote = perimetreRectangle cote cote
perimetreRectangle longueur largeur = 2.0*(longueur+largeur)

La réponse est : rien. Le compilateur est capable de comprendre les définitions, même si elles font référence à des fonctions définies plus tard dans le fichier. On peut d'ailleurs faire la même expérience avec des variables qui dépendent l'une de l'autre.

Commentaires

Il est souvent utile de commenter son code, pour le rendre plus compréhensible. Deux types de commentaires sont disponibles en Haskell :

  • Les commentaires sur une ligne. Ils commencent par – et le commentaire continue jusqu'à la fin de la ligne.
1
reponse = 42 -- commentaire à propos de cette déclaration
  • Les commentaires sur plusieurs lignes. Ils commencent par {- et se terminent par -}. Ils peuvent même être imbriqués :
1
2
3
4
5
6
7
8
9
{-
Un commentaire sur plusieurs lignes
-}

variable = "test"

{-
un commentaire {- imbriqué. -} le commentaire continue -}
message = "ceci n'est pas dans un commentaire"

Conditions et filtrage de motif

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 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

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)

Définir des opérateurs: Appeller une fonction xor dans le code n'est pas toujours très pratique : ça demande plus de parenthèses et on voit moins facilement le sens du code. On pourrait écrire xor à la place, mais ça fait quelques caractères en plus. L'autre solution est de définir un opérateur. Ici, on définit l'opérateur |&, qui fait la même chose que la fonction xor.

1
2
a |& b = (a || b) && not (a && b)
test = True |& False

Pour définir un opérateur, il faut donc faire comme pour une déclaration de fonction, mais écrire le nom de l'opérateur au milieu des deux arguments. Le nom d'un opérateur ne doit pas comprendre de lettres ou de chiffres, et ne peux pas commencer par : (deux points). Si vous avez un doute sur la validité d'un nom, le mieux est de tester.

Utiliser if

Vous pouvez tester if dans ghci :

1
2
3
4
5
6
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

1
2
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 :

1
2
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.

1
signe x = if x >= 0 then "Positif" else "Negatif"

On pourrait écrire ce code sur plusieurs lignes :

1
2
3
signe x = if x >= 0
          then "Positif"
          else "Negatif"

Mais on ne peut pas écrire ça :

1
2
3
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 :

1
2
3
4
5
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.

1
2
3
4
5
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 :

1
2
3
4
5
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.

1
2
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.

1
2
3
4
5
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 :

1
2
3
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 :

1
2
3
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.

1
2
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 :

1
2
3
4
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 :

1
2
3
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 $ax^2+bx+c$). On sait que le discriminant est donné par $\Delta=b^2-4ac$, 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 :

1
2
3
4
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 :

1
2
3
4
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 :

1
2
3
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.

  1. Des fonctions myMin et myMax qui prennent chacune deux arguments et renvoient respectivement le minimum et le maximum des deux arguments
  2. À partir de ces fonctions, codez une fonction qui donne le minimum ou le maximum de 4 nombres
  3. 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:
1
2
3
bornerDans 5 7 6 = 6 -- dans l'intervalle
bornerDans 5 7 4 = 5 -- trop petit
bornerDans 5 7 9 = 7 -- trop grand
  1. 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)
  2. En n'utilisant qu'une seule comparaison, codez une fonction qui prend une paire de nombre et renvoie cette paire triée
  3. Codez une fonction qui prend deux vecteurs représentés par des paires de nombres, et donne la somme de ces deux vecteurs
  4. Codez une fonction qui prend un vecteur et renvoie sa norme
  5. Codez une fonction qui prend un nombre et un vecteur, et renvoie le produit du vecteur par ce nombre
  6. Codez une fonction qui prend deux vecteurs et renvoie le produit scalaire de ces deux vecteurs

Plus de filtrage de motif

Pour l'instant, vous n'avez vu que deux applications du filtrage de motif : soit vous prenez plusieurs valeurs d'un type pour lequel il n'y a qu'un seul choix (par exemple un couple d'entiers ne peut pas être autre chose qu'un couple), soit vous distinguez les valeurs d'un type qui peut en prendre plusieurs (par exemple, True ou False pour les booléens). Mais en fait, le filtrage de motifs permet de faire les deux choses à la fois.

Manipuler des listes

Vous avez vu au chapitre précédent que toutes les listes pouvaient être construites à l'aide de la liste vide [] et de l'opérateur : qui combine un élément et une liste pour former une nouvelle liste avec l'élément ajouté en premier. C'est pratique pour construire des listes, mais on peut aussi s'en servir pour les détruire, c'est à dire examiner ce qu'il y a à l'intérieur, par du filtrage de motif.

Par exemple, si on veut créer une fonction qui renvoie 0 si la liste vide, son premier élément sinon, on peut la coder en utilisant un filtrage (puisqu'elle ne peut renvoyer qu'un type, cette fonction ne marche qu'avec des listes de nombres) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- Version sans filtrage
premier l = if null l then 0 else head l

-- Version avec filtrage
premier' l = case l of
    [] -> 0
    x:_ -> x

-- Ou même :
premier'' [] = 0
premier'' (x:_) = x

Dans les deux versions avec filtrage, on distingue les deux formes possibles pour la liste donnée en argument. Les parenthèses sont nécessaires dans la deuxième version avec filtrage, comme pour l'application de fonction : premier'' x:_ serait compris comme (premier'' x):_.

On peut aussi profiter d'une syntaxe spéciale pour créer un motif qui correspond à une liste de longueur connue, par exemple dans ce code qui additionne les deux premiers éléments d'une liste :

1
2
3
addition [] = 0
addition [x] = x
addition (x:y:_) = x + y

De cette façon, on peut recoder quelques fonctions sur les listes, par exemple la fonction head qui retourne le premier élément d'une liste (mais vous pourriez aussi code tail ou null de la même façon) :

1
head' (x:_) = x

Quelque chose pourrait vous choquer dans ce code : il ne traite pas tous les cas possibles. Qu'est-ce qui se passe si on donne la liste vide à la fonction ? Rien de bien : on obtient une erreur.

1
*** Exception: <interactive>:1:4-18: Non-exhaustive patterns in function head'

Par défaut, GHC ne vérifie pas que le filtrage traite tous les cas possibles, mais vous pouvez activer un avertissement avec l'option -fwarn-incomplete-patterns. L'erreur qu'on obtient dans le cas de la liste vide n'est pas très claire, mais il est possible de renvoyer son propre message d'erreur avec la fonction error :

1
2
head' [] = error "Liste vide"
head' (x:_) = x

Le filtrage de motif est quelque chose de très pratique (essayez de coder "addition" sans : le code serait beaucoup moins clair), mais pas magique : on ne peut pas utiliser n'importe qu'elle fonction dans un filtrage. Par exemple, on aimerait bien créer une fonction qui renvoie le prénom d'une personne lorsqu'on lui donne une chaîne de la forme "Bonjour <prénom>!". Une chaîne de caractères est une liste de caractères, donc on peut utiliser les même fonctions dessus, dont la fonction ++ qui concatène deux listes.

1
2
3
prenom msg = case msg of
    "Bonjour " ++ prenom ++ "!" -> prenom
    _ -> "Anonyme"

Mais ce genre de code ne marche pas : on ne peut pas utiliser n'importe quelle fonction dans un filtrage de motif. En fait, les seules choses qu'on a le droit d'utiliser sont les constructeurs du type qu'on cherche à filtrer. Ils correspondent à la façon dont le type a été défini, et aussi à la représentation en mémoire du type. Pour les listes, les constructeurs sont : et []. Pour les booléens, ce sont True et False, et pour les nombres, c'est n'importe quel nombre : par exemple, on peut utiliser 0 comme un motif.

Gestion des erreurs

Le filtrage de motifs et les types sommes (pour lesquels on a plusieurs constructeurs possibles) ne servent pas qu'avec les listes. Par exemple, ils servent pour la gestion des erreurs.

Maybe : pour représenter l'absence de valeur

Le type Maybe a deux constructeurs : Nothing qui ne prend pas de paramètre, et Just, qui prend une valeur en paramètre. Par exemple, on peut écrire Nothing, Just 42 ou même Just Nothing et Just (Just 42) (on a alors un Maybe qui contient lui-même un Maybe).

On l'utilise souvent pour la valeur de retour d'une fonction qui peut échouer : par exemple, si on a une annuaire, et une fonction qui recherche le numéro de téléphone d'une personne à partir de son nom, elle renverrait Nothing si la personne n'est pas dans l'annuaire, et Just (numéro) si la personne est dans l'annuaire.

Pour manipuler ces valeurs, on utilise le filtrage de motif. Par exemple, ici, on a une fonction demanderLeNomDuChat qui appelle un numéro, et renvoie le nom du chien. Mais cette fonction peut échouer : par exemple, la personne qu'on appelle peut ne pas répondre. Elle doit donc renvoyer une valeur de type Maybe. Maintenant, si on veut faire une fonction qui renvoie (peut-être) le nom du chat d'une personne à partir de son nom, on récupère d'abord le numéro de téléphone dans Maybe. Pour vérifier si on a vraiment un numéro de téléphone, on utilise le filtrage :

1
2
3
nomDuChat nom = case numeroDeTelephone nom of
  Nothing -> Nothing
  Just numero -> demanderLeNomDuChat numero

En fait, Nothing peut être vu comme la valeur null qui est disponible dans certains langages de programmation, et qui sert aussi à indiquer l'absence de valeur intéressante. Cependant, il y a une différence importante : les types ne portent pas de valeur nulle par défaut, mais seulement lorsqu'on les "enveloppe" dans Maybe. L'avantage, c'est qu'on ne peut pas accidentellement passer la valeur nulle à une fonction qui ne l'attend pas, puisque le type attendu est différent. C'est pour ça qu'il faut faire bien attention à ne pas oublier Just. Par exemple, le code ci-dessous est incorrect :

1
2
3
4
5
-- Renvoie l'adresse ip du serveur d'un site web
adresseIP "www.siteduzero.com" = "92.243.25.239"
adresseIP "progmod.org" = "178.33.42.21"
adresseIP "google.fr" = "66.249.92.104"
adresseIP _ = Nothing

Pourquoi ? Parce que dans presque tous les cas, on renvoie une valeur simple, qui ne peut jamais valoir Nothing, alors qu'on renvoie Nothing si le site n'est pas trouvé. Les types de retour ne correspondent pas, donc ce code est invalide. Il faut penser à mettre à chaque fois les valeurs définies dans Just :

1
2
3
4
5
-- Renvoie l'adresse ip du serveur d'un site web
adresseIP "www.siteduzero.com" = Just "92.243.25.239"
adresseIP "progmod.org" = Just "178.33.42.21"
adresseIP "google.fr" = Just "66.249.92.104"
adresseIP _ = Nothing

Une dernière chose : pour rechercher dans un "annuaire" représenté par une liste de couples (clé, valeur), vous pouvez utiliser la fonction lookup : lookup cle liste renvoie Just valeur si la clé est trouvée dans la liste, et Nothing sinon. Exemples :

1
2
3
4
5
6
Prelude> let ips = [("www.siteduzero.com","92.243.25.239"),
    ("progmod.org", "178.33.42.21"), ("google.fr", "66.249.92.104")]
Prelude> lookup "www.siteduzero.com" ips
Just "92.243.25.239"
Prelude> lookup "reddit.com" ips
Nothing

Exemple : des fonctions mathématiques

Vous savez surement qu'il y a des opérations qu'on n'a pas le droit de faire : par exemple, on ne peut pas diviser par 0. Mais il arrive souvent qu'on fasse des calculs avec des nombres dont on ne peut pas être sûr à l'avance si ils sont égaux à 0 ou pas. Par défaut, ce genre d'erreurs donne une exception qui arrête l'éxécution du programme (il y a des moyens de les intercepter, mais vous ne verrez pas ça tout de suite). Mais on pourrait adopter une autre solution : puisque la division peut échouer, pourquoi ne pas lui faire renvoyer une valeur dans Maybe ?

Par exemple, on pourrait faire cela :

1
2
3
4
-- On utilise la fonction div, qui correspond à la division euclidienne
-- On la met entre ` ` pour l'utiliser comme un opérateur
divise _ 0 = Nothing
divise x y = Just (x `div` y)

Ce code marche très bien :

1
2
3
4
*Main> 12 `divise` 5
Just 2
*Main> 12 `divise` 0
Nothing

Mais, quand on essaye de faire des calculs compliqués, on a des problèmes, puisque les autres opérations n'attendent pas "peut-être un nombre", mais un nombre normal. Il y a alors deux solutions : soit on utilise à chaque fois case sur le résultat de divise pour distinguer les différents cas, soit on adapte les opérations mathématiques pour prendre des valeurs dans Maybe comme arguments. C'est cette deuxième approche qui nous intéresse ici.

On va commencer par la fonction plus :

1
2
3
4
5
-- Cas faciles : lorsqu'un argument est "Nothing", on renvoie Nothing pour propager l'erreur
plus Nothing _ = Nothing
plus _ Nothing = Nothing
-- Le cas intéressant : pas d'erreur
plus (Just a) (Just b) = Just (a + b)

On teste la fonction :

1
2
3
4
*Main> Nothing `plus` Just 1
Nothing
*Main> Just 2 `plus` Just 3
Just 5

On peut faire de même pour plus et fois qui ne peuvent pas non plus échouer :

1
2
3
4
5
6
7
moins Nothing _ = Nothing
moins _ Nothing = Nothing
moins (Just a) (Just b) = Just (a - b)

fois Nothing _ = Nothing
fois _ Nothing = Nothing
fois (Just a) (Just b) = Just (a * b)

Il ne reste plus qu'à coder la division, où on doit aussi vérifier si on ne divise pas par Just 0:

1
2
3
4
5
6
-- Propager les erreurs
divise Nothing _ = Nothing
divise _ Nothing = Nothing
-- la division par 0 donne un résultat indéfini
divise _ (Just 0) = Nothing
divise (Just a) (Just b) = Just (a `div` b)

On peut ensuite faire quelques calculs pour tester :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
*Main> let n0 = Just 0
*Main> let n1 = Just 1
*Main> let n2 = Just 2
*Main> let undef = Nothing
*Main> n1 `plus` n2
Just 3
*Main> n1 `fois` undef
Nothing
*Main> n2 `divise` n1
Just 2
*Main> n2 `divise` n0
Nothing
*Main> (n2 `plus` n1) `divise` (n1 `moins` n1)
Nothing

Et voilà, on a maintenant un moyen d'effectuer des opérations mathématiques sans faire de division par zéro, et sans vérifier à chaque fois par quoi on divise. Les notations ne sont pas très pratique, mais vous verrez bientôt comment redéfinir les opérateurs mathématiques (+,-,*,div, …).

On remarque aussi qu'une partie du code est répétée à chaque fois, et que les définitions se ressemblent toutes. La gestion des erreurs demande beaucoup de code supplémentaire, mais vous verrez dans le chapitre sur les monades qu'on peut faire la même chose avec un code beaucoup plus court et sans répétition.

Either : un choix

Either est un type qui ressemble beaucoup à Maybe. Il y a deux constructeurs, qui prennent chacun une valeur : Left et Right. On peut par exemple les utiliser pour représenter un choix : soit un marchand de glaces livre à domicile, et dans ce cas on renvoie une valeur Left adresse, soit il fait des glaces à emporter, et dans ce cas on renvoie Right (adresse du marchand). Le mangeur de glace peut alors faire un filtrage de motif pour choisir d'aller acheter une glace, ou de la commander par téléphone.

Mais on utilise aussi souvent Either pour gérer des erreurs : si il n'y a pas d'erreur, on renvoie Right (valeur demandée), sinon Left (détails de l'erreur) (utiliser Left pour indiquer l'échec et Right la réussite est une convention presque toujours respectée). L'avantage, c'est qu'on peut de cette façon donner plus de détails sur l'erreur qu'avec un simple Nothing. On pourrait adapter nos fonctions mathématiques pour utiliser Either :

1
2
3
4
5
6
7
8
plus (Left erra) _ = Left erra 
plus _ (Left errb) = Left errb
plus (Right a) (Right b) = Right (a + b)

divise (Left erra) _ = Left erra
divise _ (Left errb) = Left errb
divise (Right _) (Right 0) = Left "Division par 0"
divise (Right a) (Right b) = Right (a `div` b)

Encore une fois, il est pénible de gérer tous les cas, mais les monades nous permettront de le faire sans efforts.


Ce chapitre est terminé. Si vous ne deviez n'en retenir qu'une chose, le filtrage de motif est le point le plus important : il est très utilisé en Haskell et permet de faire beaucoup de manipulations sur les types impossibles autrement.