web-dev-qa-db-fra.com

Aidez un développeur C # à comprendre: Qu'est-ce qu'une monade?

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 bien saisir le concept. La raison en est que les monades sont un concept de langage fonctionnel, et les exemples sont donc dans des langages avec lesquels je n’ai pas travaillé (étant donné que je n’ai pas utilisé un langage fonctionnel en profondeur). Je ne peux pas comprendre suffisamment la syntaxe pour suivre les articles à fond ... mais je peux dire qu'il y a quelque chose d'intéressant à comprendre ici.

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

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

Merci!

(EDIT: En passant, je sais qu’il ya déjà au moins 3 questions sur "ce qui est une monade" sur SO. Cependant, je suis confronté au même problème avec elles ... donc cette question est nécessaire imo, à cause du développeur C # focus. Merci.)

180
Charlie Flowers

La majeure partie de votre programmation toute la journée consiste à combiner certaines fonctions pour en créer de plus grandes. Généralement, vous avez non seulement des fonctions dans votre boîte à outils, mais également d'autres éléments tels que des opérateurs, des affectations de variables, etc., mais généralement, votre programme combine beaucoup de "calculs" pour former des calculs plus volumineux qui seront combinés davantage.

Une monade est un moyen de faire cela "en combinant des calculs".

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

a; b

Lorsque vous dites cela, vous voulez dire "faites d'abord a, puis faites b". Le résultat a; b est à nouveau fondamentalement un calcul qui peut être combiné avec plus de choses .C'est un simple monade, c'est un moyen de combiner de petits calculs avec des calculs plus gros. Le ; dit "fais la chose à gauche, puis fais la chose à droite".

Une autre chose qui peut être vue 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 "évaluez le calcul à gauche, puis appelez la méthode à droite sur le résultat de celui-ci". C'est une autre façon de combiner des fonctions/calculs ensemble, un peu plus compliqué que ;. Et le concept de chaîner des éléments avec . est une monade, car il permet de combiner deux calculs ensemble pour un nouveau calcul.

Une autre monade assez commune, qui n’a pas de syntaxe spéciale, est ce motif:

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’erreur, même si vous devez combiner de nombreux appels d’API de cette façon. Ceci est fondamentalement juste une autre monade qui combine les appels de fonction par la règle "si la fonction à gauche a renvoyé -1, retournons -1 nous-mêmes, sinon appelez la fonction à droite". Si nous avions un opérateur >>= qui faisait cela, nous pourrions simplement écrire:

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

Cela rendrait les choses plus lisibles et aiderait à résumer notre manière spéciale de combiner des fonctions, de sorte que nous n’ayons pas besoin de nous répéter encore et encore.

Et il y a beaucoup plus de façons de combiner des fonctions/calculs qui sont utiles comme modèle général et peuvent être résumés dans une monade, permettant à l'utilisateur de la monade d'écrire un code beaucoup plus concis et clair, car toute la comptabilité et la gestion de les fonctions utilisées sont effectuées dans la monade.

Par exemple, le >>= ci-dessus pourrait être étendu à "effectuer la vérification des erreurs puis appeler le côté droit du socket que nous avons obtenu en entrée", de sorte qu'il n'est pas nécessaire de spécifier explicitement socket de nombreuses fois:

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

La définition formelle est un peu plus compliquée puisque vous devez vous demander comment obtenir le résultat d’une fonction comme 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 s’intègrent bien. la façon dont vous essayez de les combiner dans votre monade. Mais le concept de base est simplement que vous formalisez différentes façons de combiner des fonctions ensemble.

144
sth

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é au moment où j'étais prêt à fouiller dans Monads. Je suis retourné au travail et me suis concentré sur les technologies requises par mon projet.

Et hier soir, je suis venu et relu ces réponses. Plus important encore , je relis l'exemple spécifique de C # dans les commentaires textuels de la vidéo de Brian Beckman 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 Monads… Je réalise que j'ai écrit certaines choses en C # qui sont Monades… ou du moins très proches et essayant de résoudre les mêmes problèmes.

Donc, voici le commentaire - ceci est une citation directe de le commentaire ici par sylvan :

C'est plutôt cool. C'est un peu abstrait cependant. J'imagine que les personnes qui ne savent pas quelles monades sont déjà confuses à cause 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, qui est l'endroit où, les monades, les monades commencent vraiment à devenir utiles.

D'accord, l'une des monades les plus faciles s'appelle la "monade peut-être" de Haskell. En C #, le type peut-être s'appelle Nullable<T>. Il s’agit en fait d’une toute petite classe qui ne fait qu’encapsuler le concept d’une valeur valide ou ayant une valeur ou "nulle" sans valeur.

Une chose utile à coller dans une monade pour combiner des valeurs de ce type est la notion d'échec. C'est à dire. nous voulons pouvoir examiner plusieurs valeurs nullables et renvoyer null dès que l'une d'elles est nulle. Cela peut être utile si, par exemple, vous recherchez un grand nombre de clés dans un dictionnaire ou quelque chose du même genre et que, à la fin, vous souhaitez traiter tous les résultats et les combiner d'une manière ou d'une autre, mais si l'une des clés ne se trouve pas dans le dictionnaire, vous voulez retourner null pour le tout. Il serait fastidieux de devoir vérifier manuellement chaque recherche null et la renvoyer, afin de pouvoir masquer cette vérification à l'intérieur de l'opérateur de liaison (qui est en quelque sorte le point de vue des monades, nous masquons la comptabilité dans l'opérateur de liaison rend le code plus facile à utiliser car nous pouvons oublier les détails).

Voici le programme qui motive le tout (je vais définir la Bind plus tard, ceci est juste pour vous montrer pourquoi c'est bien à Nice).

 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à une prise en charge pour cela dans Nullable en C # (vous pouvez ajouter des ints nullables ensemble et vous obtenez null si l'une ou l'autre est NULL). Supposons qu'il n'existe aucune fonctionnalité de ce type et qu'il ne s'agit que d'une classe définie par l'utilisateur sans magie particulière. Le fait est que nous pouvons utiliser la fonction Bind pour lier une variable au contenu de notre valeur Nullable, puis prétendre qu'il n'y a rien d'étrange, et les utiliser comme des ints normales et les additionner simplement . Nous encapsulons le résultat dans un nullable à la fin, et celui-ci sera soit nul (si l’un quelconque de f, g ou h renvoie null) ou ce sera le résultat de la somme f, g et h ensemble. (Ceci est analogue à la façon dont nous pouvons lier une ligne d'une base de données à une variable dans LINQ et le manipuler, en sachant que l'opérateur Bind s'assurera que la variable ne sera transmise que comme valide. valeurs de ligne).

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

Il est donc clair que l'opérateur de liaison doit effectuer cette vérification pour nous, et renvoyer null si il rencontre une valeur nulle et sinon transmettre la valeur à l'intérieur de la structure Nullable dans le lambda.

Voici l'opérateur Bind:

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 comme dans la vidéo. Il faut un M a (Nullable<A> en syntaxe C # pour ce cas) et une fonction de a à M b (Func<A, Nullable<B>> en syntaxe C #), et il renvoie un M b (Nullable<B>).

Le code vérifie simplement si le nullable contient une valeur et si elle extrait et la transfère à la fonction, sinon, il retourne simplement null. Cela signifie que l’opérateur Bind gérera pour nous toute la logique de vérification de zéro. Si et seulement si la valeur que nous appelons Bind on est non-nulle, cette valeur sera "transmise" à la fonction lambda, sinon nous renonçons plus tôt et l'expression entière est nulle. Cela permet au code que nous écrivons en utilisant la monade d’être totalement libre de ce comportement de vérification de zéro. Nous utilisons simplement Bind et obtenons une variable liée à la valeur dans la valeur monadique (fval, gval et hval dans l'exemple de code) et nous pouvons les utiliser en toute sécurité, sachant que Bind se 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 demander à l'opérateur Bind de gérer un flux de caractères d'entrée et de l'utiliser pour écrire des combinateurs d'analyse. Chaque combinateur d’analyseur peut alors être complètement inconscient de choses telles que le suivi, les défaillances d’analyseur, etc., et combiner simplement des analyseurs syntaxiques plus petits comme si rien ne se passait jamais, sachant qu’une mise en œuvre intelligente de Bind trie toute la logique derrière les moments difficiles. Puis plus tard, peut-être que quelqu'un ajoutera la journalisation à la monade, mais le code utilisant la monade ne changera pas, car toute la magie se produit dans la définition de l'opérateur Bind, le reste du code reste 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 notation Nice do à la fin fait ressembler ce code à un code impératif simple. Et en effet c'est par conception. Les monades peuvent être utilisées pour encapsuler tout ce qui est utile dans la programmation impérative (état mutable, IO etc.) et utilisées avec cette syntaxe de type impératif Nice, mais derrière les rideaux, ce ne sont que des monades et une mise en œuvre intelligente. de l'opérateur bind! La bonne chose 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 notation do, ce qui signifie que vous pouvez écrire vos propres petites langues en définissant simplement deux fonctions!

42
Charlie Flowers

Une monade est essentiellement un traitement différé. Si vous essayez d’écrire du code qui a des effets secondaires (par exemple des E/S) dans un langage qui ne les autorise pas et ne permet que le calcul pur, vous pouvez 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 générale des monades, mais le diable est dans les détails. Comment exactement faire vous calculez 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 que cela vous place dans un DSL dans lequel les opérations qui ressemblent syntaxiquement à ce que vous êtes habitué en dehors de la monade sont utilisées à la place pour créer 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 ultérieurement.

11
MarkusQ

Je suis sûr que d’autres utilisateurs publieront en détail, 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 telle devrait) commencer à résoudre les problèmes de manière intuitive avec Monads.

4
TheMissingLINQ

Voir ma 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 "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);
    }
}
0
Jordan

Vous pouvez penser à une monade en tant que un C # interface que les classes doivent implémenter . C’est une réponse pragmatique qui ignore toutes les mathématiques théoriques de la catégorie derrière la raison pour laquelle vous souhaitez 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 tente 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 #).

0
hao