Appliquer plusieurs arguments
Dans le chapitre sur les fonctions, on a vu qu'on pouvait définir des fonctions plus, moins, ... qui renvoient Nothing en cas d'erreur comme une division par zéro :
Code : Haskell 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | plus Nothing _ = Nothing
plus _ Nothing = Nothing
plus (Just a) (Just b) = Just (a + b)
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)
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 / b)
|
On peut essayer d'utiliser les foncteurs pour définir
plus
,
moins
et
fois
de façon plus simple. On commence simplement par traiter le premier argument avec fmap (+). Mais là on se retrouve devant un problème :
fmap (+) a
est de type
Maybe (Integer -> Integer)
, alors qu'on a besoin d'une fonction d'une fonction de type <mincode type="haskell">Maybe Integer -> Maybe Integer</minicode>.
On a donc besoin d'une fonction de type
Maybe (a -> b) -> Maybe a -> Maybe b
, qu'on peut voir comme une fonction qui applique une fonction contenue dans Maybe à une valeur contenue dans Maybe, ou alors une fonction qui transforme une fonction en une autre, suivant le point de vue. On peut la coder pour Maybe :
Code : Haskell | app :: Maybe (a -> b) -> Maybe a -> Maybe b
app (Just f) (Just x) = Just (f x)
app _ _ = Nothing
|
Mais cette fonction peut très facilement se généraliser à d'autres foncteurs, comme IO :
Code : Haskell | appIO :: IO (a -> b) -> IO a -> IO b
appIO fm xm = do
f <- fm
x <- xm
return $ f x
|
On peut ensuite utiliser ces fonctions (les lignes surlignées représentent l'entrée) :
Code : Console | *Main> app (fmap (+) (Just 1)) (Just 2)
Just 3
*Main> app (fmap (+) Nothing) (Just 2)
Nothing
*Main> app (fmap (+) (Just 1)) Nothing
Nothing
*Main> appIO (fmap (+) readLn) readLn
1
2
3 |
Les foncteurs applicatifs
Définition
Puisque cette fonction se retrouve avec plusieurs types, on la généralise en utilisant la classe de type Applicative, et on dit qu'un type est un
foncteur applicatif (puisqu'on peut
appliquer les fonctions). La classe Applicative est définie dans le module
Control.Applicative comme ceci :
Code : Haskell | class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
|
L'opérateur
<*>
correspond à la fonction qu'on a appelée
app
, mais est plus pratique à utiliser. Le module
Control.Applicative
définit aussi l'opérateur
<$>
comme un synonyme de
fmap
: on peut donc écrire
(+) <$> readLn <*> readLn
au lieu de
appIO (fmap (+) readLn) readLn
.
La fonction
pure
sert à créer une valeur du foncteur applicatif à partir d'une valeur normale. L'instance d'Appicative doit être liée à l'instance de Functor par la relation suivante :
pure f <*> v == fmap f v
. Une autre règle est qu'on doit avoir :
pure f <*> pure x == pure (f x)
:
pure
transforme l'application de fonctions en
<*>
. Il y a trois autres règles à respecter, qui sont un peu plus compliquées à comprendre et moins utiles :
Code : Haskell | pure id <*> v == v
pure (.) <*> u <*> v <*> w == u <*> (v <*> w)
u <*> pure y == pure ($ y) <*> u
|
Importer Control.Applicative ajoute un certain nombre d'instances de Functor : vous risquez de vous retrouver avec des problèmes d'instances dupliquées. Il suffit d'enlever les définitions d'instances concernées pour régler le problème.
Exemples d'instances
Il y a aussi beaucoup d'instances d'Applicative. On a vu avant que Maybe et IO disposaient d'une fonction
<*>
, mais on peut aussi leur donner une fonction
pure
:
Code : Haskell | instance Applicative Maybe where
pure = Just
Just f <*> Just x = Just $ f x
_ <*> _ = Nothing
instance Applicative IO where
pure = return
fm <*> xm = do
f <- fm
x <- xm
return (f x)
|
Par contre, même si on peut définir une instance de Functor pour
(,) e
, il n'y a pas d'instance d'Applicative : pour cela, il faudrait un moyen de créer une valeur de type e, et de combiner deux valeurs, qui permette de respecter les règles des foncteurs applicatifs. Si on choisit de spécialiser l'instance (par exemple aux entiers), on obtient une instance qui marche :
Code : Haskell | instance Applicative ((,) Integer) where
pure x = (0,x)
(a,f) <*> (b,x) = (a+b, f x)
|
Mais si vous essayez de définir cette instance, vous allez rencontrer une erreur du compilateur :
Code : Console | /home/gnomnain/haskell/tuto/functor.hs:54:0:
Illegal instance declaration for `Applicative ((,) Integer)'
(All instance types must be of the form (T a1 ... an)
where a1 ... an are type *variables*,
and each type variable appears at most once in the instance head.
Use -XFlexibleInstances if you want to disable this.)
In the instance declaration for `Applicative ((,) Integer)' |
Cela vient d'une limitation des définitions possible de typeclasses dans la version Haskell 98 du langage. Mais GHC supporte beaucoup d'extensions à Haskell 98. Ici, il nous indique d'utiliser l'extension
FlexibleInstances. Vous pouvez l'activer de deux façons : en lançant
ghci avec l'option
-XFlexibleInstances, ou en rajoutant tout en haut de votre fichier un commentaire avec le contenu suivant :
{-# LANGUAGE FlexibleInstances #-}.
Il est possible de définir une instance similaire en faisant le produit de deux entiers, en concaténant des listes, en prenant le minimum ou le maximum, ...
Pour continuer sur la gestion des erreurs, on peut aussi créer une instance d'Applicative pour Either : il suffit de prendre la première valeur d'erreur rencontrée sur les deux arguments de <*> :
Code : Haskell | instance Applicative (Either e) where
pure = Right
Right f <*> Right x = Right $ f x
Left e <*> _ = Left e
Right _ <*> Left e = Left e
|
Mais on pourrait aussi imaginer une instance qui prend l'erreur placée la plus à droite, et pas celle-là plus à gauche :
Code : Haskell | instance Applicative (Either e) where
pure = Right
Right f <*> Right x = Right $ f x
_ <*> Left e = Left e
Left e <*> _ = Left e
|
On peut même envisager de stocker une liste de toutes les erreurs rencontrées :
Code : Haskell | instance Applicative (Either [e]) where
pure = Right
Right f <*> Right x = Right $ f x
Left e <*> Right _ = Left e
Right _ <*> Left f = Left f
Left e <*> Left f = Left (e ++ f)
|
Mais la première instance est la plus utilisée, puisqu'elle est compatible avec l'instance de la classe Monad pour Either, que vous découvrirez dans un autre chapitre. Celle qui récupère toutes les erreurs est cependant utile, si on souhaite par exemple présenter le plus d'erreurs possibles à l'utilisateur.
On a vu que les fonctions disposaient d'une instance de Functor. On peut aussi les rendre membres de la classe Applicative. pure doit renvoyer une fonction de type r -> a à partir d'une valeur de type a : il faut logiquement renvoyer une fonction constante. On voit aussi que le type de l'opérateur
<*>
doit être
(r -> a -> b) -> (r -> a) -> (r -> b)
. On en déduit alors la définition (qui est déjà présente par défaut) :
Code : Haskell | instance Applicative ((->) r) where
pure = const
f <*> x = (\r -> (f r) (x r)
|
Cette instance d'Applicative permet de composer des actions qui accèdent à un contexte pour leur exécution. Par exemple, si on code un interpréteur, le contexte serait la liste des variables définies et leur valeur.
Il existe aussi une instance d'Applicative pour les listes. La fonction
pure
renvoie une liste à un élément, et
<*>
crée toutes les combinaisons possibles des valeurs dans les listes de ses deux éléments. Cette fonction se créer facilement grâce à
concatMap
, qui combine
concat
et
map
en concaténant les résultats de la fonction donnée sur les chaque élément :
Code : Haskell | instance Applicative [] where
pure x = [x]
fs <*> xs = concatMap (\f -> map f xs) fs
-- on pourrait écrire
-- fs <*> xs = concat $ map (\f -> map f xs) fs
|
Cette instance est pratique dans les cas où on veut tester toutes les possibilités. Par exemple, on peut s'en servir pour savoir si une formule logique est toujours vraie (ici, ~> note l'implication) :
Code : Haskell | True ~> False = False
_ ~> _ = True
prop a b c = (a ~> b ~> c) ~> ((a ~> b) ~> (a ~> c))
prop2 a b c = (a ~> b) ~> ((a ~> c) ~> (b ~> c))
bool = [True,False]
test prop = prop <$> bool <*> bool <*> bool
|
Grâce à ce code, on peut tester une propriété sur les 8 combinaisons possibles de a, b et c :
Code : Console | *Main> test prop
[True,True,True,True,True,True,True,True]
*Main> test prop2
[True,True,True,True,True,False,True,True] |
On voit alors que
prop
est toujours vraie, mais pas
prop2
.
Fonctions utiles
Le module
Control.Applicative
définit quelques fonctions utiles pour travailler avec les foncteurs. La fonction
liftA2 :: (Functor f) => (a -> b -> c) -> f a -> f b -> f c
permet de transformer une fonction qui travaille sur des valeurs "pures" en une fonction qui agit sur les valeurs du foncteur. Elle est facile à coder, et nous permet de factoriser les fonctions
plus
,
moins
et
fois
vue en introduction. La fonction
liftA3
fait la même chose avec les fonctions à trois arguments :
Code : Haskell | liftA2 f x y = f <$> x <*> y
liftA3 f x y z = f <$> x <*> y <*> z
plus, moins, fois :: (Num a, Functor f) => f a -> f a -> f a
plus = liftA2 (+)
moins = liftA2 (-)
fois = liftA2 (*)
|
Pour voir le fonctionnement des fonctions suivantes, un bon exemple est le type Log, qui a une instance d'Applicative (ce n'est qu'un cas particulier des instances pour les paires) :
Code : Haskell | data Log e a = Log [e] a
instance Applicative (Log e) where
pure = Log []
Log e1 f <*> Log e2 x = Log (e1 ++ e2) (f x)
log a = Log [a] ()
|
On voit qu'avec cette instance, l'ordre d'exécution a une importance :
liftA2 (+) (Log "1" 1) (Left "2" 2)
renvoie
Log "12" 3
, alors que si on inverse les arguments, on obtient
Log "21" 3
.
Les fonctions
*>
et
<*
s'occupent d'abord du premier argument puis du deuxième, mais renvoient la valeur qui est du côté de la flèche. Par exemple,
log "123" *> (log "456" *> pure 2)
renvoie
Log "123456" 2
, alors que
(log "456" *> pure 2) <* log "123"
renvoie
Log "456123" 2
. Elles sont donc importantes quand le foncteur a une notion d'ordre. La fonction
<**>
est comme
<*>
, sauf que la séquence est inversée :
f <*> x
applique d'abord l'effet de f, puis de x, alors que
<**>
applique d'abord l'effet de x, puis celui de f. Enfin, le module
Data.Traversable
définit une fonction
sequenceA
. Elle s'applique à tous les conteneurs ayant une instance de la classe
Traversable
. Pour les listes, le type de la fonction est :
sequenceA :: (Applicative f) => [f a] -> f [a]
: les actions contenues dans la liste sont séquencées, et on renvoie la liste de leur résultats.
Une autre construction
On peut aussi construire les foncteurs applicatifs d'une autre façon. La construction avec
pure
et
<*>
est assez naturelle, mais donne des règles difficiles à établir et à interpréter. On va garder la fonction
pure
, mais essayer de trouver un remplacement pour
><*>
. La fonction
liftA2
est une bonne candidate : en effet, on a
f <*>x = liftA2 ($) f x
. On a donc une première définition équivalente.
La deuxième remarque qu'on peut faire c'est que les fonctions à deux arguments peuvent être converties en fonction qui prennent une coupe et vice-versa grâce aux fonctions
curry :: ((a,b) -> c) -> a -> b -> c
et
uncurry :: (a -> b -> c) -> (a,b) -> c
. Le problème qui se pose pour coder <minicode tyê="haskell">liftA2</minicode> à partir de
fmap
, c'est qu'il faut transormer des
f a et
f b en
f (a,b). Si on a une fonction pour cela, on peut retrouver les opérations sur les foncteurs applicatifs.
La fonction pure n'est même pas nécessaire, elle peut être remplacée par une valeur
unit = pure ()
. En effet, on a
pure a = const a <$> unit
. Ces définitions sont donc liées par les relations suivantes :
Code : Haskell | unit = pure ()
pure a = const a <$> unit
(<&>) = liftA2 (,)
liftA2 f a b = f <$> (a <&> b)
f <*>x = liftA2 ($) f x
|
Les propriétés des foncteurs applicatifs s'écrivent de façon plus sympathique avec cette notation, et sont plus naturelles :
Code : Haskell | -- On pose :
assoc (a,(b,c)) = ((a,b),c)
(f *** g) (x,y) = (f x, f y)
-- Les règles :
fmap (f *** g) (x <&> y) == fmap f x <&> fmap f y
fmap fst (x <&> unit) == x
fmap snd (unit <&> x) == x
fmap assoc (u <&> (v <&> w)) = (u <&> v) <&> w
|
Cependant, en général, cette définition ne facilite pas beaucoup la définition d'une instance d'Applicative, mais risque plutôt de la compliquer puisqu'il faut définir des fonctions annexes.
Applications intéressantes
Généraliser la fonction zipWith à plus d'arguments
La fonction
zipWith
permet d'appliquer une fonction aux éléments de deux listes pris deux à deux. On a
zipWith f [x1,x2,...] [y1,y2,...] = [f x1 y1, f x2 y2, ...]
. Il existe de même une fonction
zipWith3
qui prend une fonction à trois arguments et trois listes. Mais il faut recréer une fonction pour chaque nombre d'arguments, ce n'est donc pas très pratique.
Cependant, on peut coder les fonctions
zipWithN
(où N vaut 3, 4, ...) simplement à partir de la fonction
zipWith
: on applique d'abord partiellement la fonction
f
à tous les éléments de la première liste, puis on applique les éléments de la liste obtenue à la deuxième liste, ... Par exemple :
Code : Haskell | zipWith3' f xs ys zs = zipWith (zipWith ($) (map f xs) ys) zs
|
En fait, on remarque que
zipWith ($)
joue le rôle de l'opérateur
<*>
. Il reste à trouver une fonction
pure
qui correspond. Il faut répéter l'élément donné un certain nombre de fois. Mais si on ne le met qu'un nombre fini de fois, on peut trouver une liste plus longue, et dans ce cas on n'a pas la propriété
pure id <*> u == u
, puisque la liste sera tronquée. Il faut donc générer une liste infinie : c'est ce que fait la fonction
repeat :: a -> [a]
. Mais il y a un dernier problème : on a déjà une instance d'Applicative pour les listes. On va donc utiliser un
newtype
pour définir un synonyme pour les listes. Dans la bibliothèque standard, il est nommé
ZipList
.
Code : Haskell | data ZipList a = ZipList { unZipList :: [a] }
instance Applicative ZipList where
pure = ZipList . repeat
(ZipList f) <*> (ZipList x) = ZipList $ zipWith ($) f x
|
Traiter des formulaires
Si vous avez déjà programmé des sites web, par exemple en PHP, vous savez surement que traiter des formulaires peut devenir très lourd : on se retrouve parfois avec beaucoup de conditions imbriquées, du code répétitif, ... C'est un cas classique de traitement des erreurs, et on a vu que les foncteurs applicatifs s'y prêtaient plutôt bien. On veut en général présenter toutes les erreurs de traitement à l'utilisateur. De plus, on a un contexte d'exécution à transporter : ce sont les valeurs reçues du formulaire. Or, comme pour les foncteurs, on peut composer les foncteurs applicatifs, donc on peut à la fois transporter un contexte et récupérer les erreurs.
Code : Haskell 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | type Dict = [(String,String)]
data Acc a b = Error [a] | Result b
deriving Show
instance Functor (Acc e) where
fmap f x = pure f <*> x
instance Applicative (Acc e) where
pure = Result
Result f <*> Result x = Result (f x)
Error e <*> Result _ = Error e
Result _ <*> Error f = Error f
Error e <*> Error f = Error (e ++ f)
newtype Form a = Form { runForm :: Dict -> Acc String a }
instance Functor Form where
fmap f x = pure f <*> x
instance Applicative Form where
pure = Form . pure . pure
Form fe <*> Form xe = Form (liftA2 (<*>) fe xe)
|
On a appelé
Acc
le foncteur applicatif qui accumule les erreurs. La définition de
<*>
pour
Form
est en fait la définition générale pour une composée de foncteurs applicatifs : si on appelle F et G les deux foncteurs, on a une fonction
G (a -> b) -> G a -> G b
et on veut une fonction
F (G (a -> b)) -> F (G a) -> F (G b)
. Il est donc naturel d'utiliser la fonction
liftA2
associée à F.
On doit ensuite coder quelques fonctions pour récupérer et vérifier les données. Le but est de créer une structure qui contient toutes les données nécessaires pour le résultat. La fonction clé est check, qui prend un prédicat (une fonction renvoyant un booléen), et vérifie s'il est respecté par les données. On a aussi quelques fonctions pour récupérer les valeurs :
getInt
,
getString
, ...
Code : Haskell 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | getString :: String -> Form String
getString s = Form $ (\env -> case lookup s env of
Just a -> Result a
Nothing -> Result "")
getInt :: String -> Form Int
getInt s = Form $ (\env -> case lookup s env of
Just a | all (`elem` ['0'..'9']) a -> Result (read a)
_ -> Error ["Le champ " ++ s ++ " n'est pas un entier"])
check :: String -> (a -> Bool) -> Form a -> Form a
check err pred val = Form $ (\env -> case runForm val env of
Error e -> Error e
Result v -> if pred v
then Result v
else Error [err])
|
On peut ensuite traiter un formulaire simple, pour récupérer un enregistrement de type
Glace
défini plus haut :
Code : Haskell | parseGlace :: Form Glace
parseGlace = liftA3 Glace
(check "Le champ \"parfum\" doit faire entre 1 et 40 caractères"
(\p -> let l = length p in 1 <= l && l <= 40) $ getString "parfum")
(getString "fabriquant")
(check "La note doit être comprise entre 0 et 20"
(\n -> 0 <= n && n <= 20) $ getInt "note")
|