mdulac

home

Play with Maybe "monads !"

01 Feb 2014

Si tu as déjà touché à la programmation fonctionnelle, tu as probablement entendu parler de la notion de Monade. Il existe pléthore d'articles sur Internet couvrant les monades, mais beaucoup d'entre eux sont teintés de notions mathématiques, et plus précisément de théorie des catégories.

Pour définir la notion de monade en deux mots : Contexte et Composition.

Les exemples de code qui suivent sont écrits en Haskell, un langage que j'affectionne particulièrement pour sa syntaxe concise, mais très expressive.

Imagine une fonction qui retourne, pour tout entier positif, le double de sa valeur. Comment représenter le fait que cette fonction puisse ne pas retourner de valeur utile, dans le cas où le paramètre est négatif ? Il s'agit là d'un contexte d'absence probable de valeur utile, et il y a une monade pour ça !

Maybe a

Haskell propose un type Maybe a pour représenter ce contexte, où a est un type paramétré. Ce type propose deux valeurs, Just a et Nothing. Ainsi, le type Maybe Int contient les valeurs Just Int et Nothing, le type Maybe String contient les valeurs Just String et Nothing, etc

Just "hey you !" :: Maybe String
Just 3 :: Maybe Int
Nothing :: Maybe a

Ok, maintenant que nous avons découvert ce contexte, on va l'utiliser pour implémenter notre fonction, qui retournera dans un contexte Just le double de la valeur, si elle est positive, sinon Nothing :

doublePositive :: Int -> Maybe Int
doublePositive x
    | x > 0 = Just (2 * x)
    | otherwise = Nothing
doublePositive 2 -- returns Just 4
doublePositive 6 -- returns Just 12
doublePositive -1 -- returns Nothing

Il est d'usage d'utiliser la fonction return pour placer une valeur dans son contexte, le contexte est inféré grace au prototype de la fonction :

doublePositive :: Int -> Maybe Int
doublePositive x
    | x > 0 = return (2 * x)
    | otherwise = Nothing

C'est bien d'avoir une valeur contextualisée, mais encore faut il pouvoir l'exploiter.

Usage et composition

Comment faire maintenant pour, par exemple, réappliquer la fonction sur la valeur retournée ?

let r = doublePositive 4 in doublePositive r -- Ne compile pas !

Le code ci-dessus ne compile pas car il est impossible d'appliquer la fonction doublePositive à Just Int (Maybe Int n'est pas un Int). Maybe a propose la fonction >>= qui permet de réaliser une composition de valeurs contextualisées. De cette manière, les fonctions a -> M a et a -> M b peuvent être composées en une fonction a -> M b. Le prototype de la fonction >>= est M a -> (a -> M b) -> M b

doublePositive 3 -- cette fonction retourne un Maybe Int
doublePositive -- cette fonction est du type Int -> Maybe Int
doublePositive 3 >>= doublePositive -- returns Just 12

Tu viens d'appliquer la fonction doublePositive à Just Int ! Tu peux évidemment appliquer toute fonction de type Int -> Maybe Int !

Le prototype de la fonction >>= étant M a -> (a -> M b) -> M b, tu peux évidemment transformer ton Int en String, ou en tout autre type !

doublePositive 1 >>= doublePositive >>= doublePositive -- returns Just 8
doublePositive -4 >>= doublePositive >>= doublePositive -- returns Nothing
doublePositive 3 >>= ( \x -> return (x - 2) ) -- returns Just 4
doublePositive 3 >>= ( \x -> return (x - 10) ) >>= doublePositive -- returns Nothing
doublePositive 4 >>= ( \x -> return (show x) ) -- returns Just "8"
doublePositive 4 >>= ( \x -> return ("Hello " ++ show x) ) -- returns Just "hello 8"

Les fonctions sont donc maintenant composables les unes avec les autres.

Et la monade dans tout ça ?

Tu viens de voir que Maybe a est une monade parmi d'autres. Les deux fonctions return et >>= que l'on vient d'utiliser sont disponibles pour toutes les monades. Haskell dispose d'un typeclass Monad qui t'oblige à définir ces fonctions si tu dérives Monad (Maybe est une instance de Monad, donc implémente ces fonctions).

class Monad Maybe where
    return = Just
    m >>= f = case m of
        Just x = f x
        _ = Nothing

Tu dois savoir que pour être une monade, un type doit respecter trois lois :

Maybe couvre ces trois conditions :

-- Composition neutre par return à gauche
let f = ( \x -> return (x + 5) )
return 5 >>= f -- returns Just 10
f 5 -- returns Just 10

-- Composition neutre par return à droite
Just 5 >>= return -- returns Just 5

-- Associativité
let f = ( \x -> return (x + 5) )
let g = ( \x -> return (x + 10) )
(Just 5 >>= f) >>= g -- returns Just 20
Just 5 >>= ( \x -> f x >>= g ) -- returns Just 20

Si tu veux en savoir plus sur Maybe, je te conseille de visiter le site Hoogle, et plus particulièrement la page dédiée au type Maybe.