En termes qu'un OOP programmeur comprendrait (sans aucun arrière-plan de programmation fonctionnelle), qu'est-ce qu'une monade?
Quel problème cela résout-il et quels sont les endroits les plus couramment utilisés?
EDIT:
Pour clarifier le type de compréhension que je cherchais, supposons que vous convertissiez une application FP comportant des monades en une application OOP. Que feriez-vous pour transférer les responsabilités des monades vers l'application OOP?
MISE À JOUR: Cette question a fait l’objet d’une longue série de blogs, que vous pouvez lire à Monads - merci pour cette excellente question!
En ce qui concerne un OOP programmeur (sans aucun fond de programmation fonctionnelle), qu’est-ce qu’une monade?
Une monade est un "amplificateur" de types qui obéit à certaines règles et qui comporte certaines opérations. .
Premièrement, qu'est-ce qu'un "amplificateur de types"? J'entends par là un système qui vous permet de prendre un type et de le transformer en un type plus spécial. Par exemple, en C #, considérons Nullable<T>
. C'est un amplificateur de types. Il vous permet de prendre un type, par exemple int
, et d'ajouter une nouvelle fonctionnalité à ce type, à savoir qu'il peut maintenant être null alors qu'il ne le pouvait pas auparavant.
Comme second exemple, considérons IEnumerable<T>
. C'est un amplificateur de types. Il vous permet de prendre un type, par exemple string
, et d'ajouter une nouvelle fonctionnalité à ce type, à savoir que vous pouvez désormais créer une séquence de chaînes à partir d'un nombre illimité de chaînes simples.
Quelles sont les "certaines règles"? En bref, il existe un moyen judicieux pour les fonctions sur le type sous-jacent de travailler sur le type amplifié de manière à respecter les règles normales de composition fonctionnelle. Par exemple, si vous avez une fonction sur les entiers, dites
int M(int x) { return x + N(x * 2); }
alors la fonction correspondante sur Nullable<int>
peut faire en sorte que tous les opérateurs et appels qui y travaillent travaillent ensemble "de la même manière" qu’ils le faisaient auparavant.
(C'est incroyablement vague et imprécis; vous avez demandé une explication qui ne supposait rien de la connaissance de la composition fonctionnelle.)
Quelles sont les "opérations"?
Il existe une opération "unit" (parfois appelée "opération de retour", ce qui prête à confusion, prend une valeur d'un type brut et crée la valeur monadique équivalente. Ceci, en substance, fournit un moyen de prendre une valeur d'un type non amplifié et de la transformer en une valeur du type amplifié. Il pourrait être implémenté en tant que constructeur dans un langage OO.
Il existe une opération "bind" qui prend une valeur monadique et une fonction qui peut transformer la valeur et renvoie une nouvelle valeur monadique. Bind est l'opération de clé qui définit la sémantique de la monade. Il nous permet de transformer les opérations sur le type non amplifié en opérations sur le type amplifié, qui obéit aux règles de composition fonctionnelle mentionnées précédemment.
Il existe souvent un moyen de sortir le type non amplifié du type amplifié. Strictement parlant, cette opération n'est pas nécessaire pour avoir une monade. (Bien que cela soit nécessaire si vous voulez avoir un comonad . Nous ne les considérerons pas plus loin dans cet article.)
Encore une fois, prenons Nullable<T>
comme exemple. Vous pouvez transformer int
en Nullable<int>
avec le constructeur. Le compilateur C # s’occupe de la plupart des opérations de "levage" nullables, mais si ce n’était pas le cas, la transformation de levage est simple: une opération, par exemple,
int M(int x) { whatever }
est transformé en
Nullable<int> M(Nullable<int> x)
{
if (x == null)
return null;
else
return new Nullable<int>(whatever);
}
Et reconvertir Nullable<int>
en int
se fait avec la propriété Value
.
C'est la transformation de fonction qui constitue le bit clé. Notez que la sémantique réelle de l'opération nullable - qu'une opération sur un null
propage le null
- est capturée dans la transformation. Nous pouvons généraliser cela.
Supposons que vous ayez une fonction de int
à int
, semblable à notre M
d'origine. Vous pouvez facilement en faire une fonction qui prend un int
et retourne un Nullable<int>
parce que vous pouvez simplement exécuter le résultat par le biais du constructeur nullable. Supposons maintenant que vous ayez cette méthode d'ordre supérieur:
static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
if (amplified == null)
return null;
else
return func(amplified.Value);
}
Vous voyez ce que vous pouvez faire avec ça? Toute méthode prenant un int
et renvoyant un int
, ou prenant un int
et renvoyant un Nullable<int>
peut maintenant se voir appliquer la sémantique nullable .
De plus: supposons que vous ayez deux méthodes
Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }
et vous voulez les composer:
Nullable<int> Z(int s) { return X(Y(s)); }
C'est-à-dire que Z
est la composition de X
et Y
. Mais vous ne pouvez pas le faire car X
prend un int
et Y
renvoie un Nullable<int>
. Mais puisque vous avez l'opération "bind", vous pouvez faire en sorte que cela fonctionne:
Nullable<int> Z(int s) { return Bind(Y(s), X); }
L'opération de liaison sur une monade est ce qui permet à la composition de fonctions sur des types amplifiés de fonctionner. de composition de fonction normale; que la composition avec des fonctions d'identité donne la fonction d'origine, que la composition est associative, etc.
En C #, "Bind" s'appelle "SelectMany". Regardez comment cela fonctionne sur la monade en séquence. Il faut deux choses: transformer une valeur en séquence et lier des opérations sur des séquences. En bonus, nous avons également "transformer une séquence en valeur". Ces opérations sont:
static IEnumerable<T> MakeSequence<T>(T item)
{
yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
// let's just take the first one
foreach(T item in sequence) return item;
throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
foreach(T item in seq)
foreach(T result in func(item))
yield return result;
}
La règle de la monade nullable consistait à "combiner deux fonctions produisant des valeurs nullables ensemble pour vérifier si la fonction interne aboutissait à null; si tel est le cas, génère la valeur null, sinon, appelle la fonction externe avec le résultat". C'est la sémantique souhaitée de nullable.
La règle de la monade de séquence consiste à "combiner deux fonctions qui produisent des séquences, appliquer la fonction externe à chaque élément produit par la fonction interne, puis concaténer toutes les séquences résultantes ensemble". La sémantique fondamentale des monades est capturée dans les méthodes Bind
/SelectMany
; c'est la méthode qui vous dit ce que la monade signifie vraiment .
Nous pouvons faire encore mieux. Supposons que vous avez une séquence d'ints et une méthode qui enregistre et génère des séquences de chaînes. Nous pourrions généraliser l'opération de liaison pour permettre la composition de fonctions qui prennent et retournent différents types amplifiés, à condition que les entrées de l'une correspondent aux sorties de l'autre:
static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
foreach(T item in seq)
foreach(U result in func(item))
yield return result;
}
Nous pouvons donc maintenant dire "amplifier ce groupe d’entiers individuels en une séquence d’entiers. Transformez cet entier particulier en un groupe de chaînes, amplifié en une séquence de chaînes. toutes les séquences de chaînes. " Les monades vous permettent de composer vos amplifications.
Quel problème cela résout-il et quels sont les endroits les plus couramment utilisés?
C'est un peu comme si on demandait "quels problèmes le motif singleton résout-il?", Mais je vais tenter le coup.
Les monades sont généralement utilisées pour résoudre des problèmes tels que:
C # utilise des monades dans sa conception. Comme déjà mentionné, le motif nullable ressemble beaucoup à la "monade peut-être". LINQ est entièrement construit à partir de monades; la méthode SelectMany
est ce que fait le travail sémantique de composition des opérations. (Erik Meijer aime bien souligner que chaque fonction LINQ pourrait en réalité être implémentée par SelectMany
; tout le reste n’est qu’une commodité.)
Pour clarifier le type de compréhension que je cherchais, supposons que vous convertissiez une application FP comportant des monades en une application OOP. Que feriez-vous pour transférer les responsabilités des monades dans l'application OOP?
La plupart des langages OOP ne disposent pas d'un système de types assez riche pour représenter directement le motif monad; vous avez besoin d'un système de types prenant en charge les types de types supérieurs aux types génériques. Donc je n'essaierais pas de faire ça. Je mettrais plutôt en œuvre des types génériques représentant chaque monade, ainsi que des méthodes représentant les trois opérations nécessaires: convertir une valeur en valeur amplifiée, (éventuellement) transformer une valeur amplifiée en valeur et transformer une fonction de valeurs non amplifiées en une fonction sur les valeurs amplifiées.
Un bon point de départ est la manière dont nous avons implémenté LINQ en C #. Étudiez la méthode SelectMany
; c'est la clé pour comprendre le fonctionnement de la monade en C #. C'est une méthode très simple, mais très puissante!
Suggestions, lectures complémentaires:
Ensuite, nous avons un premier gros problème. Ceci est un programme:
f(x) = 2 * x
g(x,y) = x / y
Comment pouvons-nous dire ce qui doit être exécuté en premier? Comment pouvons-nous former une séquence ordonnée de fonctions (c'est-à-dire n programme) en n'utilisant rien d'autre que des fonctions?
Solution: fonctions de composition. Si vous voulez d'abord g
puis f
, écrivez simplement f(g(x,y))
. OK mais ...
Autres problèmes: certaines fonctions peuvent échouer (c'est-à-dire g(2,0)
, diviser par 0). Nous avons pas "d'exceptions" en FP. Comment le résolvons-nous?
Laissons autorisons les fonctions à renvoyer deux types de choses: au lieu d'avoir g : Real,Real -> Real
(fonction de deux réels en un réel), permettons à g : Real,Real -> Real | Nothing
(fonction de deux réels en (réel ou rien)).
Mais les fonctions devraient (pour être plus simples) ne renvoyer que ne chose.
Solution: créons un nouveau type de données à renvoyer, un "type de boxe" qui renferme peut-être un réel ou ne sera tout simplement rien. Par conséquent, nous pouvons avoir g : Real,Real -> Maybe Real
. OK mais ...
Qu'advient-il maintenant de f(g(x,y))
? f
n'est pas prêt à consommer un Maybe Real
. Et nous ne voulons pas changer toutes les fonctions auxquelles nous pourrions nous connecter avec g
pour consommer un Maybe Real
.
Solution: avons une fonction spéciale pour "relier"/"composer"/"relier" des fonctions. De cette façon, nous pouvons, en coulisse, adapter la sortie d’une fonction à l’alimentation de la suivante.
Dans notre cas: g >>= f
(connectez/composez g
à f
). Nous voulons que >>=
récupère la sortie de g
, l'examine et, au cas où il s'agit de Nothing
, n'appelle pas f
et ne retourne Nothing
; ou au contraire, extrayez la boîte Real
et introduisez f
avec elle. (Cet algorithme est juste l'implémentation de >>=
pour le type Maybe
).
De nombreux autres problèmes peuvent être résolus en utilisant le même schéma: 1. Utilisez une "boîte" pour codifier/stocker différentes significations/valeurs et disposez de fonctions telles que g
qui renvoient ces "valeurs encadrées". 2. Demandez aux compositeurs/lieurs g >>= f
d'aider à connecter la sortie de g
à l'entrée de f
, de sorte que nous n'ayons pas besoin de changer du tout f
.
Les problèmes remarquables pouvant être résolus avec cette technique sont les suivants:
avoir un état global que toutes les fonctions de la séquence de fonctions ("le programme") peuvent partager: solution StateMonad
.
Nous n'aimons pas les "fonctions impures": les fonctions qui produisent une sortie différente pour une entrée identique . Par conséquent, marquons ces fonctions en leur faisant renvoyer une valeur tagged/boxed: IO
monad.
Bonheur total !!!!
Je dirais que l'analogie OO la plus proche des monades est le " modèle de commande ".
Dans le modèle de commande, vous enroulez une instruction ou une expression ordinaire dans un objet de commande . L'objet de commande expose une méthode execute qui exécute l'instruction encapsulée. Ainsi, les instructions sont transformées en objets de première classe pouvant être transmis et exécutés à volonté. Les commandes peuvent être composées afin que vous puissiez créer un objet programme en enchaînant et en imbriquant des objets commande.
Les commandes sont exécutées par un objet séparé, l'appelant . L'avantage d'utiliser le modèle de commande (plutôt que d'exécuter simplement une série d'instructions ordinaires) est que différents invocateurs peuvent appliquer une logique différente à la manière dont les commandes doivent être exécutées.
Le modèle de commande peut être utilisé pour ajouter (ou supprimer) des fonctionnalités linguistiques non prises en charge par la langue de l'hôte. Par exemple, dans un langage hypothétique OO sans exceptions, vous pouvez ajouter une sémantique d'exception en exposant les méthodes "try" et "throw" aux commandes. Lorsqu'une commande appelle throw, l'invocateur revient dans la liste (ou l'arborescence) de commandes jusqu'au dernier appel "try". Inversement, vous pouvez supprimer la sémantique des exceptions d’une langue (si vous croyez les exceptions sont mauvaises ) en capturant toutes les exceptions émises par chaque commande et en les transformant en codes d’erreur qui sont ensuite transmis à la commande suivante.
Même une sémantique d'exécution plus sophistiquée telle que des transactions, une exécution non déterministe ou des suites peut être implémentée de la sorte dans un langage qui ne la prend pas en charge de manière native. C'est un modèle assez puissant si vous y réfléchissez.
Or, en réalité, les modèles de commande ne sont pas utilisés comme caractéristique de langage général comme celle-ci. Les frais généraux liés à la conversion de chaque instruction en une classe distincte entraîneraient une quantité insupportable de code passe-partout. Mais en principe, il peut être utilisé pour résoudre les mêmes problèmes que les monades pour résoudre en fp.
En ce qui concerne un OOP programmeur (sans aucun fond de programmation fonctionnelle), qu’est-ce qu’une monade?
Quel problème cela résout-il et quels sont les endroits les plus couramment utilisés? Quels sont les endroits les plus couramment utilisés?
En termes de OO programmation, un monade est une interface (ou plutôt un mixin), paramétrée par un type, avec deux méthodes, return
et bind
qui décrivent:
Le problème qu'il résout est le même type de problème que n'importe quelle interface, à savoir: "J'ai un groupe de classes différentes qui font des choses différentes, mais qui semblent les faire d'une manière qui présente une similitude sous-jacente. puis-je décrire cette similitude entre eux, même si les classes elles-mêmes ne sont pas vraiment des sous-types de quelque chose de plus proche que la classe "Object" elle-même? "
Plus spécifiquement, la Monad
"interface" est similaire à IEnumerator
ou IIterator
en ce sens qu'elle prend un type qui prend lui-même un type. Le "point" principal de Monad
est toutefois de pouvoir connecter des opérations basées sur le type intérieur, même au point de créer un nouveau "type interne", tout en conservant - voire en améliorant - la structure d’information du principal. classe.
Vous avez une présentation récente "Monadologie - aide professionnelle sur le type anxiété" de Christopher League (12 juillet 2010), ce qui est assez intéressant sur des sujets de continuation et de monade.
La vidéo accompagnant cette présentation (slideshare) est en réalité disponible sur vimeo.
La partie Monad commence environ 37 minutes, sur cette vidéo d’une heure, et commence par la diapositive 42 de ses 58 diapositives.
Il est présenté comme "le modèle de conception principal pour la programmation fonctionnelle", mais le langage utilisé dans les exemples est Scala, qui est à la fois OOP et fonctionnel.
Vous pouvez en savoir plus sur Monad dans Scala dans le billet de blog " Monads - Une autre façon de résumer les calculs dans Scala ", de Debasish Ghosh (27 mars 2008).
Un constructeur de type M est un monade s'il prend en charge ces opérations:
# the return function
def unit[A] (x: A): M[A]
# called "bind" in Haskell
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]
# Other two can be written in term of the first two:
def map[A,B] (m: M[A]) (f: A => B): M[B] =
flatMap(m){ x => unit(f(x)) }
def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
flatMap(ma){ x => mb }
Ainsi, par exemple (en scala):
Option
est une monadeunité de def [A] (x: A): Option [A] = Un peu (x) def flatMap [A, B] (m: Option [A]) (f: A => Option [B]): Option [B] = m correspond à { cas Aucun => Aucun cas Certains (x) => f (x ) }
List
est Monadunité de def [A] (x: A): Liste [A] = Liste (x) def flatMap [A, B] (m: Liste [A]) (f: A => Liste [B]): Liste [B] = m correspondance { cas Nil => Nil cas x :: xs => f(x) ::: flatMap (xs) (f) }
Monad est un gros problème dans Scala en raison de sa syntaxe pratique conçue pour tirer parti des structures Monad:
for
compréhension en Scala :
for {
i <- 1 to 4
j <- 1 to i
k <- 1 to j
} yield i*j*k
est traduit par le compilateur en:
(1 to 4).flatMap { i =>
(1 to i).flatMap { j =>
(1 to j).map { k =>
i*j*k }}}
L'abstraction de clé est la flatMap
, qui lie le calcul par le chaînage.
Chaque appel de flatMap
renvoie le même type de structure de données (mais de valeur différente), qui sert d'entrée à la commande suivante dans la chaîne.
Dans l'extrait de code ci-dessus, flatMap prend en entrée une fermeture (SomeType) => List[AnotherType]
et renvoie un List[AnotherType]
. Le point important à noter est que tous les flatMaps prennent le même type de fermeture en entrée et renvoient le même type en sortie.
C'est ce qui "lie" le fil de calcul - chaque élément de la séquence du for-comprehension doit respecter cette même contrainte de type.
Si vous prenez deux opérations (cela peut échouer) et passez le résultat à la troisième, comme:
lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]
mais sans profiter de Monad, vous obtenez un code OOP alambiqué comme:
val user = getLoggedInUser(session)
val confirm =
if(!user.isDefined) None
else lookupVenue(name) match {
case None => None
case Some(venue) =>
val confno = reserveTable(venue, user.get)
if(confno.isDefined)
mailTo(confno.get, user.get)
confno
}
tandis qu'avec Monad, vous pouvez travailler avec les types actuels (Venue
, User
) comme toutes les opérations fonctionnent et garder caché le travail de vérification Option, en raison des flatmaps de la syntaxe for:
val confirm = for {
venue <- lookupVenue(name)
user <- getLoggedInUser(session)
confno <- reserveTable(venue, user)
} yield {
mailTo(confno, user)
confno
}
La partie de rendement ne sera exécutée que si les trois fonctions ont Some[X]
; toute None
serait directement renvoyée à confirm
.
Alors:
Les monades permettent un calcul ordonné dans la programmation fonctionnelle, ce qui nous permet de modéliser le séquencement des actions sous une forme structurée Nice, un peu comme un DSL.
Et le plus grand pouvoir vient avec la capacité de composer des monades qui servent différents objectifs, en abstractions extensibles au sein d'une application.
Cette séquence et la mise en séquence d'actions par une monade sont effectuées par le compilateur de langage qui effectue la transformation par la magie des fermetures.
En passant, Monad n’est pas seulement un modèle de calcul utilisé en PF:
La théorie des catégories propose de nombreux modèles de calcul. Parmi eux
- le modèle de calcul Arrow
- le modèle de calcul Monad
- le modèle applicatif de calculs
J'ai écrit un court article comparant le code standard OOP python au code monadique python illustrant le processus de calcul sous-jacent à l'aide de diagrammes. Cela ne suppose aucune connaissance préalable de la PF. J'espère que vous le trouverez utile - http://nikgrozev.com/2013/12/10/monads-in-15-minutes/
Pour respecter les lecteurs rapides, je commence par une définition précise, puis par une explication rapide plus "simple", puis par des exemples.
Voici une définition à la fois concise et précise légèrement reformulé:
Une monade (en informatique) est formellement une carte qui:
envoie chaque type
X
d'un langage de programmation donné à un nouveau typeT(X)
(appelé "type deT
- calculs avec des valeurs dansX
");équipé d'une règle pour composer deux fonctions de la forme
f:X->T(Y)
etg:Y->T(Z)
en une fonctiong∘f:X->T(Z)
;de manière associative dans le sens évident et unital par rapport à une fonction d'unité donnée appelée
pure_X:X->T(X)
, à considérer comme une valeur du calcul pur qui renvoie simplement cette valeur.
Donc, en termes simples, une monade est une règle à passer de tout type X
à un autre type T(X)
, et une règle à passer de deux fonctions f:X->T(Y)
et g:Y->T(Z)
(que vous voudriez composer mais que vous ne pouvez pas écrire) à une nouvelle fonction h:X->T(Z)
. Ce qui cependant n'est pas la composition au sens strict du terme. Nous sommes essentiellement en train de "plier" la composition de la fonction ou de redéfinir la manière dont les fonctions sont composées.
De plus, nous avons besoin de la règle de composition de la monade pour satisfaire les axiomes mathématiques "évidents":
f
avec g
puis avec h
(de l'extérieur) devrait être identique à la composition g
avec h
puis avec f
(de l'intérieur).f
avec la fonction identité de chaque côté devrait donner f
.Encore une fois, avec des mots simples, nous ne pouvons pas nous contenter de redéfinir notre composition de fonction comme nous le souhaitons:
f(g(h(k(x)))
, sans vous soucier de spécifier l'ordre des paires de fonctions de composition. Comme la règle de la monade prescrit seulement comment composer une paire de fonctions , sans cet axiome, nous aurions besoin de savoir quelle paire est composée en premier et ainsi de suite. (Notez que cette propriété est différente de la propriété de commutativité que f
composée avec g
était identique à g
composée avec f
, ce qui n’est pas obligatoire).Encore une fois en bref: une monade est la règle de l'extension du type et des fonctions de composition satisfaisant les deux axiomes - associativité et propriété unital.
Concrètement, vous voulez que la monade soit implémentée pour vous par le langage, le compilateur ou le framework qui se chargera de composer les fonctions pour vous. Vous pouvez donc vous concentrer sur l'écriture de la logique de votre fonction plutôt que sur la manière dont leur exécution est mise en œuvre.
C'est essentiellement ça, en un mot.
Étant mathématicien professionnel, je préfère éviter d’appeler h
la "composition" de f
et g
. Parce que mathématiquement, ça ne l'est pas. L'appeler la "composition" suppose à tort que h
est la vraie composition mathématique, ce qu'elle n'est pas. Il n'est même pas déterminé de manière unique par f
et g
. Au lieu de cela, il est le résultat de la nouvelle "règle de composition" de notre monade. Ce qui peut être totalement différent de la composition mathématique réelle même si celle-ci existe!
Monad est pas un foncteur ! Un foncteur F
est une règle permettant de passer du type X
au type F(X)
et aux fonctions (morphisme) entre les types X
et Y
aux fonctions comprises entre F(X)
et F(Y)
(envoi d'objets à objets et leurs morphismes aux morphismes en théorie des catégories). Au lieu de cela, une monade envoie une paire de fonctions f
et g
à une nouvelle h
.
Pour le rendre moins sec, permettez-moi de vous illustrer par un exemple: j'annule de petites sections afin que vous puissiez aller directement au but.
Supposons que nous voulions composer deux fonctions:
f: x -> 1 / x
g: y -> 2 * y
Mais f(0)
n'est pas défini, donc une exception e
est levée. Alors comment définir la valeur de composition g(f(0))
? Lancer une exception à nouveau, bien sûr! Peut-être le même e
. Peut-être une nouvelle exception mise à jour e1
.
Qu'est-ce qui se passe précisément ici? Premièrement, nous avons besoin de nouvelles valeurs d’exception (différentes ou identiques). Vous pouvez les appeler nothing
ou null
ou peu importe, mais l’essence reste la même - elles doivent être de nouvelles valeurs, par exemple. il ne devrait pas s'agir d'une number
dans notre exemple ici. Je préfère ne pas les appeler null
pour éviter toute confusion avec la façon dont null
peut être implémenté dans un langage spécifique. De même, je préfère éviter nothing
car il est souvent associé à null
, qui, en principe, est ce que null
devrait faire, mais ce principe est souvent invoqué pour des raisons pratiques.
C’est un sujet banal pour tout programmeur expérimenté, mais j’aimerais laisser quelques mots pour dissiper toute confusion:
Exception est un objet encapsulant des informations sur la manière dont le résultat d'exécution non valide s'est produit.
Cela peut aller de jeter tous les détails et de renvoyer une seule valeur globale (comme NaN
ou null
), ou de générer une longue liste de journaux ou ce qui est exactement arrivé, de l'envoyer à une base de couche de stockage de données;)
La différence importante entre ces deux exemples extrêmes d’exception est que, dans le premier cas, il n’existe aucun effet secondaire . Dans la seconde il y a. Ce qui nous amène à la question (en milliers de dollars):
Réponse plus courte : Oui, mais uniquement si elles ne provoquent pas d'effets secondaires.
Réponse plus longue. Pour être pure, la sortie de votre fonction doit être déterminée uniquement par son entrée. Nous modifions donc notre fonction f
en envoyant 0
à la nouvelle valeur abstraite e
que nous appelons exception. Nous nous assurons que la valeur e
ne contient aucune information extérieure qui ne soit déterminée uniquement par notre entrée, qui est x
. Voici donc un exemple d'exception sans effet secondaire:
e = {
type: error,
message: 'I got error trying to divide 1 by 0'
}
Et en voici un avec effet secondaire:
e = {
type: error,
message: 'Our committee to decide what is 1/0 is currently away'
}
En réalité, cela n'a d'effets secondaires que si ce message peut éventuellement changer à l'avenir. Mais si elle est garantie de ne jamais changer, cette valeur devient particulièrement prévisible, et il n'y a donc aucun effet secondaire.
Pour le rendre encore plus bête. Une fonction retournant 42
ever est clairement pure. Mais si quelqu'un de fou décide de faire de 42
une variable que la valeur peut changer, la même fonction cesse d'être pure dans les nouvelles conditions.
Notez que j'utilise la notation littérale d'objet pour plus de simplicité afin de démontrer l'essence. Malheureusement, les langages tels que JavaScript sont perturbés, où error
n'est pas un type qui se comporte comme nous le voulons ici en ce qui concerne la composition des fonctions, alors que les types réels tels que null
ou NaN
do ne vous comportez pas de cette façon, mais passez plutôt par des conversions de type artificiel et pas toujours intuitif.
Comme nous voulons faire varier le message à l'intérieur de notre exception, nous déclarons vraiment un nouveau type E
pour tout l'objet exception, puis c'est ce que fait le maybe number
, à part son nom déroutant, qui consiste à soit de type number
, soit du nouveau type d'exception E
, il s'agit donc vraiment de l'union number | E
de number
et E
. Cela dépend en particulier de la manière dont nous voulons construire E
, ce qui n’est ni suggéré ni reflété dans le nom maybe number
.
C'est l'opération mathématique prenant les fonctions f: X -> Y
et g: Y -> Z
et en construisant leur composition comme fonction h: X -> Z
satisfaisant h(x) = g(f(x))
. Le problème avec cette définition se produit lorsque le résultat f(x)
n'est pas autorisé en tant qu'argument de g
.
En mathématiques, ces fonctions ne peuvent être composées sans travail supplémentaire. La solution strictement mathématique pour notre exemple ci-dessus de f
et g
consiste à supprimer 0
de l'ensemble de la définition de f
. Avec ce nouvel ensemble de définitions (nouveau type plus restrictif de x
), f
devient composable avec g
.
Cependant, il n’est pas très pratique en programmation de restreindre l’ensemble de définitions de f
comme cela. Au lieu de cela, des exceptions peuvent être utilisées.
Ou bien, une autre approche consiste à créer des valeurs artificielles telles que NaN
, undefined
, null
, Infinity
etc. Ainsi, vous évaluez 1/0
à Infinity
et 1/-0
à -Infinity
. Et forcez ensuite la nouvelle valeur dans votre expression au lieu de lancer une exception. Pour aboutir à des résultats que vous pouvez trouver ou non prévisibles:
1/0 // => Infinity
parseInt(Infinity) // => NaN
NaN < 0 // => false
false + 1 // => 1
Et nous sommes de retour aux numéros normaux prêts à avancer;)
JavaScript nous permet de continuer à exécuter des expressions numériques à tout prix sans générer d'erreurs, comme dans l'exemple ci-dessus. Cela signifie que cela permet également de composer des fonctions. C’est exactement ce qu’est la monade: c’est une règle de composer des fonctions satisfaisant les axiomes définis au début de cette réponse.
Mais la règle de la fonction de composition, issue de l'implémentation de JavaScript pour traiter les erreurs numériques, est-elle une monade?
Pour répondre à cette question, tout ce dont vous avez besoin est de vérifier les axiomes (laissé comme exercice ne faisant pas partie de la question ici;).
En fait, une règle plus utile serait plutôt la règle prescrivant que si f
lève une exception pour un certain x
, sa composition le fait avec tout g
. De plus, créez l'exception E
globalement unique avec une seule valeur possible ( objet terminal en théorie des catégories). Maintenant les deux axiomes sont vérifiables instantanément et nous obtenons une monade très utile. Et le résultat est ce qui est connu sous le nom de peut-être monade .
Une monade est un type de données qui encapsule une valeur et à laquelle, essentiellement, deux opérations peuvent être appliquées:
return x
crée une valeur du type monad qui encapsule x
m >>= f
(lu comme "l'opérateur de liaison") applique la fonction f
à la valeur de la monad m
C'est ce qu'est une monade. Il y a encore quelques détails techniques , mais fondamentalement, ces deux opérations définissent une monade. La vraie question est: "Qu'est-ce qu'une monade fait ?", Et cela dépend des monades - les listes sont des monades, Maybes sont des monades, IO les opérations sont des monades. Tout ce que cela signifie quand on dit que ces choses sont des monades, c'est qu'elles ont l'interface monad de return
et >>=
.
De wikipedia :
En programmation fonctionnelle, une monade est une sorte de type de données abstrait utilisé pour représenter des calculs (au lieu de données dans le modèle de domaine). Les monades permettent au programmeur d'enchaîner des actions pour créer un pipeline, dans lequel chaque action est décorée avec des règles de traitement supplémentaires fournies par la monade. Les programmes écrits en style fonctionnel peuvent utiliser des monades pour structurer des procédures comprenant des opérations séquencées, 1 [2] ou pour définir des flux de contrôle arbitraires (comme la gestion des accès concurrents, des continuations ou des exceptions).
Formellement, une monade est construite en définissant deux opérations (bind et return) et un constructeur de type M qui doit remplir plusieurs propriétés pour permettre la composition correcte des fonctions monadiques (c'est-à-dire des fonctions qui utilisent les valeurs de la monad comme arguments). L'opération return prend une valeur d'un type brut et la place dans un conteneur monadique de type M. L'opération bind effectue le processus inverse, en extrayant la valeur d'origine du conteneur et en la passant à la fonction suivante associée dans le pipeline.
Un programmeur composera des fonctions monadiques pour définir un pipeline de traitement de données. La monade agit comme un cadre, car il s'agit d'un comportement réutilisable qui décide de l'ordre dans lequel les fonctions monadiques spécifiques du pipeline sont appelées et gère tous les travaux d'infiltration requis par le calcul [3]. Les opérateurs de liaison et de retour entrelacés dans le pipeline seront exécutés après le retour de chaque fonction monadique et prendront en charge les aspects particuliers traités par la monade.
Je crois que cela l'explique très bien.
Je vais essayer de faire la définition la plus courte possible à l'aide de OOP termes:
Une classe générique CMonadic<T>
est un monade si elle définit au moins les méthodes suivantes:
class CMonadic<T> {
static CMonadic<T> create(T t); // a.k.a., "return" in Haskell
public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}
et si les lois suivantes s'appliquent à tous les types T et à leurs valeurs possibles t
identité de gauche:
CMonadic<T>.create(t).flatMap(f) == f(t)
bonne identité
instance.flatMap(CMonadic<T>.create) == instance
associativité:
instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))
Exemples:
Une monade de liste peut avoir:
List<int>.create(1) --> [1]
Et flatMap sur la liste [1,2,3] pourrait fonctionner comme suit:
intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]
Iterables et Observables peuvent également être monadiques, ainsi que des promesses et des tâches.
Commentaire:
Les monades ne sont pas si compliquées. La fonction flatMap
ressemble beaucoup à la plus courante map
. Il reçoit un argument de fonction (également appelé délégué), qu'il peut appeler (immédiatement ou plus tard, zéro fois ou plus) avec une valeur provenant de la classe générique. Il s'attend à ce que la fonction passée intègre également sa valeur de retour dans le même type de classe générique. Pour aider avec cela, il fournit create
, un constructeur qui peut créer une instance de cette classe générique à partir d'une valeur. Le résultat renvoyé par flatMap est également une classe générique du même type, contenant souvent les mêmes valeurs que celles contenues dans les résultats renvoyés par une ou plusieurs applications de flatMap aux valeurs précédemment contenues. Cela vous permet de chaîner flatMap autant que vous le souhaitez:
intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
.flatMap(x => x % 3 == 0
? List<string>.create("x = " + x.toString())
: List<string>.empty())
Il se trouve que ce type de classe générique est utile comme modèle de base pour un grand nombre de choses. Ceci (avec les jargonismes de la théorie des catégories) est la raison pour laquelle les monades semblent si difficiles à comprendre ou à expliquer. Ils sont très abstraits et ne deviennent manifestement utiles qu’une fois qu’ils sont spécialisés.
Par exemple, vous pouvez modéliser des exceptions à l'aide de conteneurs monadiques. Chaque conteneur contiendra soit le résultat de l'opération, soit l'erreur survenue. La fonction suivante (délégué) dans la chaîne de rappels flatMap ne sera appelée que si la fonction précédente contient une valeur dans le conteneur. Sinon, si une erreur a été compressée, l'erreur continuera à se propager dans les conteneurs chaînés jusqu'à ce qu'un conteneur contenant une fonction de gestionnaire d'erreurs soit attaché via une méthode appelée .orElse()
(une telle méthode serait une extension autorisée).
Notes: Les langages fonctionnels vous permettent d’écrire des fonctions pouvant fonctionner sur n’importe quel type de classe générique monadique. Pour que cela fonctionne, il faudrait écrire une interface générique pour les monades. Je ne sais pas s'il est possible d'écrire une telle interface en C #, mais pour autant que je sache, ce n'est pas le cas:
interface IMonad<T> {
static IMonad<T> create(T t); // not allowed
public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
// because the function must return the same kind of monad, not just any monad
}
Si une monade a une interprétation "naturelle" dans OO dépend de la monade. Dans un langage tel que Java, vous pouvez traduire monad peut-être en langage de vérification des pointeurs nuls, de sorte que les calculs qui échouent (c’est-à-dire, rien en Haskell) émettent des pointeurs nuls en tant que résultats. Vous pouvez traduire la monade d'état dans le langage généré en créant une variable mutable et des méthodes pour changer son état.
Une monade est un monoïde dans la catégorie des endofoncteurs.
L'information que cette phrase rassemble est très profonde. Et vous travaillez dans une monade avec n'importe quelle langue impérative. Une monade est une langue "séquencée" spécifique à un domaine. Il vérifie certaines propriétés intéressantes qui, ensemble, font d’une monade un modèle mathématique de "programmation impérative". Haskell facilite la définition de langages impératifs petits (ou grands), qui peuvent être combinés de différentes manières.
En tant que programmeur OO, vous utilisez la hiérarchie de classes de votre langue pour organiser les types de fonctions ou de procédures pouvant être appelées dans un contexte, ce que vous appelez un objet. Une monade est également une abstraction de cette idée, dans la mesure où différentes monades peuvent être combinées de manière arbitraire, "important" efficacement toutes les méthodes de la sous-monade dans le champ d'application.
Sur le plan architectural, on utilise ensuite des signatures de type pour exprimer explicitement les contextes pouvant être utilisés pour calculer une valeur.
On peut utiliser des transformateurs monades à cet effet, et il existe une collection de haute qualité de toutes les monades "standard":
avec les transformateurs monades et les classes de types correspondants. Les classes de types permettent une approche complémentaire pour combiner des monades en unifiant leurs interfaces, de sorte que des monades concrètes puissent implémenter une interface standard pour le type "monade". Par exemple, le module Control.Monad.State contient une classe MonadState s m, et (State s) est une instance du formulaire.
instance MonadState s (State s) where
put = ...
get = ...
La longue histoire est qu’une monade est un foncteur qui attache un "contexte" à une valeur, qui a le moyen d’injecter une valeur dans la monade et qui a le moyen d’évaluer les valeurs par rapport au contexte qui lui est attaché. de manière restreinte.
Alors:
return :: a -> m a
est une fonction qui injecte une valeur de type a dans une monade "action" de type m a.
(>>=) :: m a -> (a -> m b) -> m b
est une fonction qui effectue une action monade, évalue son résultat et applique une fonction au résultat. La chose intéressante à propos de (>> =) est que le résultat est dans la même monade. En d'autres termes, dans m >> = f, (>> =) extrait le résultat de m et le lie à f, de sorte que le résultat se trouve dans la monade. (Alternativement, nous pouvons dire que (>> =) tire f en m et l'applique au résultat.) En conséquence, si nous avons f :: a -> mb et g :: b -> mc, nous pouvons actions "séquence":
m >>= f >>= g
Ou, en utilisant "do notation"
do x <- m
y <- f x
g y
Le type pour (>>) peut être éclairant. Il est
(>>) :: m a -> m b -> m b
Il correspond à l'opérateur (;) dans les langages procéduraux tels que C. Il permet une notation telle que:
m = do x <- someQuery
someAction x
theNextAction
andSoOn
Dans la logique mathématique et philosophique, nous avons des cadres et des modèles qui sont "naturellement" modélisés avec le monadisme. Une interprétation est une fonction qui examine le domaine du modèle et calcule la valeur de vérité (ou les généralisations) d'une proposition (ou d'une formule, sous les généralisations). Dans une logique modale de la nécessité, on pourrait dire qu'une proposition est nécessaire si elle est vraie dans "chaque monde possible" - si elle est vraie dans tous les domaines admissibles. Cela signifie qu'un modèle dans un langage pour une proposition peut être réifié comme un modèle dont le domaine consiste en une collection de modèles distincts (un correspondant à chaque monde possible). Chaque monade a une méthode appelée "join" qui aplatit les calques, ce qui implique que toute action de monade dont le résultat est une action de monade peut être intégrée à la monade.
join :: m (m a) -> m a
Plus important encore, cela signifie que la monade est fermée sous l'opération "empilement de couches". C’est ainsi que fonctionnent les transformateurs monades: ils combinent des monades en fournissant des méthodes de type "jointure" pour des types tels que
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
afin que nous puissions transformer une action dans (MaybeT m) en une action dans m, effaçant efficacement des couches. Dans ce cas, runMaybeT :: MaybeT m a -> m (Peut-être a) est notre méthode de type jointure. (MaybeT m) est une monade, et MaybeT :: m (Peut-être a) -> Peut-être que m est effectivement un constructeur pour un nouveau type d'action de monade en m.
Une monade libre pour un foncteur est la monade générée par l'empilement f, ce qui implique que chaque séquence de constructeurs pour f est un élément de la monade libre (ou, plus exactement, quelque chose de la même forme que l'arbre de séquences de constructeurs pour F). Les monades libres sont une technique utile pour construire des monades souples avec un minimum de plaques chauffantes. Dans un programme Haskell, je pourrais utiliser des monades libres pour définir des monades simples pour la "programmation système de haut niveau" afin de préserver la sécurité des types (je n'utilise que des types et leurs déclarations. Les implémentations sont simples avec l'utilisation de combinateurs):
data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a
type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom :: Random r r
runRandomIO :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO' :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a] (some kind of list-based backend (for pseudo-randoms))
Le monadisme est l'architecture sous-jacente de ce que vous pourriez appeler le modèle "interpréteur" ou "commande", résumé sous sa forme la plus claire, étant donné que tout calcul monadique doit être "exécuté", au moins de manière triviale. (Le système d'exécution exécute la monade IO pour nous et constitue le point d'entrée de tout programme Haskell. IO "pilote" le reste des calculs en exécutant des actions IO dans l'ordre). .
Le type de jointure est également celui qui indique qu'une monade est un monoïde dans la catégorie des endofoncteurs. La jointure est généralement plus importante à des fins théoriques, en vertu de son type. Mais comprendre le type signifie comprendre les monades. Les types de jointures de type joint et transformateur monade sont en réalité des compositions d’endofoncteurs, au sens de composition de fonction. Pour le mettre dans un pseudo-langage ressemblant à Haskell,
Foo :: m (m a) <-> (m. M) a
ne monade est un tableau de fonctions
(Pst: un tableau de fonctions est juste un calcul).
En fait, au lieu d'un vrai tableau (une fonction dans un tableau de cellules), vous avez ces fonctions chaînées par une autre fonction >> =. Le >> = permet d'adapter les résultats de la fonction i pour alimenter la fonction i + 1, effectuer des calculs entre eux ou, même, ne pas appeler la fonction i + 1.
Les types utilisés ici sont des "types avec contexte". C'est une valeur avec un "tag". Les fonctions en cours de chaînage doivent prendre une "valeur nue" et renvoyer un résultat marqué. Un des devoirs de >> = est d'extraire une valeur nue de son contexte. Il y a aussi la fonction "return", qui prend une valeur nue et la met avec une balise.
Un exemple avec Maybe . Utilisons-le pour stocker un entier simple sur lequel effectuer des calculs.
-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return (a*b)
-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom
-- tagged value
val1 = Just 160
-- array of functions feeded with val1
array1 = val1 >>= divideBy 2 >>= multiply 3 >>= divideBy 4 >>= multiply 3
-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
v <- divideBy 2 n
v <- multiply 3 v
v <- divideBy 4 v
v <- multiply 3 v
return v
-- array of functions,
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0 >>= multiply 3 >>= divideBy 4 >>= multiply 3
main = do
print array1
print (array2 160)
print array3
Juste pour montrer que les monades sont un tableau de fonctions avec des opérations d'assistance, considérons l'équivalent de l'exemple ci-dessus, en utilisant simplement un tableau de fonctions réel
type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions
myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]
-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs
Et ce serait utilisé comme ceci:
print (runMyMonad (Just 160) myArray1)
Une explication simple des Monades avec une étude de cas de Marvel est ici .
Les monades sont des abstractions utilisées pour séquencer des fonctions dépendantes qui sont efficaces. Par efficace, cela signifie qu'ils renvoient un type sous la forme F [A], par exemple Option [A] où Option est F, appelé constructeur de type. Voyons cela en 2 étapes simples
A => C = A => B andThen B => C
Cependant, si la fonction renvoie un type d’effet tel que Option [A], c’est-à-dire que A => F [B], la composition ne fonctionne pas car pour aller à B, nous avons besoin de A => B mais nous avons A => F [B].
Nous avons besoin d'un opérateur spécial, "bind" qui sait comment fusionner ces fonctions qui retournent F [A].
A => F[C] = A => F[B] bind B => F[C]
La fonction "bind" est définie pour la fonction spécifique F.
Il y a aussi "return" , de type A => F [A] pour tout A, défini pour ce spécifique F aussi. Pour être une monade, F doit avoir ces deux fonctions définies.
Nous pouvons donc construire une fonction efficace A => F [B] à partir de toute fonction pure A => B ,
A => F[B] = A => B andThen return
mais un donné F peut également définir ses propres fonctions spéciales "intégrées" opaques de types tels que l'utilisateur ne peut pas se définir un ( langage pur ), comme
Les monades en utilisation typique sont l'équivalent fonctionnel des mécanismes de traitement des exceptions de la programmation procédurale.
Dans les langages procéduraux modernes, vous placez un gestionnaire d'exceptions autour d'une séquence d'instructions, chacune d'entre elles pouvant générer une exception. Si l'une des instructions génère une exception, l'exécution normale de la séquence d'instructions est interrompue et transférée à un gestionnaire d'exceptions.
Les langages de programmation fonctionnels, cependant, évitent philosophiquement d'éviter les fonctionnalités de traitement des exceptions en raison de leur nature "goto". La perspective de la programmation fonctionnelle est que les fonctions ne doivent pas avoir d’effets secondaires comme les exceptions qui perturbent le déroulement du programme.
En réalité, les effets secondaires ne peuvent pas être exclus dans le monde réel, principalement à cause des E/S. Les monades en programmation fonctionnelle sont utilisées pour gérer cela en prenant un ensemble d'appels de fonction chaînés (chacun d'entre eux pouvant produire un résultat inattendu) et en transformant tout résultat inattendu en données encapsulées pouvant toujours circuler en toute sécurité dans les appels de fonction restants.
Le flux de contrôle est préservé, mais l'événement inattendu est encapsulé et traité en toute sécurité.
En termes OO, une monade est un conteneur fluide.
La configuration minimale requise est une définition de class <A> Something
prenant en charge un constructeur Something(A a)
et au moins une méthode Something<B> flatMap(Function<A, Something<B>>)
.
On peut dire que cela compte également si votre classe monad a des méthodes avec la signature Something<B> work()
qui préserve les règles de la classe - le compilateur cuit dans flatMap au moment de la compilation.
Pourquoi une monade est-elle utile? Parce que c'est un conteneur qui permet des opérations en chaîne qui préservent la sémantique. Par exemple, Optional<?>
conserve la sémantique de isPresent pour Optional<String>
, Optional<Integer>
, Optional<MyClass>
, etc.
Par exemple,
Something<Integer> i = new Something("a")
.flatMap(doOneThing)
.flatMap(doAnother)
.flatMap(toInt)
Notez que nous commençons par une chaîne de caractères et finissons par un entier. Plutôt cool.
Dans OO, cela peut prendre un peu de main, mais toute méthode sur Something renvoyant une autre sous-classe de Something répond au critère d'une fonction de conteneur renvoyant un conteneur du type d'origine.
C’est ainsi que vous conservez la sémantique - c’est-à-dire que la signification et les opérations du conteneur ne changent pas, elles enveloppent et améliorent simplement l’objet à l’intérieur du conteneur.
Voir mon réponse à "Qu'est-ce qu'une monade?"
Il commence par un exemple motivant, passe en revue l'exemple, dérive un exemple de monade et définit formellement "monade".
Il ne suppose aucune connaissance de la programmation fonctionnelle et utilise un pseudocode avec la syntaxe function(argument) := expression
avec les expressions les plus simples possibles.
Ce programme C++ est une implémentation de la monade pseudocode. (Pour référence: M
est le constructeur de type, feed
est l'opération "bind" et wrap
est l'opération "retour".)
#include <iostream>
#include <string>
template <class A> class M
{
public:
A val;
std::string messages;
};
template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
M<B> m = f(x.val);
m.messages = x.messages + m.messages;
return m;
}
template <class A>
M<A> wrap(A x)
{
M<A> m;
m.val = x;
m.messages = "";
return m;
}
class T {};
class U {};
class V {};
M<U> g(V x)
{
M<U> m;
m.messages = "called g.\n";
return m;
}
M<T> f(U x)
{
M<T> m;
m.messages = "called f.\n";
return m;
}
int main()
{
V x;
M<T> m = feed(f, feed(g, wrap(x)));
std::cout << m.messages;
}
Si vous avez déjà utilisé Powershell, les modèles décrits par Eric devraient vous paraître familiers. Les applets de commande Powershell sont des monades; la composition fonctionnelle est représentée par n pipeline .
Entretien de Jeffrey Snover avec Erik Meijer entre dans les détails.
Je partage ma compréhension des monades, qui peuvent ne pas être théoriquement parfaites. Les monades sont à propos de propagation du contexte. Monad is, vous définissez un contexte pour certaines données, puis vous définissez comment le contexte sera traité avec les données tout au long de son pipeline de traitement. Et la définition du contexte de propagation consiste principalement à définir comment fusionner plusieurs contextes. Cela garantit également que les contextes ne peuvent pas être retirés accidentellement des données. Ce concept simple peut être utilisé pour assurer l'exactitude du temps de compilation d'un programme.
D'un point de vue pratique (résumant ce qui a été dit dans de nombreuses réponses précédentes et articles connexes), il me semble qu'un des "buts" (ou utilité) fondamentaux de la monade est de tirer parti des dépendances implicites dans les invocations de méthodes récursives. alias composition de fonction (ie lorsque f1 appelle f2 appelle f3, f3 doit être évalué avant f2 avant f1) pour représenter la composition séquentielle de manière naturelle, en particulier dans le contexte d’un modèle d’évaluation paresseux (c’est-à-dire, la composition séquentielle en tant que séquence simple , par exemple "f3 (); f2 (); f1 ();" en C - le truc est particulièrement évident si vous pensez à un cas où f3, f2 et f1 ne renvoient en réalité rien [leur enchaînement en tant que f1 (f2 (f3)) est artificielle, uniquement destinée à créer une séquence]).
Ceci est particulièrement pertinent lorsque des effets secondaires sont impliqués, c'est-à-dire lorsqu'un état est modifié (si f1, f2, f3 ne présentaient aucun effet secondaire, l'ordre dans lequel ils sont évalués importait peu; ce qui est une grande propriété de pure langages fonctionnels, pour pouvoir paralléliser ces calculs par exemple). Plus les fonctions sont pures, mieux c'est.
Je pense que de ce point de vue étroit, les monades pourraient être considérées comme un sucre syntaxique pour les langages qui favorisent l’évaluation paresseuse (qui évalue les choses seulement lorsque cela est absolument nécessaire, en suivant un ordre qui ne repose pas sur la présentation du code), et qui n’ont autres moyens de représenter une composition séquentielle. Le résultat final est que les sections de code qui sont "impures" (c'est-à-dire qui ont des effets secondaires) peuvent être présentées naturellement, de manière impérative, tout en étant clairement séparées des fonctions pures (sans effets secondaires), qui peuvent être supprimées. évalué paresseusement.
Ceci n’est cependant qu’un aspect, comme on l’avait dit ici .