En C #, qu'est-ce qu'une monade?

190

On parle beaucoup de monades ces jours-ci. J'ai lu quelques articles / billets de blog, mais je ne peux pas aller assez loin avec leurs exemples pour saisir pleinement le concept. La raison en est que les monades sont un concept de langage fonctionnel, et donc les exemples sont dans des langages avec lesquels je n'ai pas travaillé (puisque je n'ai pas utilisé un langage fonctionnel en profondeur). Je ne peux pas saisir la syntaxe assez profondément pour suivre les articles complètement ... mais je peux dire qu'il y a quelque chose qui vaut la peine d'être compris.

Cependant, je connais assez bien C #, y compris les expressions lambda et d'autres fonctionnalités fonctionnelles. Je sais que C # n'a qu'un sous-ensemble de fonctionnalités fonctionnelles, et donc peut-être que les monades ne peuvent pas être exprimées en C #.

Cependant, il est sûrement possible de transmettre le concept? Du moins je l'espère. Vous pouvez peut-être présenter un exemple C # comme base, puis décrire ce qu'un développeur C # souhaiterait pouvoir faire à partir de là, mais ne peut pas parce que le langage manque de fonctionnalités de programmation fonctionnelles. Ce serait fantastique, car cela transmettrait l'intention et les avantages des monades. Alors, voici ma question: quelle est la meilleure explication que vous pouvez donner des monades à un développeur C # 3?

Merci!

(EDIT: Au fait, je sais qu'il y a déjà au moins 3 questions "qu'est-ce qu'une monade" sur SO. Cependant, je suis confronté au même problème avec eux ... donc cette question est nécessaire imo, à cause du développeur C # focus. Merci.)

Fleurs de Charlie
la source
Veuillez noter qu'il s'agit en fait d'un développeur C # 3.0. Ne le confondez pas avec .NET 3.5. A part ça, belle question.
Razzie
4
Il convient de souligner que les expressions de requête LINQ sont un exemple de comportement monadique en C # 3.
Erik Forbes
1
Je pense toujours que c'est une question en double. Une des réponses dans stackoverflow.com/questions/2366/can-anyone-explain-monads lien vers channel9vip.orcsweb.com/shows/Going+Deep/... , où l'un des commentaires a un très bel exemple C #. :)
jalf
4
Pourtant, ce n'est qu'un lien d'une réponse à l'une des questions SO. Je vois la valeur dans une question centrée sur les développeurs C #. C'est quelque chose que je demanderais à un programmeur fonctionnel qui faisait du C # si j'en connaissais un, il semble donc raisonnable de le demander sur SO. Mais je respecte également votre droit à votre opinion.
Charlie Flowers
1
N'est-ce pas tout ce dont vous avez besoin? ;) Mon point est juste que l'une des autres questions (et maintenant celle-ci aussi, donc yay) avait une réponse spécifique à C # (qui semble vraiment bien écrite, en fait. Probablement la meilleure explication que j'ai vue)
jalf

Réponses:

147

La plupart de ce que vous faites dans la programmation toute la journée consiste à combiner certaines fonctions pour en créer de plus grandes. Habituellement, vous avez non seulement des fonctions dans votre boîte à outils, mais aussi d'autres choses comme des opérateurs, des affectations de variables et autres, mais généralement votre programme combine de nombreux "calculs" à des calculs plus importants qui seront combinés plus loin.

Une monade est un moyen de faire cette "combinaison de calculs".

Habituellement, votre "opérateur" le plus élémentaire pour combiner deux calculs est ;:

a; b

Lorsque vous dites cela, vous voulez dire «d'abord faire a, puis faire b». Le résultat a; best à nouveau un calcul qui peut être combiné avec plus de choses. C'est une simple monade, c'est une façon de combiner de petits calculs à de plus grands. Le ;dit "faites la chose à gauche, puis faites la chose à droite".

Une autre chose qui peut être considérée comme une monade dans les langages orientés objet est le .. Vous trouvez souvent des choses comme ceci:

a.b().c().d()

Le .signifie fondamentalement "évaluer le calcul sur la gauche, puis appeler la méthode sur la droite sur le résultat de cela". C'est une autre façon de combiner fonctions / calculs ensemble, un peu plus compliqué que ;. Et le concept de chaîner des choses avec .est une monade, car c'est une façon de combiner deux calculs ensemble en un nouveau calcul.

Une autre monade assez courante, qui n'a pas de syntaxe particulière, est ce modèle:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

Une valeur de retour de -1 indique un échec, mais il n'existe aucun moyen réel d'abstraire cette vérification d'erreurs, même si vous avez beaucoup d'appels API que vous devez combiner de cette manière. Il s'agit simplement d'une autre monade qui combine les appels de fonction par la règle "si la fonction de gauche renvoie -1, renvoie -1 nous-mêmes, sinon appelle la fonction de droite". Si nous avions un opérateur >>=qui faisait cette chose, nous pourrions simplement écrire:

socket.bind(...) >>= socket.connect(...) >>= socket.send(...)

Cela rendrait les choses plus lisibles et aiderait à faire abstraction de notre façon spéciale de combiner des fonctions, de sorte que nous n'ayons pas besoin de nous répéter encore et encore.

Et il existe de nombreuses autres façons de combiner des fonctions / calculs qui sont utiles en tant que modèle général et peuvent être abstraites dans une monade, permettant à l'utilisateur de la monade d'écrire un code beaucoup plus concis et clair, puisque toute la comptabilité et la gestion de les fonctions utilisées se font dans la monade.

Par exemple, ce qui précède >>=pourrait être étendu pour "faire la vérification des erreurs, puis appeler le côté droit du socket que nous avons obtenu en entrée", afin que nous n'ayons pas besoin de spécifier explicitement socketbeaucoup de fois:

new socket() >>= bind(...) >>= connect(...) >>= send(...);

La définition formelle est un peu plus compliquée car vous devez vous soucier de savoir comment obtenir le résultat d'une fonction en tant qu'entrée de la suivante, si cette fonction a besoin de cette entrée et que vous voulez vous assurer que les fonctions que vous combinez correspondent la façon dont vous essayez de les combiner dans votre monade. Mais le concept de base est simplement que vous formalisez différentes manières de combiner des fonctions.

qc
la source
28
Très bonne réponse! Je vais lancer une citation d'Oliver Steele, essayant de relier les monades à la surcharge d'opérateurs à la C ++ ou C #: les monades vous permettent de surcharger le ';' opérateur.
Jörg W Mittag
6
@ JörgWMittag J'ai déjà lu cette citation, mais cela avait l'air d'un non-sens excessivement grisant. Maintenant que je comprends les monades et que je lis cette explication du comment ';' en est un, je comprends. Mais je pense que c'est vraiment une déclaration irrationnelle pour la plupart des développeurs impératifs. ';' n'est plus considéré comme un opérateur que // l'est pour la plupart.
Jimmy Hoffa
2
Êtes-vous sûr de savoir ce qu'est une monade? Les monades ne sont pas une «fonction» ou un calcul, il existe des règles pour les monades.
Luis
Dans votre ;exemple: Quels objets / types de données sont ;mappés? (Pensez Listcartes Tà List<T>) Comment ;carte morphisme / fonctions entre les objets / types de données? Qu'est - ce pure, join, bindpour ;?
Micha Wiedenmann
44

Cela fait un an que j'ai posté cette question. Après l'avoir posté, je me suis plongé dans Haskell pendant quelques mois. Je l'ai énormément apprécié, mais je l'ai mis de côté juste au moment où j'étais prêt à me plonger dans les Monades. Je suis retourné au travail et me suis concentré sur les technologies dont mon projet avait besoin.

Et hier soir, je suis venu et j'ai relu ces réponses. Plus important encore , j'ai relu l'exemple spécifique C # dans les commentaires de texte de la vidéo de Brian Beckman que quelqu'un mentionne ci-dessus . C'était tellement clair et éclairant que j'ai décidé de l'afficher directement ici.

A cause de ce commentaire, non seulement j'ai l'impression de comprendre exactement ce que sont les Monades… Je me rends compte que j'ai en fait écrit des choses en C # qui sont des Monades… ou du moins très proches, et je m'efforce de résoudre les mêmes problèmes.

Alors, voici le commentaire - tout cela est une citation directe du commentaire ici par sylvan :

C'est plutôt cool. C'est un peu abstrait cependant. J'imagine que les gens qui ne savent pas quelles monades sont déjà confus en raison du manque d'exemples réels.

Alors laissez-moi essayer de me conformer, et juste pour être vraiment clair, je vais faire un exemple en C #, même si cela aura l'air moche. Je vais ajouter l'équivalent Haskell à la fin et vous montrer le sucre syntaxique Haskell cool qui est là où, IMO, les monades commencent vraiment à devenir utiles.

D'accord, donc l'une des monades les plus faciles s'appelle la "Monade peut-être" dans Haskell. En C #, le type Maybe est appelé Nullable<T>. C'est fondamentalement une classe minuscule qui encapsule simplement le concept d'une valeur qui est soit valide et a une valeur, soit est "nulle" et n'a aucune valeur.

Une chose utile à coller à l'intérieur d'une monade pour combiner des valeurs de ce type est la notion d'échec. C'est-à-dire que nous voulons pouvoir regarder plusieurs valeurs Nullable et retourner nulldès que l'une d'entre elles est NULL. Cela peut être utile si, par exemple, vous recherchez beaucoup de clés dans un dictionnaire ou quelque chose du genre, et que vous souhaitez à la fin traiter tous les résultats et les combiner d'une manière ou d'une autre, mais si l'une des clés n'est pas dans le dictionnaire, vous voulez revenir nullpour tout. Il serait fastidieux d'avoir à vérifier manuellement chaque recherche nullet retour, afin que nous puissions cacher cette vérification à l'intérieur de l'opérateur de liaison (qui est en quelque sorte le point des monades, nous masquons la comptabilité dans l'opérateur de liaison, ce qui rend le code plus facile à utiliser car on peut oublier les détails).

Voici le programme qui motive le tout (je définirai le Bindplus tard, c'est juste pour vous montrer pourquoi c'est sympa).

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

Maintenant, ignorez un instant qu'il existe déjà un support pour faire cela pour Nullableen C # (vous pouvez ajouter des entiers Nullable ensemble et vous obtenez NULL si l'un ou l'autre est nul). Supposons qu'il n'y ait pas de telle fonctionnalité et que ce soit juste une classe définie par l'utilisateur sans magie particulière. Le fait est que nous pouvons utiliser la Bindfonction pour lier une variable au contenu de notre Nullablevaleur, puis prétendre qu'il ne se passe rien d'étrange, et les utiliser comme des entiers normaux et simplement les additionner. Nous terminerons le résultat dans une annulable à la fin, et que annulable sera soit nulle (si l' un des f, gou hrenvoie NULL) ou il sera le résultat de la somme f, gethensemble. (Ceci est analogue à la façon dont nous pouvons lier une ligne dans une base de données à une variable dans LINQ, et faire des choses avec elle, en sachant que l' Bindopérateur s'assurera que la variable ne recevra que des valeurs de ligne valides).

Vous pouvez jouer avec cela et changer n'importe lequel de f, get hpour retourner null et vous verrez que le tout retournera null.

Il est donc clair que l'opérateur de liaison doit faire cette vérification pour nous, et renflouer en retournant null s'il rencontre une valeur nulle, et sinon transmettre la valeur à l'intérieur de la Nullablestructure dans le lambda.

Voici l' Bindopérateur:

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

Les types ici sont exactement comme dans la vidéo. Il prend une M a ( Nullable<A>dans la syntaxe C # pour ce cas), et une fonction de aà M b( Func<A, Nullable<B>>dans la syntaxe C #), et il renvoie un M b ( Nullable<B>).

Le code vérifie simplement si le nullable contient une valeur et si c'est le cas, l'extrait et le transmet à la fonction, sinon il renvoie simplement null. Cela signifie que l' Bindopérateur gérera toute la logique de vérification de null pour nous. Si et seulement si la valeur que nous appelons Bindest non nulle alors cette valeur sera "transmise" à la fonction lambda, sinon nous sortons rapidement et l'expression entière est nulle. Cela permet au code que nous écrire en utilisant la monade être entièrement libre de ce comportement de vérification nul, nous utilisons simplement Bindet d' obtenir une variable liée à la valeur à l' intérieur de la valeur monadique ( fval, gvalet hvaldans le code exemple) et nous pouvons les utiliser en toute sécurité dans la connaissance qui Bindse chargera de les vérifier pour null avant de les transmettre.

Il existe d'autres exemples de choses que vous pouvez faire avec une monade. Par exemple, vous pouvez Binddemander à l' opérateur de prendre en charge un flux d'entrée de caractères et de l'utiliser pour écrire des combinateurs d'analyseurs. Chaque combinateur d'analyseur peut alors être complètement inconscient de choses comme le back-tracking, les échecs de l'analyseur, etc., et simplement combiner des analyseurs plus petits ensemble comme si les choses n'iraient jamais mal, sachant qu'une mise en œuvre intelligente Bindtrie toute la logique derrière bits difficiles. Ensuite, peut-être que quelqu'un ajoute la journalisation à la monade, mais le code utilisant la monade ne change pas, car toute la magie se produit dans la définition de l' Bindopérateur, le reste du code est inchangé.

Enfin, voici l'implémentation du même code dans Haskell ( -- commence une ligne de commentaire).

-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

Comme vous pouvez le voir, la belle donotation à la fin le fait ressembler à du code impératif simple. Et en effet, c'est par conception. Les monades peuvent être utilisées pour encapsuler tous les éléments utiles de la programmation impérative (état mutable, IO, etc.) et utilisées en utilisant cette belle syntaxe de type impératif, mais derrière les rideaux, ce ne sont que des monades et une implémentation intelligente de l'opérateur de liaison! Ce qui est cool, c'est que vous pouvez implémenter vos propres monades en implémentant >>=et return. Et si vous le faites, ces monades pourront également utiliser la donotation, ce qui signifie que vous pouvez essentiellement écrire vos propres petits langages en définissant simplement deux fonctions!

Fleurs de Charlie
la source
3
Personnellement, je préfère la version F # de la monade, mais dans les deux cas, ils sont géniaux.
ChaosPandion
3
Merci d'être revenu ici et d'avoir mis à jour votre message. Ce sont des suivis comme ceux-ci qui aident les programmeurs qui cherchent dans un domaine spécifique à vraiment comprendre comment les autres programmeurs considèrent finalement ledit domaine, au lieu de simplement se débarrasser de "comment faire pour la technologie x en y". Toi un homme!
kappasims
J'ai emprunté le même chemin que vous et je suis arrivé au même endroit pour comprendre les monades, cela dit que c'est la meilleure explication du comportement de liaison d'une monade que j'ai jamais vue pour un développeur impératif. Bien que je pense que vous ne touchez pas tout à fait à tout sur les monades, ce qui est un peu plus expliqué ci-dessus par qc.
Jimmy Hoffa
@Jimmy Hoffa - vous avez sans doute raison. Je pense que pour vraiment les comprendre plus profondément, le meilleur moyen est de commencer à les utiliser beaucoup et d'acquérir de l' expérience . Je n'ai pas encore eu cette opportunité, mais j'espère bientôt.
Charlie Flowers
Il semble que la monade ne soit qu'un niveau d'abstraction plus élevé en termes de programmation, ou que ce soit juste une définition de fonction continue et non différentiable en mathématiques. Quoi qu'il en soit, ce n'est pas un nouveau concept, surtout en mathématiques.
liang
11

Une monade est essentiellement un traitement différé. Si vous essayez d'écrire du code qui a des effets secondaires (par exemple E / S) dans un langage qui ne les autorise pas, et n'autorise que le calcul pur, une esquive consiste à dire: "Ok, je sais que vous ne ferez pas d'effets secondaires pour moi, mais pouvez-vous s'il vous plaît calculer ce qui se passerait si vous le faisiez? "

C'est une sorte de triche.

Maintenant, cette explication vous aidera à comprendre l'intention globale des monades, mais le diable est dans les détails. Comment faire exactement vous calculer les conséquences? Parfois, ce n'est pas joli.

La meilleure façon de donner un aperçu de la façon dont une personne habituée à la programmation impérative est de dire qu'elle vous met dans un DSL dans lequel des opérations qui ressemblent syntaxiquement à ce à quoi vous êtes habitué en dehors de la monade sont utilisées à la place pour construire une fonction qui ferait. ce que vous voulez si vous pouviez (par exemple) écrire dans un fichier de sortie. Presque (mais pas vraiment) comme si vous construisiez du code dans une chaîne à évaluer plus tard.

MarkusQ
la source
1
Comme dans le livre I Robot? Où le scientifique demande-t-il à un ordinateur de calculer le voyage spatial et lui demande de sauter certaines règles? :) :) :) :)
OscarRyz
3
Hmm, A Monad peut être utilisé pour un traitement différé et pour encapsuler des fonctions d'effets secondaires, en fait c'était sa première vraie application dans Haskell, mais c'est en fait un modèle beaucoup plus générique. D'autres utilisations courantes incluent la gestion des erreurs et la gestion des états. Le sucre syntaxique (faire en Haskell, Expressions de calcul en F #, syntaxe Linq en C #) est simplement cela et fondamental pour les Monades en tant que telles.
Mike Hadlow
@MikeHadlow: Les instances monades pour la gestion des erreurs ( Maybeet Either e) et la gestion des états ( State s, ST s) me semblent des instances particulières de "Veuillez calculer ce qui se passerait si vous faisiez [effets secondaires pour moi]". Un autre exemple serait le non-déterminisme ( []).
pyon
c'est exactement ça ; avec un (enfin, deux) ajouts qu'il s'agit d'un E DSL, c'est-à-dire d' un DSL embarqué , car chaque valeur "monadique" est une valeur valide de votre langage "pur" lui-même, représentant un "calcul" potentiellement impur. De plus, une construction monadique "bind" existe dans votre langage pur qui vous permet d'enchaîner des constructeurs purs de telles valeurs où chacun sera invoqué avec le résultat de son calcul précédent, lorsque tout le calcul combiné est "exécuté". Cela signifie que nous avons la possibilité de créer des branches sur les résultats futurs (ou dans tous les cas, sur la chronologie «d'exécution» séparée).
Will Ness
mais pour un programmeur cela signifie que nous pouvons programmer dans l'EDSL tout en le mélangeant avec les calculs purs de notre langage pur. une pile de sandwichs multicouches est un sandwich multicouche. c'est aussi simple que cela .
Will Ness
4

Je suis sûr que d'autres utilisateurs publieront en profondeur, mais j'ai trouvé cette vidéo utile dans une certaine mesure, mais je dirai que je ne suis toujours pas au point de maîtriser le concept de sorte que je pourrais (ou devrais) commencer à résoudre problèmes intuitivement avec les Monades.

TheMissingLINQ
la source
1
Ce que j'ai trouvé encore plus utile, c'est le commentaire contenant un exemple C # sous la vidéo.
jalf
Je ne sais pas plus utile, mais cela a certainement mis les idées en pratique.
TheMissingLINQ
0

Vous pouvez considérer une monade comme un C # interfaceque les classes doivent implémenter . C'est une réponse pragmatique qui ignore toute la catégorie mathématique théorique derrière pourquoi vous voudriez choisir d'avoir ces déclarations dans votre interface et ignore toutes les raisons pour lesquelles vous voudriez avoir des monades dans un langage qui essaie d'éviter les effets secondaires, mais j'ai trouvé que c'était un bon début en tant que personne qui comprend les interfaces (C #).

hao
la source
Peux-tu élaborer? Qu'est-ce qu'une interface qui la relie aux monades?
Joel Coehoorn
2
Je pense que le billet de blog contient plusieurs paragraphes consacrés à cette question.
hao
0

Voir ma réponse à "Qu'est-ce qu'une monade?"

Il commence par un exemple motivant, fonctionne à travers 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 une function(argument) := expressionsyntaxe avec les expressions les plus simples possibles.

Ce programme C # est une implémentation de la monade pseudocode. (Pour référence: Mest le constructeur de type, feedest l'opération "bind" et wrapest l'opération "return".)

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

    public class T {};
    public class U {};
    public class V {};

    public static M<U> g(V x)
    {
        M<U> m = new M<U>();
        m.messages = "called g.\n";
        return m;
    }

    public static M<T> f(U x)
    {
        M<T> m = new M<T>();
        m.messages = "called f.\n";
        return m;
    }

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}
Jordan
la source