Les Monades et leur utilisation en PHP
le 30 septembre 2021
La présentation liée à l'article :
A l'occasion du meetup AFUP de Rennes, je vous introduit dans cet article les monades et leur utilisation en PHP.
Pourquoi les monades ?
Une nouvelle façon de programmer avec des "effets".
Une autre raison serait à cause du paradox de Crockford's qui est vue comme une "malédiction" :
Lorsque vous avez pleinement compris ce qu'est une Monad, vous perdez l'habilité d'expliquer ce concept à d'autres personnes. En référence au talk de 2012 Douglas Crockford: Monads and Gonads
Un peu d'histoire
Philip Wadler commence, en 1992, un article qui se révélera décisif dans l'application des monades à la programmation fonctionnelle : Monads for functional programming
Depuis, l'usage des monades s'est répandu dans plusieurs langages, et en particulier dans Haskell, auquel elles ont apporté une réponse élégante pour les entrées/sorties et pour le contrôle d'exécution.
Partons sur un example
Nous allons partir d'un exemple simple qui consiste à écrire une fonction qui évalue des expressions. Pour support nous utiliserons le langage Haskell, un langage de programmation fonctionnel crée en 1990, il n'est pas nécessaire de connaitre ce language pour l'exemple.
Partons d'un type de données pour laquelle on va évaluer une expression qui peut être soit une valeur entière, soit une division entre deux expressions représenté sous cette forme :
Data Expr = Val Int | Div Expr Expr
Maintenant division l'espace en deux : ce que l'on peut écrire en notations mathématiques et l'équivalent en Haskell
On aurait les équivalences suivantes :
- 1 (entier) équivaut à Val 1 (Haskell)
- 6 / 2 (entier) équivaut à Div (Val 6) (Val 2) (Haskell)
- 6 / ( 3 / 1 ) (entier) équivaut à Div (Val 6) (Div (Val 3) (Val 1)) (Haskell)
Ce qui nous intéresse est de savoir comment écrire un programme qui évalue ces expressions. On écrit alors une fonction d'évaluation qui prend une Expression en entrée et une Valeur entière en sortie.
eval :: Expr -> Int
eval (Val n) = n
eval (Div (Expr x) (Expr y)) = eval(x) / eval(y)
Il est facile de construire une fonction récursive pour évaluer le résultat. Cependant nous avons un problème car le programme peut crasher étant donné que la division par zéro est indéfinie.
Que peut-on faire pour résoudre ce problème, on défini une version "safe" de l'opérateur de division qui ne permet pas de faire crasher le programme. On défini alors la fonction safediv :
safediv :: Int -> Int -> MayBe Int
safediv (Int n) (Int m) = if m == 0 then Nothing else Just(n / m)
Le type de la fonction est Int vers Int vers "peut être Int" représenté par Maybe. Ici Maybe peut recevoir deux types de constructeurs : Nothing ou Just Dans cette représentation, le programme ne crash plus. Nous pouvons réécrire notre fonction d'évaluation comme ci :
eval :: Expr -> Maybe
eval (Val n) = Just(n)
eval (Div (Expr x) (Expr y)) = case eval(x) at
Nothing -> Nothing
Just n -> case eval(y) of
Nothing -> Nothing
Just m -> safediv (n m)
Le programme ne crash plus, mais il devient plus compliqué à écrire. Comment faire pour le simplifier ? On peut identifier des patterns au niveau de l'évaluation des cas. On va alors abstraire ces patterns :
case [m] as Maybe of
Nothing -> Nothing
Just x -> [f] as function x
Ce qui se traduit de cette manière en language Haskell avec le symbole de sequence >>=
m >>= f case m of
Nothing -> Nothing
Just x -> f x
On peut simplifier notre fonction eval
eval :: Expr -> Maybe Int
eval (Val n) = return n
eval (Div (Expr x) (Expr y)) = eval(x) >>= (ƛn ->
eval(y) >>= (ƛm ->
safediv n m))
Mais on peut faire encore plus simple grâce à la notation "do" de Haskell
eval :: Expr -> Maybe Int
eval (Val n) = return n
eval (Div (Expr x) (Expr y)) = do n <- end x
m <- end y
safediv n m
Nous avons écrit de manière élégante le programme initial mais sans avoir de crash. Ce que nous venons de voir ici, c'est de découvrir la Monade Maybe :
return :: a -> Maybe a
>>= :: Maybe a -> (a -> Maybe b) -> Maybe b
Les monades apportent les idées suivantes :
- les utiliser en tant que framework pour écrire d'autres effets.
- un support pour la programmation pure avec effets.
- l'utilisation d'effets par l'utilisation de types explicites.
- des fonctions pour n'importe quel type d'effet.
Comment les représenter
Dans la programmation fonctionnelle, on parle de fonctions pures, c'est à dire une fonction pour laquelle nous pouvons :
- passer des arguments par valeur
- avoir une valeur de retour
- ne pas utiliser de variables globales, ni d'appel à l'aide du mot clef
$this
ou du mot clefstatic
- facilement tester grâce aux tests unitaires
- mettre en cache les valeurs de retours selon les valeurs d'entrée grâce à la mémoïsation.
Les langages fonctionnels ont comme autre propriété la transparence référentielle. Ce terme recouvre le principe simple selon lequel le résultat du programme ne change pas si on remplace une expression par une expression de valeur égale. Ce principe est violé dans le cas de procédures à effets de bord puisqu'une telle procédure, ne dépendant pas uniquement de ses arguments d'entrée, ne se comporte pas forcément de façon identique à deux instants donnés du programme.
Par exemple :
- file_get_contents($filename) n'assure pas de retourner le même contenu si le fichier a été édité
- mysql_query($query) ne renvoie pas les mêmes données, car elles dépendent de l'état de la base de données
- time() : renvoie une valeur dynamique en fonction du temps
- rand() : retourne une valeur aléatoire
C'est un concept qui a été popularisé par la communauté Haskell pour lequel il faut apprendre la théorie mathématique sur les Catégories.
On pourrait parler des monades de manière algébrique.
Cela signifie que toute monade donne lieu à la fois à une catégorie (appelée catégorie de Kleisli) et à un monoïde dans la catégorie des foncteurs (des valeurs aux calculs), avec la composition monadique comme opérateur binaire et l'unité comme identité.
Un foncteur n’est rien d’autre qu’une fonction qui applique une autre fonction à des objets dotés d’une structure et qui préserve cette structure. Par analogie avec la théorie des catégories, les objets ici sont des morphismes, en l’occurrence Maybe Int en Haskell pour lesquels fmap conserve bien leur structure de Maybe. Un foncteur est donc toujours relatif à un type de données générique. En Haskell, les foncteurs sont explicites. Ils sont définis par le type algébrique Functor et Maybe est une instance de ce type et spécifie donc comment fmap s’applique à elle. On dit alors, par extension, que Maybe est un foncteur.
Une monade n’est pas cette fois-ci un morphisme comme le sont les foncteurs et les foncteurs applicatifs mais plutôt une construction, un type algébrique abstrait, reposant sur les foncteurs et qui est défini par les propriétés suivantes :
- il existe une correspondance qui à tout type générique relie un type monadique. Autrement dit, par simplification, un constructeur qui à une valeur retourne une monade avec cette valeur. Dans notre exemple, fmap compute retourne une monade avec un tel constructeur (la fonction compute curryfiée)
- il existe une opération de composition interne associative entre monades sous forme d’un foncteur, donc qui préserve la structure monadique. Dans notre exemple, il s’agit de
>>=
- il existe un élément neutre, appelé identité. Dans le cas des Maybe a, c’est Nothing.
Une monade est une application aux catégories (en gros, dans notre cas, aux foncteurs) ce que sont les monoïdes en algèbre (qui sont des ensembles munis d’une loi de composition interne associative et d’un élément neutre). D’où le nom de monade.
Pour l'instant, ne nous soucions pas trop de ce qu'est une Monade. Cela devrait devenir relativement évident alors que nous jouons avec quelques-unes. Au lieu de cela, pour nos besoins, pensez simplement à une Monade comme un conteneur d'état, où différentes Monades font des choses différentes à cet état.
Un peu de théorie
Définition formelle : Une monade peut se voir comme la donnée d'un triplet constitué des trois éléments suivants.
- Un constructeur de type appelé type monadique, qui associe au type
t
le typeMt
- Une fonction nommée
unit
oureturn
qui construit à partir d'un élément de type sous-jacenta
un autre objet de type monadiqueMa
. Cette fonction est alors de signaturet -> Mt
. - Une fonction bind, représentée par l'opérateur infixe
>>=
, associant à un type monadique et une fonction d'association un autre type monadique. Il permet de composer une fonction monadique à partir d'autres fonctions monadiques. Cet opérateur est de type>>=: Mt (t -> Mu) -> Mu
.
En composant la fonction >>=
(dite fonction de liaison) avec la fonction return
, on peut appliquer n'importe quelle fonction g : t -> t
à une monade de type Mt
. En ce sens une monade de type Mt
est un type algébrique qui dérive du type t
.
Axiomes :
La définition précédente s'accompagne de plusieurs axiomes. L'opérateur return
agit comme une sorte d'élément neutre pour >>=
.
- composition à gauche par
return
(≡ désigne l'égalité structurelle) :(return x) >>= f ≡ fx
- composition à droite par
return
:m >>= return ≡ m
- associativité :
(m >>= f) >>= g ≡ m >>= ƛx
Quelles sont les applications ?
Pour un langage impur, c'est à dire permettant toute sorte d'effet de bord dans les fonctions, les monades ne sont pas d'une nécessité évidente, bien qu'elles puissent offrir une solution élégante et rapide à certaines catégories de problèmes.
En revanche, pour un langage fonctionnel pur, c'est à dire ne permettant aucun effet de bord, et aucun contrôle d'exécution implicite, les monades offrent une construction permettant d'enrichir les capacités des fonctions. Expliquons cela par un exemple : dans un langage pur, une fonction de type Int → Int → Int sera capable de prendre deux entiers pour en composer un autre. Point. C'est à dire que cette fonction ne pourra lire absolument aucun état du programme, ne pourra accéder ni à un fichier, ni à l'heure courante, ni même à un nombre aléatoire. Elle ne pourra pas non plus logger dans un terminal, ou écrire un rapport d'exécution dans une variable.
La signature de cette fonction a un sens très strict quant à ses capacités : elle peut lire deux entiers, et les utiliser pour en produire un autre. Elle est donc pure, et cela lui confère entre autres une propriété essentielle, le déterminisme. Appelée avec les mêmes arguments, elle produira inlassablement le même résultat, quel que soit l'état de son environnement d'exécution (programme, heure, fichiers, entrées de l'utilisateur etc).
Une autre catégorie couramment rencontrée est celle des fonctions monadiques, qui forment une généralisation des fonctions ordinaires. Pour plus d'informations à ce sujet, les applications avec la catégorie de Kleisli est bien détaillée sur cette article : https://www.atikteam.com/fr/blog/page/Programmation-Fonctionnelle-et-Theorie-des-Categories#la-cat%C3%A9gorie-de-kleis
Des exemples d'application
- la monade Identité
MonadPHP : https://blog.ircmaxell.com/2013/07/taking-monads-to-oop-php.html#Getting-Start
- la monade
Failable
En cas de succès, on construira Ok a, et en cas d'échec, on construire Failed "Motif de l'erreur". Oui, vous reconnaissez peut-être ici le principe des exceptions. Sauf que dans notre cas, le contrôle d'exécution n'est pas interrompu, et le type de retour est explicite, permettant une vérification statique complète des appels, donc un code beaucoup plus sûr et rapide.
- la monade
Maybe
L'utilisation la plus simple des monades consiste à encapsuler un objet de type existant dans un objet portant plus d'information.
Par exemple en langage Haskell, une monade de type Maybe(t)
est ou bien un objet de type t
normal, ou bien la valeur Nothing
. Cela permet de traiter de façon élégante les opérations interdites.
Exemple avec la monade Option https://github.com/schmittjoh/php-option
- la monade
List
Le cas de getGrandParentName : https://blog.ircmaxell.com/2013/07/taking-monads-to-oop-php.html#Another-Practical-Examp
- la monade
Logger
- la monade
IO
Une autre utilisation fondamentale des monades est la gestion des entrées/sorties dans un programme purement fonctionnel, c'est-à-dire sans effets de bord.
- il y a plusieurs bibliothèques remarquables sur l'utilisation des monades en PHP :
- https://github.com/ircmaxell/monad-php : Monad, Identity, Maybe, Chain, Deferred, ListMonad, Promise
- https://github.com/schmittjoh/php-option : Option, LazyOption, Some, None
- https://github.com/GrahamCampbell/Result-Type : Result, Success, Error
- https://github.com/whsv26/functional : Option (do notation), Some, None, Either
- https://github.com/Innmind/Immutable : Maybe, Either, RegExp
- https://github.com/marcosh/lamphpda : Identity, Either, Maybe, IO
- https://github.com/darkwood-fr/railway-fbp : Rail
En conclusion
Les monades apportent une boite à outil (ou framework) qui permet de programmer avec effets
. Elles supportent la programmation pure, dans le sens ou elles permettent de convertir un comportement impure en comportement pure grâce aux effets
. Un autre point important est de pouvoir utiliser les effets
de manière explicitement typée
, et donc de pouvoir préciser quel effet
ou side-effet
un programme pourra être confronté par l'écriture du type de données. Une dernière idée est de pouvoir écrire n'importe quelle fonction en utilisant les effect
que l'on pourrait appeler effet de polymorphisme
, comme par exemple composer les effets
vu comme séquence d'effets.
Les monades sont une des plus importantes découverte de l'informatique depuis les années 1980. Si vous souhaitez en savoir plus, le meilleur serait de les utiliser et mettre en pratique les différentes ressources citées dans cet article.
Ressources liées à l'article
- Articles :
- Wikipedia :
- Monade en Français : https://fr.wikipedia.org/wiki/Monade_(informatique)
- Monade en Anglais : https://en.wikipedia.org/wiki/Monad_(functional_programming)
- Programmation fonctionnelle en Français : https://fr.wikipedia.org/wiki/Programmation_fonctionnelle
- Programmation fonctionnelle en Anglais : https://en.wikipedia.org/wiki/Functional_programming
- Haskell programming langage : https://fr.wikipedia.org/wiki/Haskell
- Introducing monads + MonadPHP :
- Blog : https://blog.ircmaxell.com/2013/07/taking-monads-to-oop-php.html
- Dépot Github : https://github.com/ircmaxell/monad-php
- Article similaire : https://blog.emptyq.net/a?ID=00004-b2ebfe61-2306-4a38-b20b-c2618fe0ef5f
- Why monads are useful : https://jameswestby.net/tech/why-monads-are-useful.html
- Les monades dans la programmation fonctionnelle : https://www.atikteam.com/fr/blog/page/Les-Monades-Dans-La-Programmation-Fonctionnelle
- Promise is neither a Functor nor an Applicative nor a Monad https://stackoverflow.com/questions/45712106/why-are-promises-monads
- Monads and Monoids : https://bartoszmilewski.com/2017/09/06/monads-monoids-and-categories
- Wikipedia :
- PHP
- Le cas d'utilisation avec la syntaxe Yield : https://github.com/whsv26/functional/blob/14957faba58044deab7ab23fd7f00466e98445e9/doc/Monads.md
- https://github.com/ircmaxell/monad-php
- https://github.com/schmittjoh/php-option
- https://github.com/GrahamCampbell/Result-Type
- https://github.com/whsv26/functional
- https://github.com/darkwood-fr/railway-fbp
- Slides
- Functional programming slides : https://fr.slideshare.net/Mittie/monads-from-definition
- Monads in Java and slides : https://fr.slideshare.net/mariofusco/monadic-java
- YouTube
- What is a monad : https://www.youtube.com/watch?v=t1e8gqXLbsU
- Go mad for monads : https://www.youtube.com/watch?v=F5fUgXFSH0Q
- functional PHP : https://www.youtube.com/watch?v=M3_xnTK6-pA
- Douglas Crockford: Monads and Gonads : https://www.youtube.com/watch?v=dkZFtimgAcM
- Articles
- getting started GitHub resources : https://github.com/marcelgsantos/getting-started-with-fp-in-php
- relationship with Either monad : https://fsharpforfunandprofit.com/rop
- Foncteurs, Foncteurs Applicatifs et Monades https://www.moquillon.fr/index.php/post/2018/01/06/Fonctor%2C-Fonctors-Applicatifs-et-Monads