Foncteur, applicatif, monade¶
Classes de types polymorphes¶
Kind¶
Le système de typage associe un genre (kind) à tout type et toute classe.
Dans GHCi, on peut l’obtenir avec la commande :kind
.
Les types concrets comme
Int
,Int -> Int
ou[Int]
sont de genre*
.Les types d’ordre supérieur, paramétrés par un autre type, comme
Maybe
ou[]
(liste) sont de genre* -> *
.Il y a d’autres genres que nous ne détaillons pas comme
* -> * -> *
,(* -> *) -> *
ou* -> Constraint
.
Classes¶
Foncteur, applicatif et monade sont des types de genre * -> *
et
pour lesquels sont définies des opérations qui respectent certaines lois.
Une monade est un foncteur applicatif qui est lui-même un foncteur. Autrement dit, la monade enrichit l’ensemble des opérations que définit le foncteur applicatif, lui-même plus grand que celui que définit le foncteur.
Foncteur¶
map¶
Intuitivement, un foncteur est un conteneur pour lequel on peut appliquer une fonction à son contenu.
Un exemple typique est la liste, dont la fonction map
permet d’appliquer une fonction à tous ses éléments.
Prelude> map (*2) [1,2,3]
[2,4,6]
Classe Functor
et fmap
¶
Le concept de foncteur généralise map
à toutes les instances de la classe Functor
,
qui exige la définition d’une fonction fmap
:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Cette fonction doit être implémentée de façon à ce que ces deux lois soient respectées :
fmap id == id
fmap (f . g) == fmap f . fmap g
Une liste est un foncteur¶
Une liste est un foncteur dont la fonction fmap
correspond exactement à map
:
Prelude> fmap (*2) [1,2,3]
[2,4,6]
Prelude> fmap (*2) []
[]
Rappel sur Maybe
¶
Maybe
est défini dans le Prélude comme :
data Maybe a = Just a | Nothing
On peut l’utiliser ainsi :
minimum' :: (Ord a) => [a] -> Maybe a
minimum' [] = Nothing
minimum' all@(x:xs) = Just (minimum all)
*Main> minimum' [1,3,-2,5]
Just (-2)
*Main> minimum' []
Nothing
Maybe
est un foncteur¶
Le typage nous empêche d’utiliser directement la valeur.
*Main> 2 * minimum' [1,3,-2,5]
<interactive>:16:1:
Non type-variable argument in the constraint: Num (Maybe a)
(Use FlexibleContexts to permit this)
When checking that ‘it’ has the inferred type
it :: forall a. (Num a, Num (Maybe a), Ord a) => Maybe a
On peut néanmoins manipuler ces valeurs grâce à fmap
:
*Main> fmap (*2) (Just 5)
Just 10
*Main> fmap (*2) Nothing
Nothing
Foncteur applicatif¶
Fonction pure
et opérateur <*>
¶
Intuitivement, les foncteurs applicatifs rendent possible non seulement l’application d’une fonction à un paramètre sur leur contenu, mais aussi l’application d’une fonction à plusieurs paramètres sur autant de foncteurs applicatifs.
Prelude> (*) 2 5
10
Prelude> pure (*) <*> (Just 2) <*> (Just 5)
Just 10
Comme cet exemple le suggère, Maybe
est un foncteur applicatif.
Classe Applicative
¶
Ce concept est implémenté par la classe Applicative
qui hérite de Functor
.
Un type qui instancie Applicative
doit donc instancier également Functor
,
et définir la fonction pure
, ainsi que l’opérateur <*>
:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Comme pour fmap
, ces fonctions doivent être implémentées de façon à ce que
plusieurs lois soient respectées, notamment :
pure f <*> x = fmap f x
Une liste est un foncteur applicatif¶
Une liste est un foncteur applicatif pour lequel pure
construit une liste contenant l’élément donné
en argument et l’opérateur <*>
prend une liste de fonctions et une liste de valeurs et construit une liste
où chacune des fonctions est appliquée sur chacune des valeurs.
Prelude> [(*2), (+1)] <*> [1,2,3]
[2,4,6,2,3,4]
Prelude> pure (*) <*> [2, 1] <*> [1,2,3]
[2,4,6,1,2,3]
Prelude> pure (*)
[(*)]
Prelude> [(*)] <*> [2,1]
[(*2),(*1)]
Prelude> [(*2), (*1)] <*> [1,2,3]
[2,4,6,1,2,3]
Types monadiques¶
Chaînage¶
Intuitivement, les monades sont des conteneurs, comme les foncteurs, qui offrent une manière d’enchaîner des fonctions encapsulant leur valeur de retour dans une monade.
minimum' :: (Ord a) => [a] -> Maybe a
minimum' [] = Nothing
minimum' all@(x:xs) = Just (minimum all)
*Main> minimum' [1,3,-2,5]
Just (-2)
*Main> Just [1,3,-2,5] >>= minimum'
Just (-2)
*Main> Nothing >>= minimum'
Nothing
Comme le suggère cet exemple Maybe
est une monade.
Classe Monad
¶
Ce concept est implémenté par la classe Monad
qui hérite de Applicative
.
Un type qui instancie Monad
doit donc instancier également Applicative
,
et définir l’opérateur >>=
(bind), le reste étant implémenté par défaut.
class Applicative m => Monad m where
return :: a -> f a
return = pure
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
m >> n = m >>= (\_ -> n)
Bind¶
L’opérateur >>=
prend une monade m a
, contenant une valeur de type a
,
applique une fonction (a -> m b)
sur celle-ci et retourne la monade
qui en résulte m b
. L’opérateur >>
est une version particulière où la valeur
de la première monade n’est pas transmise à la seconde.
La sémantique de ces opérateurs doit respecter les lois monadiques :
return a >>= k = k a
m >>= return = m
xs >>= return . f = fmap f xs
m >>= (\x -> k x >>= h) = (m >>= k) >>= h
défi 1 : opérations monadiques¶
Que donnent les expressions suivantes ?
return 'a' :: [Char]
etreturn 4 :: Maybe Int
"abc" >>= (\x -> [x,x])
et"" >>= (\x -> [x,x])
[(4,'a'),(2,'b'),(1,'a')] >>= (\(nb,elt) -> take nb (repeat elt))
Just 5 >>= (\x -> Just (x*2))
Nothing >>= (\x -> Just (x*2))
Just 5 >>= (\x -> return (x*2))
Just 5 >>= return . (*2)
Just 5 >> Just 6
etNothing >> Just 6
[2,5,0] >> "bc"
et"bc" >> [2,5,0]
Sucre syntaxique : notation do
¶
Il y a une notation qui évite l’usage explicite de >>
et >>=
:
do e1 ; e2 = e1 >> e2
do p <- e1; e2 = e1 >>= \p -> e2
Par exemple :
*Main> Just [1,3,-2,5] >>= minimum' >>= (\x -> return (x-10))
Just (-12)
*Main> do lst<- (Just [1,3,-2,5]); min<- minimum' lst; return (min-10)
Just (-12)
Notez qu’en général on passe à la ligne plutôt qu’utiliser un point-virgule.
défi 2 : notation do
¶
Que donnent les expressions suivantes ?
do x <- "abc"; [x,x]
do (nb,elt) <- [(4,'a'),(2,'b')]; take nb (repeat elt))
do x <- Just 5; Just (x*2)
do x <- Nothing; Just (x*2)
do x <- Just 5; return (x*2)
do Just 5; Just 6
do [2,5,0]; "bc"
do [2,5,0]; "bc"; [0]
String
et IO
¶
show
, read
¶
Les fonctions show
and read
permettent de transformer
une valeur d’une instance des classes Show
and Read
en String
et inversement.
*Main> show (Carte Trois Carreau)
"Carte Trois Carreau"
*Main> read "Carte Deux Pique" :: Carte
Carte Deux Pique
Notez qu’on indique le type de la valeur que doit retourner read
car le système de type n’a aucun moyen de le deviner.
*Main> read "Carte Deux Pique"
*** Exception: Prelude.read: no parse
*Main> read "n'importe quoi" :: Carte
*** Exception: Prelude.read: no parse
getLine
, putStrLn
¶
Tout action d’entrée-sortie retourne une valeur qui est étiquettée par le type monadique IO
.
Par exemple, getLine
réalise une action de lecture et retourne une chaîne de caractère.
getLine :: IO String
Les actions qui ne retournent aucune valeur utile prennent le type IO ()
.
Par exemple, putStrLn
prend une chaîne pour l’afficher mais ne retourne rien.
putStrLn :: String -> IO ()
Remarque : print = putStrLn . show
.
bind¶
Les actions sont séquencées avec l’opérateur >>=
.
>>= :: IO a -> (a -> IO b) -> IO b
Ce programme réalise deux actions d’entrée-sortie à la suite : il lit une chaîne, puis l’affiche sur la sortie standard.
main :: IO ()
main = getLine >>= putStrLn
Notation do
¶
Le mot-clé do
introduit une séquence d’instructions
implicitement enchaînées.
Ces instructions sont soit des actions, soit des définitions locales
avec des let-statement où des <-
pour récupérer les valeurs provenant
d’entrées-sorties.
Ce programme est l’équivalent du précédent :
main :: IO ()
main = do l <- getLine
putStrLn l
Autre exemple¶
main :: IO ()
main =
putStrLn "Entrez un texte :"
>> getLine
>>= putStrLn . reverse
main :: IO ()
main = do
putStrLn "Entrez un texte :"
line <- getLine
(putStrLn . reverse) line
Autre exemple avec let
¶
main :: IO ()
main =
putStrLn "Entrez un texte :"
>> getLine
>>= (\line -> let rline = reverse line in putStrLn rline)
main :: IO ()
main = do
putStrLn "Entrez un texte :"
line <- getLine
let rline = reverse line
putStrLn rline
Autre exemple avec return
¶
getName :: IO (Maybe String)
getName =
putStrLn "Name : "
>> getLine
>>= (\name -> let maybeName = if null name
then Nothing
else Just name
in return maybeName )
getName :: IO (Maybe String)
getName = do
putStrLn "Name : "
name <- getLine
let maybeName = if null name then Nothing else Just name
return maybeName
main :: IO ()
main = getName >>= print
Rappel de compilation¶
La commande
ghc fichier.hs -o executable
produit directement un exécutable :./executable
, à partir du moment oùfichier.hs
contient unmain
.D’autres options sont utiles, notamment
-outputdir build
pour mettre les fichiers intermédiaires dans un répertoire séparébuild
(voirman ghc
oughc --help
).
défi 3 : jeu de devinette¶
A partir d’un type Carte
modélisant une carte à jouer,
définissez la fonction :
jeu :: Carte -> IO ()
appelée ci-dessous :
main = do
let laCarte = Carte As Trefle
putStr "indice: "
print laCarte
-- jeu de devinette
jeu laCarte
L’utilisateur tape le nom d’une carte. Si c’est celle mémorisée dans le programme, il a gagné, sinon il a le droit de proposer une nouvelle carte.
Conclusion¶
Capacités/Connaissances¶
Donnez des exemples de monades:
Maybe
,[]
,IO
.Expliquez ce qu’est une monade.
Utilisez à bon escient les principales fonctions d’entrée-sorties:
read
,show
,getLine
,putStrLn
.Traduire une opération bind par un do et inversement.
Ecrire un petit programme en définissant la fonction
main
défi 1 : Opérations habituelles¶
Prelude> return 'a' :: [Char]
"a"
Prelude> return 4 :: Maybe Int
Just 4
Prelude> "abc" >>= (\x -> [x,x])
"aabbcc"
Prelude> "" >>= (\x -> [x,x])
""
Prelude> [(4,'a'),(2,'b'),(1,'a')] >>=
(\(nb,elt) -> take nb (repeat elt))
"aaaabba"
défi 1 : Opérations habituelles (suite)¶
Prelude> Just 5 >>= (\x -> Just (x*2))
Just 10
Prelude> Nothing >>= (\x -> Just (x*2))
Nothing
Prelude> Just 5 >>= (\x -> return (x*2))
Just 10
Prelude> Just 5 >>= return . (*2))
Just 10
Prelude> Just 5 >> Just 6
Just 6
Prelude> Nothing >> Just 6
Nothing
Prelude> [2,5,0] >> "bc"
"bcbcbc"
Prelude> "bc" >> [2,5,0]
[2,5,0,2,5,0]
défi 2 : Notation do
¶
Prelude> do x <- "abc"; [x,x]
"aabbcc"
Prelude> do (nb,elt) <- [(4,'a'),(2,'b')]; take nb (repeat elt)
"aaaabb"
Prelude> do x <- Just 5; Just (x*2)
Just 10
Prelude> do x <- Nothing; Just (x*2)
Nothing
Prelude> do x <- Just 5; return (x*2)
Just 10
Prelude> do Just 5; Just 6
Just 6
Prelude> do [2,5,0]; "bc"
"bcbcbc"
Prelude> do [2,5,0]; "bc"; [0]
[0,0,0,0,0,0]
défi 3 : Jeu de devinette (types)¶
data CouleurCarte = Trefle | Carreau | Coeur | Pique
deriving (Eq, Ord, Enum, Read, Show)
data ValeurCarte = Deux | Trois | Quatre | Cinq | Six | Sept |
Huit | Neuf | Dix | Valet | Dame | Roi | As
deriving (Eq, Ord, Enum, Read, Show)
data Carte = Carte ValeurCarte CouleurCarte
deriving (Eq, Read, Show)
défi 3 : Jeu de devinette (suite)¶
jeu :: Carte -> IO ()
jeu carteATrouver =
do
line <- getLine
if (read line) == carteATrouver
then putStrLn "Vous avez gagne!"
else do
putStrLn "Non, reessayez!"
jeu carteATrouver
main = do
let laCarte = Carte As Trefle
putStr "indice: "
print laCarte
-- jeu de devinette
jeu laCarte