Les monades concernent la composition des fonctions et cachent la partie fastidieuse de celle-ci.
Après 7 ans à être programmeur Go, taper if err != nil
peut devenir assez fastidieux. Chaque fois que je tape if err != nil
Je remercie les Gophers pour un langage lisible avec un excellent outillage, mais en même temps je les maudis de me faire sentir comme si j’étais Bart Simpson en détention.
Je soupçonne que je ne suis pas le seulmais
Monads are not just used to hide some error handling, but can also be used for list comprehensions and concurrency, to name but a few examples.
Ne lisez pas ceci
Chez Erik Meijer Cours d’introduction à la programmation fonctionnelle sur Edxil nous demande de ne pas écrire un autre message sur monads
puisqu’il y en a déjà tellement.
Je vous recommande de regarder Vidéos de Bartosz Milewski sur la théorie des catégories qui se termine par une vidéo la meilleure explication des monades que j’ai jamais vu, plutôt que de lire ce post.
Arrêtez de lire maintenant!
Ok bien… soupir… rappelez-vous juste que je vous ai prévenu.
Avant que je puisse expliquer monads
je dois d’abord expliquer functors
. UN functor
est une superclasse de monad
ce qui signifie que tout monads
sont functors
aussi. j’utiliserai functors
dans mon explication de monads
alors s’il vous plaît ne passez pas sous silence cette section.
On peut penser à un functor
en tant que conteneur, qui contient un type d’élément.
Les exemples comprennent:
- Une tranche avec des éléments de type T :
[]T
est un conteneur où les éléments sont classés dans une liste. - Un arbre:
type Node[T any] struct { Value T; Children []Node[T] }
est un conteneur dont les éléments sont structurés en arbre ; - Une chaîne:
<-chan T
est un récipient, comme un tuyau qui contient de l’eau ; - Un pointeur :
*T
est un contenant qui peut être vide ou contenir un seul article ; - Une fonction:
func(A) T
est un conteneur, comme une boîte verrouillée, qui a d’abord besoin d’une clé, avant que vous puissiez voir l’article ; - Valeurs de retour multiples :
func() (T, error)
est un conteneur qui contient éventuellement un élément. Nous pouvons voir l’erreur dans le cadre du conteneur. A partir de maintenant, nous ferons référence à(T, error)
comme tuple.
Programmeurs non Go : Go n’a pas de types de données algébriques ni de types d’union. Cela signifie qu’au lieu d’une fonction renvoyant une valeur
or
une erreur, les programmeurs we Go renvoient une valeurand
une erreur, où l’un d’entre eux est généralement nul. Parfois, nous brisons la convention et renvoyons une valeur et une erreur, où les deux ne sont pas nulles, juste pour essayer de se confondre. Oh on s’amuse.La façon la plus populaire d’avoir des types d’union dans Go serait d’avoir une interface (classe abstraite) et ensuite d’avoir un commutateur de type (une forme très naïve de correspondance de modèle) sur le type d’interface.
L’autre exigence pour qu’un conteneur soit un functor
est que nous avons besoin d’une mise en œuvre de la fmap
fonction pour ce type de conteneur. Les fmap
function applique une fonction à chaque élément du conteneur sans modifier le conteneur ou la structure de quelque manière que ce soit.
L’exemple classique, que vous pourriez reconnaître dans MapReduce de Hadoop, Python, Ruby ou presque n’importe quel autre langage auquel vous pouvez penser, est le map
fonction pour une tranche :
Nous pouvons également mettre en œuvre fmap
pour un arbre :
Ou une chaîne :
Ou un pointeur :
Ou une fonction :
Ou une fonction qui renvoie une erreur :
Tous ces conteneurs avec leurs fmap
les implémentations sont des exemples de functors
.
Maintenant que nous comprenons qu’un functor
est juste:
- un nom abstrait pour un conteneur et
- que nous pouvons appliquer une fonction aux éléments à l’intérieur du conteneur
on peut aller droit au but : le concept abstrait d’un monad
.
UN monad
est simplement un type embelli. Hmmm… ok ça n’aide pas à l’expliquer, c’est trop abstrait. Et c’est généralement le problème d’essayer d’expliquer ce qu’est un monad
est. C’est comme essayer d’expliquer ce que sont les «effets secondaires», c’est tout simplement trop large. Permettez-moi plutôt d’expliquer la raison de l’abstraction d’un monad
. La raison est de composer des fonctions qui renvoient ces types embellis.
Commençons par la composition de fonctions simples, sans types embellis. Dans cet exemple, nous voulons composer deux fonctions f
et g
et renvoie une fonction qui prend l’entrée attendue par f
et renvoie la sortie de g
:
Évidemment, cela ne fonctionnera que si le type de sortie de f
correspond au type d’entrée de g
. Une autre version de ceci consisterait à composer des fonctions qui renvoient des erreurs.
Maintenant, nous pouvons essayer d’abstraire cette erreur comme un embellissement M
et voyons ce qu’il nous reste :
Nous devons retourner une fonction qui prend un A
comme paramètre d’entrée, nous commençons donc par déclarer la fonction de retour. Maintenant que nous avons un A
nous pouvons appeler f
et obtenir et une valeur mb
de type M[B]
mais maintenant quoi ?
Nous échouons, car c’est trop abstrait. Je veux dire maintenant que nous avons mb
, Qu’est-ce qu’on fait? Lorsque nous savions qu’il s’agissait d’une erreur, nous pouvions la vérifier, mais maintenant qu’elle est abstraite, nous ne pouvons plus. Mais… si nous savons que notre embellissement M
est aussi un functor
alors nous pouvons fmap
plus de M
:
La fonction g
que nous voulons fmap
with ne renvoie pas un type simple comme C
ça revient M[C]
. Heureusement, ce n’est pas un problème pour fmap
mais cela change un peu la signature de type :
Alors maintenant nous avons une valeur mmc
de type M[M[C]]
:
Nous avons besoin d’un moyen d’aller de M[M[C]]
pour M[C]
.
Nous avons besoin de notre embellissement M
ne pas être juste un functor
, mais aussi d’avoir une autre propriété. Cette propriété supplémentaire est une fonction appelée join
et est défini pour chaque monad
juste comme fmap
a été défini pour chaque functor
.
Étant donné la jointure, nous pouvons maintenant écrire :
Cela signifie que nous pouvons composer deux fonctions qui renvoient des types embellis, si l’embellissement définit fmap
et join
. Autrement dit, pour qu’un type soit un monad
ces deux fonctions doivent lui être définies.
Les monades sont functors
nous n’avons donc pas besoin de définir fmap
encore pour eux. Nous avons juste besoin de définir join
.
Nous allons maintenant définir join
pour:
- des listes, ce qui se traduira par des compréhensions de listes,
- erreurs, ce qui entraînera une gestion monadique des erreurs et
- canaux, ce qui se traduira par un pipeline de concurrence.
Liste des compréhensions
Join on a slice est le plus simple et probablement le plus facile pour commencer. Les join
La fonction concatène simplement toutes les tranches.
Voyons pourquoi nous avons besoin join
encore une fois, mais cette fois en se concentrant spécifiquement sur les tranches. Voici notre fonction de composition pour les tranches :
Si nous passons a
pour f
on a bs
qui est de type []B
.
Nous pouvons maintenant fmap
plus de []B
avec g
ce qui nous donnera une valeur de type [][]C
et pas []C
:
Et c’est pourquoi nous avons besoin join
. Nous devons partir de css
pour cs
ou de [][]C
pour []C
.
Prenons un exemple plus concret :
Si nous remplaçons nos types :
A
pour le typeint
,B
pour le genreint64
etC
pour le genrestring
.
Alors nos fonctions deviennent :
Maintenant, nous pouvons les utiliser dans un exemple :
Cela fait d’une tranche notre première monad
.
Il est intéressant de noter que c’est exactement ainsi que fonctionnent les compréhensions de liste dans Haskell :
Mais vous pourriez mieux le reconnaître de Python :
Gestion des erreurs monadiques
Nous pouvons également définir join
sur les fonctions qui renvoient une valeur et une erreur. Pour cela, nous devons d’abord revenir sur le fmap
fonctionnent à nouveau, à cause de certaines particularités de Go.
Voici notre fmap
fonction à nouveau pour une fonction qui renvoie une valeur et une erreur :
Nous savons que notre fonction de composition va appeler fmap
avec une fonction f
qui renvoie également une erreur. Cela se traduira par notre fmap
signature ressemblant à ceci :
Malheureusement, les tuples ne sont pas des citoyens de première classe en Go, nous ne pouvons donc pas écrire :
Il existe plusieurs façons de contourner ce problème. Je préfère utiliser une fonction, car les fonctions, qui renvoient un tuple, sont toujours des citoyens de première classe :
Nous pouvons maintenant définir notre fmap
pour les fonctions qui renvoient une valeur et une erreur, en utilisant notre solution :
Ce qui nous ramène à notre point principal, notre join
fonction sur (func() (C, error), error)
. C’est assez simple et fait simplement l’une des vérifications d’erreur pour nous.
Nous pouvons maintenant utiliser notre fonction de composition, puisque nous avons défini join
et fmap
:
Cela nous oblige à faire moins de vérifications d’erreurs, puisque le monad
le fait pour nous en arrière-plan en utilisant le join
une fonction.
Voici un autre Exempleoù je me sens comme Bart Simpson :
Techniquement compose
pourrait prendre plus de deux fonctions comme paramètres. Cela signifie que nous pourrions enchaîner toutes les fonctions ci-dessus en un seul appel et réécrire l’exemple ci-dessus :
Il y a bien d’autres monads
là-bas. Pensez à deux fonctions qui renvoient le même type d’embellissement et que vous aimeriez composer. Faisons un autre exemple.
Pipelines simultanés
Nous pouvons également définir join
sur les chaînes.
Ici, nous avons un canal in
qui nous alimentera plus de canaux de type T
. Nous créons d’abord le out
canal, démarrez une routine aller, qui sera utilisée pour l’alimenter, puis retournez-le. À l’intérieur de la routine go, nous commençons une nouvelle routine go pour chacun des canaux lus à partir de in
. Ces routines de go envoient leurs événements entrants à out
, fusionnant les multiples entrées en un seul flux. Enfin, nous utilisons un groupe d’attente pour nous assurer que nous fermons le out
canal une fois que toutes les entrées sont reçues.
Bref on lit tout T
s de in
et les poussant tous vers le out
canal.
Programmeurs Non Go : je dois réussir
c
comme paramètre de la routine de go interne, carc
est une variable unique qui prend la valeur de chaque élément du canal. Cela signifie que si nous l’utilisions simplement, à l’intérieur de la fermeture au lieu de créer une copie de la valeur en la passant comme paramètre, nous ne serions probablement en train de lire que depuis le canal le plus récent. C’est une erreur courante commise par les programmeurs go.
Cela signifie que nous pouvons définir une fonction de composition sur les fonctions qui renvoient des canaux.
Et à cause de la façon dont join
est implémenté, nous obtenons la simultanéité presque gratuitement.
Si vous souhaitez utiliser ces outils, vous pouvez les utiliser dans Go right en utilisant des génériques ou la génération de code avec dérivantqui vous permet de composer plus de deux fonctions à la fois.
C’était une explication très vague de monads
et il y a beaucoup de choses que j’ai intentionnellement laissées de côté, pour simplifier les choses, mais il y a encore une chose que j’aimerais couvrir.
Techniquement, notre fonction de composition définie dans la section précédente s’appelle la Kleisli Arrow
.
Quand les gens parlent de monads
ils mentionnent rarement le Kleisli Arrow
qui a été pour moi la clé de la compréhension monads
. Si vous avez de la chance, ils l’expliquent en utilisant fmap
et join
mais si vous êtes malchanceux, comme moi, ils l’expliquent en utilisant la fonction bind.
Pourquoi?
Car bind
est la fonction dans Haskell que vous devez implémenter pour votre type si vous voulez qu’il soit considéré comme un Monad
.
Répétons notre implémentation de la fonction compose ici :
Si la bind
fonction a été implémentée, nous pourrions simplement l’appeler, au lieu de fmap
et join
.
Ce qui signifie que bind(mb, g)
= join(fmap(g, mb))
.
Les bind
fonction pour les listes serait concatMap
ou alors flatMap
selon la langue.
Loucher
J’ai trouvé que Go commençait à brouiller les lignes pour moi entre bind
et le Kleisli Arrow
. Go renvoie une erreur dans un tuple, mais un tuple n’est pas un citoyen de première classe. Par exemple, ce code ne compilera pas, car vous ne pouvez pas passer f
les résultats à g
de manière en ligne :
Vous devez l’écrire :
Ou vous devez faire g
prendre une fonction comme entrée, puisque les fonctions sont des citoyens de première classe.
Mais cela signifie que notre fonction bind :
tel que défini pour les erreurs :
ne sera pas amusant à utiliser, à moins que nous n’écrasions ce tuple dans une fonction :
Si nous louchons, nous pouvons également voir notre tuple de retour comme une fonction :
Et si nous louchons à nouveau, alors nous pouvons voir que c’est notre fonction de composition, où f
prend juste zéro paramètre:
Ta da, nous avons notre Kleisli Arrow
juste en louchant quelques fois.
Les monades font abstraction d’une partie de la logique répétée des fonctions de composition avec des types embellis, de sorte que vous n’ayez pas à vous sentir comme Bart Simpson en détention, mais plutôt comme Bart Simpson sur sa planche à roulettes, jouant une balle de cricket, alors que c’est à son tour de chauve souris.
Si vous voulez essayer monads
et d’autres concepts de programmation fonctionnelle en Go, alors vous pouvez le faire en utilisant mon générateur de code, GoDérive.
Avertissement : L’un des concepts clés de la programmation fonctionnelle est l’immuabilité. Cela rend non seulement les programmes plus faciles à raisonner, mais permet également des optimisations du compilateur. Pour simuler cette immuabilité, en Go, vous aurez tendance à copier beaucoup de structures qui conduiront à des performances non optimales. La raison pour laquelle les langages de programmation fonctionnels s’en sortent est exactement parce qu’ils peuvent compter sur l’immuabilité et toujours pointer vers les anciennes valeurs, au lieu de les copier à nouveau.
Si vous voulez vraiment passer à la programmation fonctionnelle, je vous recommande Orme. C’est un langage de programmation fonctionnel typé statiquement pour le front-end. Il est aussi facile à apprendre pour un langage fonctionnel, que Go l’est à apprendre pour un langage impératif. j’ai fait ça guider en une journée et j’ai pu commencer à être productif ce soir-là. Le créateur a fait tout son possible pour en faire un langage facile à apprendre en supprimant même le besoin de comprendre les monades. j’ai personnellement trouvé
Elm
une joie à utiliser dans le front-end en conjonction avec Go dans le back-end. Si vous commencez à vous ennuyer dans Go and Elm, ne vous inquiétez pas, il y a beaucoup plus à apprendre, Haskell vous attend.