Fonctions¶
Définition et appel de fonctions¶
Définitions¶
Dans un premier temps, on écrit les fonctions dans un fichier .hs
qu’on charge avec :load
après avoir lancé le REPL en tapant ghci
.
On utilise deux sortes de définition: l’équation et la signature.
Les fonctions sont définies par une équation comme dans
inc n = n + 1
.On peut aussi explicitement typer la fonction (signature) :
inc :: Integer -> Integer
plus généralement
inc :: Num a => a -> a
En général, il n’est pas nécessaire de donner cette signature car elle est automatiquement déduite, mais c’est une forme de documentation utile.
Lambda-fonctions¶
Les équations sont en fait une manière élégante de définir des fonctions
inc x = x + 1
add x y = x + y
équivalente à l’utilisation de lambda-fonctions (fonctions anonymes, où la barre oblique rappelle le plus long des deux traits d’un lambda).
inc = \x -> x + 1
add = \x y -> x + y
Appels de fonctions¶
En Haskell, il n’y a pas besoin de parenthèses pour appeler une fonction. Tous les arguments sont listés après la fonction.
Schéma général : func arg1 arg2 arg3 ...
Exemple 1 : func arg1 (add 2 5) arg ...
.
Le deuxième argument est add 2 5
.
Exemple 2 : func arg1 add 2 5 arg ...
.
Les arguments 2, 3, 4 sont respectivement add, 2
et 5
.
Opérateurs et fonctions¶
Les opérateurs binaires tels que :
ou ++
ne sont rien d’autre que des fonctions à deux paramètres.
Ce sont généralement des signes qui sont placés entre les deux arguments :
1 : [2,3]
. Mais on peut tout aussi bien les entourer de parenthèses pour les utiliser comme des fonctions :
(:) 1 [2,3]
.
Inversement, les fonctions peuvent être placées entre quotes penchées pour les utiliser comme des opérateurs infixes :
take 2 "abc"
est equivalent à 2 `take` "abc"
.
Premières définitions¶
Ajoutez les définitions de
inc
etadd
dans un fichier appeléex.hs
.Dans GHCI, chargez votre fichier avec
:load ex.hs
.Que font les appels suivants ?
add 2 5
3 `add` 6
4 + 7
(+) 4 7
inc 3
(add 1) 3
add (1 3)
Distinction des cas¶
Stratégie récursive¶
De nombreuses fonctions nécéssitent la répétition d’un traitement.
Comme il n’y a pas de boucles, on les définit récursivement.
Il est donc nécessaire de distinguer le cas de base, du cas récursif.
soit par des gardes (guards) pour distinguer des intervalles de valeurs.
soit par une case-expression pour brancher selon la structure d’une valeur.
Gardes¶
Les équations peuvent contenir des gardes.
Etant donnée
f arg = expr
, l’appelf val
sera toujours evalué àexpr
,tandis qu’avec
f arg | cond = expr
, l’appelf val
est évalué àexpr
si et seulement sicond
est vrai, oùcond
est une expression booléenne impliquant l’argument, comme par exemplearg > 0
.
Gardes multiples¶
Plusieurs gardes peuvent être chaînés. Dans ce cas, ils sont testés les uns après les autres.
sign x | x > 0 = 1 | x == 0 = 0 | otherwise = -1
sign x
| x > 0 = 1
| x == 0 = 0
| otherwise = -1
sont la traduction Haskell de :
Remarque : layout¶
Haskell utilise l’alignement en colonne pour accroître la lisibilité. Une ligne indentée plus à droite que la précédente est la suite de la ligne précédente.
Ci-dessous, il y a une erreur car aucune expression ne peut commencer par |
.
sign x
| x > 0 = 1
| x == 0 = 0
| otherwise = -1
2:1: parse error on input ‘|’
Remarque : commentaires¶
-- Un commentaire en une ligne commence avec deux tirets.
{- Un commentaire sur plusieurs lignes peut être contenu dans
un bloc de cette façon.
-}
Notez que dans les fichiers d’extension .lhs
, seules les lignes
précédées par >
et espace (Bird style) sont considérées comme
du code Haskell.
défi 1. addElemInList
¶
A l’aide de gardes, définissez la fonction
addElemInList :: a -> Int -> [a] -> [a]
qui ajoute un élément donné, un nombre de fois donné, dans une liste donnée.
*Main> addElemInList 1 3 []
[1,1,1]
*Main> addElemInList 'a' 2 "bb"
"aabb"
*Main>
Case expression¶
Une case-expression fournit le moyen de distinguer plusieurs cas selon une valeur et sa structure.
myLen :: [a] -> Integer
myLen lst = case lst of
[] -> 0
(x:xs) -> 1 + myLen xs
dans le cas où
lst
est une liste vide,myLen lst
est évaluée en0
,dans le cas où
lst
est composé d’un élémentx
en tête d’une listexs
(possiblement vide)myLen lst
est évaluée en1 + myLen xs
.
Tous les membres de gauche (devant les flèches) et tous les membres de droite (derrière) doivent avoir le même type,
respectement [a]
et Integer
dans cet exemple.
Pattern matching¶
Dans une case-expression les membres de gauche sont des motifs (patterns) avec lesquels est mise en correspondance (matching) la valeur associée au paramètre.
Une mise en correspondance avec un motif peut soit
produire une erreur, si la valeur contient une erreur ;
échouer, si la valeur ne correspond pas au motif (
[1,2,3] != []
). Dans ce cas, le motif suivant est essayé et si tout échoue, une erreur se produit ;réussir sinon (
[1,2,3] = 1:[2,3]
, correspondant au motifx:xs
oùx=1
etxs=[2,3]
). Dans ce cas, le membre de droite est évalué et retourné comme résultat de l’appel.
défi 2. dupli
¶
A l’aide d’une case expression, définissez la fonction
dupli :: [a] -> [a]
qui duplique les éléments d’une liste donnée.
*Main> dupli [1,2,3]
[1,1,2,2,3,3]
*Main> dupli "abc"
"aabbcc"
Sucre syntaxique¶
Dans un programme Haskell, une fonction est assez souvent définie par une série d’équations car
f x1 x2 ... xk = case (x1, ..., xk) of
(p11, ..., p1k) -> e1
...
(pn1, ..., pnk) -> en
est équivalent à
f p11 ... p1k = e1
...
f pn1 ... pnk = en
Mais une case expression peut être utilisée aussi dans une expression plus complexe.
Fonctions récursives¶
défi 3. compress
¶
Définissez la fonction
compress :: (Eq a) => [a] -> [a]
qui supprime les copies consécutives des éléments d’une liste.
*Main> compress "aaaabccaadeeee"
"abcade"
Let-expression¶
Parfois on a besoin de localement se référer à une valeur intermédiaire.
Une let-expression (let var = expr1 in expr2
) peut
être utilisée partout où une expression est requise.
(let x = 2 in x*2) + 3
Dans certains cas (REPL ou notation do
), un
let-statement (let var = expr
) peut
être utilisé.
Prelude> let x = 2
Prelude> x*2 + 3
7
Clause where
¶
La clause where permet de partager une variable entre des parties d’une définition qui ne forme pas syntaxiquement une expression.
f x
| cond x = a
| otherwise = g a
where
a = w x
défi 4. encode
¶
Définissez la fonction
encode :: (Eq a) => [a] -> [(Int, a)]
qui encode une liste donnée de façon à ce que toute suite
de n
éléments égaux à x
soit remplaçée par le tuple (n,x)
.
*Main> encode "aaaabccaadeeee"
[(4,'a'),(1,'b'),(2,'c'),(2,'a'),(1,'d'),(4,'e')]
Astuce : on suppose qu’on connaît le résultat pour une liste xs
.
Comment construire incrémentalement le résultat pour x:xs
?
Modèles de traitement¶
Récursion¶
Vous imaginez peut-être qu’on définit toujours une fonction de manière récursive en Haskell.
Mais en fait, les programmeurs expérimentés ne le font que rarement.
En pratique, il y a des modèles de traitement très fréquents qu’implémentent certaines fonctions.
Le programmeur peut alors rester sur un plus haut niveau en combinant ces fonctions avec l’opérateur de composition
.
(f (g x)
est équivalent à(f . g) x
).
Prélude¶
Le Prélude est un module doté d’un grand nombre de fonctions standards. Il est implicitement importé dans chaque programme Haskell.
Certaines fonctions ont déjà été mentionnées :
length, head, tail, take, drop, init, last, reverse, elem
, etc.Nous allons en voir d’autres maintenant :
map
,filter
,foldl
,foldr
,foldl'
, etc.
filter
¶
Un premier modèle de traitement consiste à ne conserver d’une liste que les éléments vérifiant une certaine condition.
filter :: (a -> Bool) -> [a] -> [a]
Le premier argument est un prédicat, c’est-à-dire une fonction qui
prend un a
et qui répond oui ou non. Le second argument est une
liste d’élément de type a
.
Tous les éléments pour lesquels la réponse est positive sont
dans la liste résultante (de taille plus petite ou égale).
Par exemple, l’expression filter (\x -> x < 10) [9,10,11,12]
est évaluée
à [9]
.
défi 5 : filter
¶
En utilisant les fonctions filter
et length
, donnez l’expression
qui retourne le nombre de ‘a’ dans la chaîne de caractère “aaaabccaadeeee”.
map
¶
Un deuxième modèle de traitement consiste à appliquer une même opération à tous les éléments d’une liste.
map :: (a -> b) -> [a] -> [b]
Le premier argument est une fonction qui prend un a
et retourne un b
.
Celle-ci est appliquée à tous les éléments de type a
d’une liste,
ce qui donne une liste d’éléments de type b
(de même taille).
En ayant défini inc x = x + 1
, l’expression map inc [1,2,3]
est évaluée
en [2,3,4]
.
Applications partielles¶
add :: Integer -> Integer -> Integer
add x y = x + y
add
peut être évaluée avec deux arguments ou un seul,
dans ce dernier cas, il s’agit d’une application partielle.
add 2 3 :: Integer -- resultat de l'addition 2 + 3
add 1 :: Integer -> Integer -- fonction qui ajoute 1
Donc on peut aussi écrire map (add 1) [1,2,3]
Sections¶
En Haskell, une section est l’application partielle d’un opérateur infixe (qui s’écrit entre les deux opérandes). Par exemple :
(+)
\(\equiv\)\x y -> x+y
(x+)
\(\equiv\)\y -> x+y
(+y)
\(\equiv\)\x -> x+y
inc = (+1)
add = (+)
Donc on peut aussi écrire map (+1) [1,2,3]
défi 6 : map
¶
En utilisant la fonction addElemInList
et map
, donnez l’expression qui
transforme le codage [(4,'a'),(1,'b'),(2,'c'),(2,'a'),(1,'d'),(4,'e')]
en
["aaaa","b","cc","aa","d","eeee"]
.
Définition de liste en intention¶
Il existe une syntaxe particulière permettant de définir des listes en intention (appelée list comprehension)
et qui remplace avantageusement l’utilisation de map
et filter
dans un grand nombre de cas.
[ f x | x <- xs, x < k ]
signifie “la liste de tous les f x
telle que x
vienne de la liste xs
et soit inférieur à k
”
et s’inspire de la notation mathématique
foldr
and co.¶
Un troisième modèle de traitement consiste à combiner les éléments d’une liste.
Il est connu sous le nom fold (ou reduce dans d’autres langages). Il y a plusieurs variantes en Haskell : foldr, foldl or foldl’
A partir d’une fonction binaire f
et d’une valeur initiale z
,
les fonctions foldr
et foldl
vont combiner les valeurs d’une liste,
en partant de la droite (r pour right) ou de la gauche (l pour left).
foldr f z [a,b,c]
\(\equiv\)a `f` (b `f` (c `f` z))
foldl f z [a,b,c]
\(\equiv\)((z `f` a) `f` b) `f` c
foldl'
est une variante plus efficace defoldl
foldr
en détail¶
foldr :: (a -> b -> b) -> b -> [a] -> b
La premier argument est une fonction qui combine un a
et un b
et retourne une valeur de type b
.
Le deuxième est une valeur initiale de type b
. Le troisième une liste de a
.
La fonction de combinaison va être d’abord appliquée à la valeur initiale z
et à l’élément de fin de liste (le plus à droite).
Le résultat sera ensuite combiné avec l’élément précédent et ainsi de suite.
La dernière application de la fonction de combinaison retournera la valeur finale.
défi 7 : foldr
¶
En utilisant la fonction foldr
, donnez l’expression qui transforme la liste
["aaaa","b","cc","aa","d","eeee"]
en la chaîne "aaaabccaadeeee"
Point-free style¶
Grâce à l’application partielle, comme pour inc = (+1)
,
il est possible de définir mySum
sans donner aucun argument :
mySum :: [Integer] -> Integer
mySum = foldr (+) 0
au lieu de
mySum lst = foldr (+) 0 lst
C’est le “point-free style” (où “point” réfère aux arguments) qu’adopte les développeurs expérimentés parce qu’il décrit ce qu’est une fonction, plutôt que ce qu’elle fait.
Défi bonus : définir quelques fonctions¶
Comme dans le Prélude, définissez les fonctions suivantes à l’aide de foldr
:
length :: [a] -> Int
sum :: Num a => [a] -> a
product :: Num a => [a] -> a
and :: [Bool] -> Bool
or :: [Bool] -> Bool
any :: (a -> Bool) -> [a] -> Bool
all :: (a -> Bool) -> [a] -> Bool
Conclusion¶
Capacités/Connaissances¶
Définir une fonction récursive sur des listes.
Expliquer le mécanisme du pattern matching et de l’évalution paresseuse
Tracer l’évaluation d’expressions données
Citer des exemples où une fonction est passée en argument à une autre fonction.
Utiliser à bon escient les fonctions
filter
,map
,foldr
oufoldl
, etc.Définir une fonction sur les listes par une syntaxe en extension ou par des fonctions usuelles.
défi 1. addElemInList
¶
addElemInList :: a -> Int -> [a] -> [a]
addElemInList x c lst
| c <= 0 = lst
| c > 0 = addElemInList x (c-1) (x:lst)
défi 2. dupli
¶
dupli :: [a] -> [a]
dupli lst = case lst of
(x:xs) -> x:x:(dupli xs)
[] -> []
défi 3. compress
¶
compress :: (Eq a) => [a] -> [a]
compress [] = []
compress [x] = [x]
compress (x:y:xs)
| x == y = compress (y:xs)
| otherwise = x:(compress (y:xs))
défi 4. encode
¶
encode :: (Eq a) => [a] -> [(Int, a)]
encode [] = []
encode (x:xs) = case res of
[] -> [(1,x)]
( (nb,elem) : ys)
| x == elem -> (nb+1,x):(ys)
| otherwise -> (1,x):res
where res = encode xs
défi 5 : filter
¶
Prelude> length (filter (\x -> x == 'a') "aaaabccaadeeee")
6
Prelude> ( length . filter (\x -> x == 'a') ) "aaaabccaadeeee"
6
défi 6 : map
¶
*Main> :l addElemInList.hs
[1 of 1] Compiling Main ( addElemInList.hs, interpreted )
Ok, modules loaded: Main.
*Main> let f t = case t of (nb, elem) -> addElemInList elem nb []
*Main> map f [(4,'a'),(1,'b'),(2,'c'),(2,'a'),(1,'d'),(4,'e')]
["aaaa","b","cc","aa","d","eeee"]
défi 7 : foldr
¶
Prelude> foldr (++) [] ["aaaa","b","cc","aa","d","eeee"]
"aaaabccaadeeee"
Défi bonus : définir quelques fonctions¶
import Prelude hiding (length, sum, product, and, or, any, all)
--on cache les fonctions existantes pour les redéfinir
length :: [a] -> Int
length = foldr (\_ y -> y+1) 0
sum :: Num a => [a] -> a
sum = foldr (+) 0
product :: Num a => [a] -> a
product = foldr (*) 1
and :: [Bool] -> Bool
and = foldr (&&) True
or :: [Bool] -> Bool
or = foldr (||) False
any :: (a -> Bool) -> [a] -> Bool
any p = foldr (\x y -> (p x) || y) False
all :: (a -> Bool) -> [a] -> Bool
all p = foldr (\x y -> (p x) && y) True