Définir des fonctions
Retour au sommaire
Introduction
- SourceDans 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
- SourceVariables dans un fichier et chargement
Créez un fichier nommé declaration.hs avec pour contenu :
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.
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.
foo = 42 foo = 1337
Si on définit deux fois une variable, le compilateur se plaint :
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 :
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 :
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.
double perimetreCercle(double r) { return 2*pi*r; }
À part les indications de type, la différence principale, c'est qu'un 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 :
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é ?
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é ?
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.
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 :
{- 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
- SourceCette 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 :
==: 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 :
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)
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.
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 :
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
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 :
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.
signe x = if x >= 0 then "Positif" else "Negatif"
On pourrait écrire ce code sur plusieurs lignes :
signe x = if x >= 0 then "Positif" else "Negatif"
Mais on ne peut pas écrire ça :
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 :
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.
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 :
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.
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.
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 :
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 :
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.
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 :
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 :
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 a*x²+b*x+c). On sait que le discriminant est donné par b²-4*a*c, 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 :
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 :
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 :
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 qui 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:
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
Plus de filtrage de motif
- SourceIl vous reste encore quelques trucs intéressants à voir sur le filtrage de motif.
Des types somme
Maybe
Une valeur de type Maybe peut être construite de deux manières différentes : avec le constructeur Nothing qui ne prend pas d'argument, et avec le constructeur Just qui prend un argument. Par exemple, Just 5 est une valeur de type Maybe.
On peut voir ça comme une sorte de conteneur qui peut soit contenir un élément, soit rien du tout. Cela peut servir pour une opération qui peut échouer, par exemple rechercher dans un annuaire : soit le nom est dans l'annuaire, alors on donne le numéro (Just numero), soit le nom n'y est pas et on ne renvoie pas de résultat (Nothing).
Pour manipuler des valeurs de type Maybe, il faut utiliser le filtrage de motif. Les constructeurs peuvent être utilisés dans un motif. Par exemple, cette fonction renvoie False si la valeur qu'on lui donne est Nothing, True sinon :
maybeToBool Nothing = False maybeToBool (Just _) = True
On ne peut pas utiliser autre chose que des constructeurs dans un filtrage de motif : le code suivant n'est pas correct parce que just2 n'est pas un constructeur.
just2 x = Just (Just x)) test x = case x of just2 _ -> true Nothing -> False
À part ça, les constructeurs peuvent s'utiliser comme des fonctions normales. Le nom d'un constructeur commence toujours par une majuscule, celui d'une fonction par une minuscule.
Une fonction bien pratique quand on travaille avec des valeurs de type Maybe, c'est une fonction qui renvoie la valeur à l'intérieur du constructeur Just ou une valeur par défaut si c'est Nothing. On peut coder la fonction comme ceci :
defaultVal _ (Just x) = x defaultVal x Nothing = x
Utiliser Maybe pour la gestion d'erreurs
Maintenant, vous allez voir comment utiliser Maybe pour faire de la gestion d'erreur. Le problème qui se pose, c'est que certaines opérations, comme une division par 0, peuvent faire planter le programme. Pour éviter cela, il faut vérifier avant chaque division si on divise par un nombre égal à 0, et réagir de façon appropriée dans ce cas-là. Si on fait tout ce traitement à la main, le code devient très lourd dès qu'il y a plus de quelques opérations dans le calcul. Pour régler ce problème, on peut utiliser des nombres normaux, et rajouter une valeur possible qui indique une erreur de calcul. Cela correspond très bien à ce qu'on peut faire avec Maybe. Si un de deux arguments de l'opération qu'on souhaite faire est indéfini, le résultat est indéfini lui aussi (donc Nothing), sinon on applique l'opération normale , en n'oubliant pas de remettre le résultat dans Just, ou on renvoie Nothing pour indiquer une erreur. Pour cet exemple, on va juste coder les opérations mathématiques de base : plus, moins, fois, divise. On va commencer par l'opération plus : on veut que si un des deux arguments est Nothing, la fonction renvoie Nothing :
plus Nothing _ = Nothing plus _ Nothing = Nothing
Maintenant, on gère le cas où les deux arguments sont définis :
plus (Just a) (Just b) = Just (a + b)
On fait la même chose pour moins et fois (le code est presque le même) :
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)
Maintenant, il ne reste plus qu'à coder la division. Comme pour les autres opérations, on gère le cas où un des arguments est indéfini :
divise Nothing _ = Nothing divise _ Nothing = Nothing
Ensuite, si c'est une division par 0, on renvoie Nothing, sinon on renvoie le résultat :
-- la division par 0 donne un résultat indéfini divise _ (Just 0) = Nothing divise (Just a) (Just b) = Just (a / b)
Et voilà ! On a maintenant un moyen de faire des opérations sans risquer de division par 0. Enregistrez le code et chargez-le dans ghci. On va d'abord définir quelques nombres, puis faire des opérations avec.
*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
On a donc bien un code qui marche. Les noms des opérations ne sont pas très pratiques à entrer, mais vous verrez bientôt comment redéfinir les opérations mathématiques (+,-,*,/,...).
Finalement, on remarque que l'on a répété beaucoup trop de fois une partie du code :
plus Nothing _ = Nothing plus _ Nothing = Nothing
De plus, si la définition de divise est légèrement différente des autres, les définitions de plus, moins, fois ne différent que par leur nom et l'opérateur qui est utilisé. La gestion d'erreurs avec Maybe demande donc beaucoup trop de répétition, mais vous verrez dans le chapitre sur les monades qu'il est possible d'écrire ce code de façon beaucoup plus courte, sans répétition.
Either : un choix
Either est un type qui ressemble à Maybe. Il y a deux constructeurs pour ce type : Left a et Right b. L'avantage, c'est que les deux côtés peuvent être de types différents.
Either est souvent utilisé pour la gestion d'erreur : une valeur Left indique une erreur, une valeur Right indique que tout s'est bien passé, l'avantage par rapport à Maybe étant qu'on peut ajouter une information utile avec l'erreur, comme un message.
Par exemple, si on ajoutait l'opération "racine carrée" à nos fonctions sécurisées plus haut, les erreurs peuvent venir de deux endroits différents, et on pourrait avoir envie de savoir d'où. Ce qu'il faudrait, c'est qu'en cas d'erreur, on obtienne la première erreur survenue.
On peut faire ça comme ceci (seuls le code pour plus et divise sont intéressants, les autres ressemblent beaucoup) :
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 / b)
Encore une fois, le problème de la gestion pénible des cas pourra être résolu grâce aux monades.
Manipuler des listes
En fait, les listes sont aussi définies de cette façon. Il y a deux constructeurs et vous les connaissez déjà : ce sont [], qui représente la liste vide, et : qui permet de construire une liste en rajoutant un élément au début d'une autre liste.
Toutes les fonctions que vous avez vu sur les listes peuvent donc se coder avec du filtrage de motif. Par exemple, le code de head pourrait être (on définit head' ici pour éviter d'avoir un conflit avec la fonction déjà définie dans le Prelude) :
head' (x:_) = x
Pour des raisons de précédence des opérateurs, il faut mettre le motif entre parenthèses quand on l'utilise comme argument d'une fonction (c'est en fait comme si on appelait la fonction). Maintenant, on va tester ce code : que se passe-t-il quand on lui donne la liste vide ?
Prelude> head' [1,2,3] 1 Prelude> head' [] *** Exception: maybeEither.hs:48:0-14: Non-exhaustive patterns in function head'
Notre filtrage de motif ne prend pas en compte tous les cas possibles. Quand on appelle la fonction avec des arguments qui ne correspondent à aucun des cas, on obtient une exception qui indique la provenance dans le fichier source (ici, la ligne où était la définition de head' dans le fichier), et la cause de l'erreur : ici, le fait que notre filtrage de motif ne soit pas exhaustif. On peut renvoyer des exceptions avec un message d'erreur personnalisé grâce à la fonction error. Par exemple, head'' donnera un message d'erreur plus utile :
head'' (x:_) = x head'' [] = error "Liste vide"
Comme exercice, vous pouvez essayer de coder quelques fonctions sur les listes que vous avez déjà vues : tail, null et une fonction pour renvoyer les deux premiers éléments, une fonction qui renvoie True si la liste contient deux éléments et False sinon.