In C #, Cos'è una monade?


190

Di questi tempi si parla molto delle monadi. Ho letto alcuni articoli / post di blog, ma non posso andare abbastanza lontano con i loro esempi per comprendere appieno il concetto. Il motivo è che le monadi sono un concetto di linguaggio funzionale, e quindi gli esempi sono in lingue con cui non ho lavorato (dal momento che non ho usato un linguaggio funzionale in profondità). Non riesco ad afferrare la sintassi abbastanza profondamente per seguire gli articoli fino in fondo ... ma posso dire che c'è qualcosa che vale la pena capire lì.

Tuttavia, conosco abbastanza bene C #, comprese le espressioni lambda e altre caratteristiche funzionali. So che C # ha solo un sottoinsieme di caratteristiche funzionali, quindi forse le monadi non possono essere espresse in C #.

Tuttavia, sicuramente è possibile trasmettere il concetto? Almeno lo spero. Forse si può presentare un esempio di C # come una fondazione, e poi descrivere ciò che un C # sviluppatore desiderio che potesse fare da lì, ma non può perché la lingua manca di funzioni di programmazione funzionale. Sarebbe fantastico, perché trasmetterebbe l'intento e i benefici delle monadi. Quindi, ecco la mia domanda: qual è la migliore spiegazione che puoi dare alle monadi a uno sviluppatore C # 3?

Grazie!

(EDIT: A proposito, so che ci sono almeno 3 domande "che cos'è una monade" già su SO. Tuttavia, devo affrontare lo stesso problema con loro ... quindi questa domanda è necessaria anche a causa dello sviluppo di C # messa a fuoco. Grazie.)


Si noti che in realtà è uno sviluppatore C # 3.0. Non confonderlo con .NET 3.5. A parte questo, bella domanda.
Razzie,

4
Vale la pena sottolineare che le espressioni di query LINQ sono un esempio di comportamento monadico in C # 3.
Erik Forbes,

1
Penso ancora che sia una domanda duplicata. Una delle risposte a stackoverflow.com/questions/2366/can-anyone-explain-monads collegamento a channel9vip.orcsweb.com/shows/Going+Deep/... , in cui uno dei commenti ha un bel esempio C #. :)
jalf

4
Tuttavia, questo è solo un collegamento da una risposta a una delle domande SO. Vedo valore in una domanda focalizzata sugli sviluppatori C #. È qualcosa che vorrei chiedere a un programmatore funzionale che era solito fare C # se ne conoscessi uno, quindi sembra ragionevole chiederlo su SO. Ma rispetto anche il tuo diritto alla tua opinione.
Charlie Flowers,

1
Ma una risposta non è tutto ciò di cui hai bisogno? ;) Il mio punto è solo che una delle altre domande (e ora anche questa, quindi yay) aveva una risposta specifica per C # (che sembra davvero ben scritta, in realtà. Probabilmente la migliore spiegazione che ho visto)
jalf

Risposte:


147

La maggior parte di ciò che fai nella programmazione per tutto il giorno è la combinazione di alcune funzioni per creare funzioni più grandi da esse. Di solito non hai solo funzioni nella tua cassetta degli attrezzi, ma anche altre cose come operatori, assegnazioni di variabili e simili, ma in generale il tuo programma combina molti "calcoli" con calcoli più grandi che verranno ulteriormente combinati insieme.

Una monade è un modo per fare questa "combinazione di calcoli".

Di solito il tuo "operatore" di base per combinare due calcoli è ;:

a; b

Quando dici questo vuoi dire "prima fai a, poi fai b". Il risultato a; bè fondamentalmente di nuovo un calcolo che può essere combinato con più cose. Questa è una monade semplice, è un modo di combinare piccoli calcoli con quelli più grandi. La ;dice "fare la cosa a sinistra, poi fare la cosa a destra".

Un'altra cosa che può essere vista come una monade nei linguaggi orientati agli oggetti è la .. Spesso trovi cose come questa:

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

I .fondamentalmente significa "valutare il calcolo sulla sinistra, e quindi chiamare il metodo a destra sul risultato di tale". È un altro modo per combinare insieme funzioni / calcoli, un po 'più complicato di ;. E il concetto di concatenare le cose insieme .è una monade, dal momento che è un modo di combinare due calcoli insieme a un nuovo calcolo.

Un'altra monade abbastanza comune, che non ha una sintassi speciale, è questo modello:

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;

Un valore restituito di -1 indica un errore, ma non esiste un modo reale per sottrarre questo controllo degli errori, anche se si dispone di molte chiamate API che è necessario combinare in questo modo. Questo è fondamentalmente solo un'altra monade che combina le chiamate di funzione della regola "se la funzione a sinistra ha restituito -1, restituisce -1 noi stessi, altrimenti chiama la funzione a destra". Se avessimo un operatore >>=che faceva questa cosa potremmo semplicemente scrivere:

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

Renderebbe le cose più leggibili e aiuterebbe ad astrarre il nostro modo speciale di combinare le funzioni, in modo che non abbiamo bisogno di ripeterci ancora e ancora.

E ci sono molti altri modi per combinare funzioni / calcoli che sono utili come modello generale e che possono essere astratti in una monade, consentendo all'utente della monade di scrivere un codice molto più conciso e chiaro, dal momento che tutta la contabilità e la gestione di le funzioni usate vengono eseguite nella monade.

Ad esempio, quanto sopra >>=potrebbe essere esteso per "fare il controllo degli errori e quindi chiamare il lato destro sul socket che abbiamo ottenuto come input", in modo che non abbiamo bisogno di specificare esplicitamente socketmolte volte:

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

La definizione formale è un po 'più complicata poiché devi preoccuparti di come ottenere il risultato di una funzione come input per quella successiva, se quella funzione ha bisogno di quell'input e poiché vuoi assicurarti che le funzioni che combini si adattino il modo in cui provi a combinarli nella tua monade. Ma il concetto di base è solo quello di formalizzare modi diversi di combinare le funzioni.


28
Bella risposta! Ho intenzione di inserire una citazione di Oliver Steele, cercando di mettere in relazione Monads con un sovraccarico dell'operatore in C ++ o C #: Monads ti consente di sovraccaricare il ';' operatore.
Jörg W Mittag,

6
@ JörgWMittag Ho letto quella citazione prima, ma sembrava un'assurdità eccessivamente inebriante. Ora che capisco le monadi e leggo questa spiegazione di come ';' è uno, ho capito. Ma penso che sia davvero un'affermazione irrazionale per la maggior parte degli sviluppatori imperativi. ';' non è più visto come un operatore di quanto // sia per la maggior parte.
Jimmy Hoffa,

2
Sei sicuro di sapere cos'è una monade? Le monadi non sono una "funzione" o un calcolo, ci sono regole per le monadi.
Luis,

Nel tuo ;esempio: quali oggetti / tipi di dati vengono mappati ;? (Pensa alle Listmappe ) In che modo mappare i morfismi / le funzioni tra oggetti / tipi di dati? Ciò che è , , per ? TList<T>;purejoinbind;
Micha Wiedenmann,

44

È passato un anno da quando ho pubblicato questa domanda. Dopo averlo pubblicato, ho approfondito Haskell per un paio di mesi. Mi è piaciuto moltissimo, ma l'ho messo da parte proprio mentre ero pronto a scavare nelle Monadi. Sono tornato al lavoro e mi sono concentrato sulle tecnologie richieste dal mio progetto.

E ieri sera sono venuto a rileggere queste risposte. Ancora più importante , ho riletto il # esempio specifico C nei commenti di testo di Brian Beckman video di qualcuno accenna sopra . Era così completamente chiaro e illuminante che ho deciso di pubblicarlo direttamente qui.

A causa di questo commento, non solo sento di capire esattamente cosa sono le Monadi ... Mi rendo conto di aver effettivamente scritto alcune cose in C # che sono Monadi ... o almeno molto vicine, e che cerco di risolvere gli stessi problemi.

Quindi, ecco il commento - questa è tutta una citazione diretta dal commento qui di Sylvan :

Questo è abbastanza bello. È un po 'astratto però. Posso immaginare persone che non sanno quali monadi siano già confuse a causa della mancanza di esempi reali.

Quindi lasciami provare a rispettare, e solo per essere davvero chiaro farò un esempio in C #, anche se sembrerà brutto. Aggiungerò l'Haskell equivalente alla fine e ti mostrerò il fantastico zucchero sintattico di Haskell che è dove, IMO, le monadi iniziano davvero a diventare utili.

Ok, quindi una delle Monadi più semplici è chiamata "Forse monade" in Haskell. In C # viene chiamato il tipo Maybe Nullable<T>. È fondamentalmente una piccola classe che incapsula semplicemente il concetto di un valore che è valido e ha un valore, oppure è "null" e non ha valore.

Una cosa utile da tenere dentro una monade per combinare valori di questo tipo è la nozione di fallimento. Vale a dire che vogliamo essere in grado di guardare più valori nullable e restituire nullnon appena uno di essi è nullo. Questo potrebbe essere utile se, ad esempio, cerchi molte chiavi in ​​un dizionario o qualcosa del genere e alla fine desideri elaborare tutti i risultati e combinarli in qualche modo, ma se una qualsiasi delle chiavi non è presente nel dizionario, vuoi tornare nullper tutto. Sarebbe noioso dover controllare manualmente ogni ricerca nulle ritorno, quindi possiamo nascondere questo controllo all'interno dell'operatore di bind (che è una specie di punto di monade, nascondiamo la contabilità nell'operatore di bind che rende il codice più facile da utilizzare poiché possiamo dimenticare i dettagli).

Ecco il programma che motiva il tutto (lo definirò più Bindavanti, questo è solo per mostrarti perché è bello).

 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();
        }
    }

Ora, ignora per un momento che esiste già il supporto per farlo Nullablein C # (puoi aggiungere insieme null nullable e ottenere null se uno dei due è null). Facciamo finta che non ci sia una tale caratteristica, ed è solo una classe definita dall'utente senza magia speciale. Il punto è che possiamo usare la Bindfunzione per associare una variabile al contenuto del nostro Nullablevalore e quindi far finta che non ci sia nulla di strano e usarli come normali input e semplicemente sommarli. Abbiamo avvolgere il risultato in un annullabile alla fine, e che annullabile o sarà nullo (se qualsiasi f, go hritorni null) o sarà il risultato della somma f, gehinsieme. (questo è analogo al modo in cui possiamo associare una riga in un database a una variabile in LINQ e fare cose con essa, al sicuro nella consapevolezza che l' Bindoperatore si assicurerà che alla variabile vengano passati solo valori di riga validi).

Puoi giocare con questo e cambiare uno qualsiasi di f, ge hper restituire null e vedrai che l'intera cosa restituirà null.

Quindi, chiaramente, l'operatore di bind deve fare questo controllo per noi, e salvarlo restituendo null se incontra un valore null, e altrimenti passa il valore all'interno della Nullablestruttura nella lambda.

Ecco l' Bindoperatore:

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;
}

I tipi qui sono proprio come nel video. Prende un M a ( Nullable<A>nella sintassi C # per questo caso) e una funzione da aa M b( Func<A, Nullable<B>>nella sintassi C #) e restituisce un M b ( Nullable<B>).

Il codice controlla semplicemente se il nullable contiene un valore e, in tal caso, lo estrae e lo passa alla funzione, altrimenti restituisce solo null. Ciò significa che l' Bindoperatore gestirà per noi tutta la logica di controllo null. Se e solo se il valore che chiamiamo Bindnon è nullo, quel valore verrà "trasmesso" alla funzione lambda, altrimenti eseguiremo il salvataggio in anticipo e l'intera espressione sarà nulla. Ciò consente al codice che scriviamo utilizzando la monade di essere completamente libero da questo comportamento di controllo null, usiamo Binde otteniamo una variabile legata al valore all'interno del valore monadico ( e fval, nel codice di esempio) e possiamo usarli in modo sicuro nella conoscenza che si occuperà di controllarli per null prima di passarli.gvalhvalBind

Ci sono altri esempi di cose che puoi fare con una monade. Ad esempio, è possibile fare in modo che l' Bindoperatore si occupi di un flusso di input di caratteri e utilizzarlo per scrivere combinatori parser. Ogni combinatore di parser può quindi essere completamente ignaro di cose come il back-tracking, i fallimenti del parser ecc. E combinare semplicemente parser più piccoli insieme come se le cose non andassero mai male, sicure nella consapevolezza che un'implementazione intelligente Bindrisolve tutta la logica dietro pezzi difficili. Quindi, in seguito, forse qualcuno aggiunge la registrazione alla monade, ma il codice che utilizza la monade non cambia, poiché tutta la magia si verifica nella definizione Binddell'operatore, il resto del codice rimane invariato.

Infine, ecco l'implementazione dello stesso codice in Haskell ( -- inizia una riga di commento).

-- 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)

Come puoi vedere la bella donotazione alla fine fa sembrare un codice imperativo diretto. E infatti questo è di progettazione. Le monadi possono essere usate per incapsulare tutte le cose utili nella programmazione imperativa (stato mutabile, IO, ecc.) E usate usando questa bella sintassi simil-imperativa, ma dietro le tende ci sono solo monadi e un'implementazione intelligente dell'operatore bind! Il bello è che puoi implementare le tue monadi implementando >>=e return. E se lo fai, quelle monadi saranno anche in grado di usare la donotazione, il che significa che puoi praticamente scrivere le tue piccole lingue semplicemente definendo due funzioni!


3
Personalmente preferisco la versione F # della monade, ma in entrambi i casi sono fantastici.
ChaosPandion,

3
Grazie per essere tornato qui e aver aggiornato il tuo post. Sono follow-up come questi che aiutano i programmatori che stanno cercando in un'area specifica a capire davvero come gli altri programmatori alla fine considerano detta area, invece di avere semplicemente "come faccio a fare la tecnologia x in y" per uscire. Tu amico!
kappasims,

Ho preso la stessa strada che hai sostanzialmente e arrivato nello stesso posto comprendendo le monadi, che ha detto che questa è la migliore spiegazione del comportamento vincolante di una monade che io abbia mai visto per uno sviluppatore imperativo. Anche se penso che tu non stia completamente toccando tutto sulle monadi che è un po 'più spiegato sopra da STH.
Jimmy Hoffa,

@Jimmy Hoffa - senza dubbio hai ragione. Penso che per capirli davvero più in profondità, il modo migliore è iniziare a usarli molto e fare esperienza . Non ho ancora avuto questa opportunità, ma spero di farlo presto.
Charlie Flowers,

Sembra che la monade sia solo un livello più alto di astrazione in termini di programmazione, o è solo una definizione di funzione continua e non differenziabile in matematica. Ad ogni modo, non sono un nuovo concetto, specialmente in matematica.
liang,

11

Una monade è essenzialmente un'elaborazione differita. Se stai provando a scrivere un codice che ha effetti collaterali (ad es. I / O) in una lingua che non li consente e consente solo il calcolo puro, una schivata è dire "Ok, so che non farai effetti collaterali per me, ma puoi per favore calcolare cosa accadrebbe se lo facessi? "

È una specie di imbroglio.

Ora, questa spiegazione ti aiuterà a capire l'intento generale delle monadi, ma il diavolo è nei dettagli. Esattamente come si fa a calcolare le conseguenze? A volte, non è carino.

Il modo migliore per dare una visione d'insieme di come una persona abituata alla programmazione imperativa è di dire che ti mette in una DSL in cui le operazioni che assomigliano sintatticamente a ciò a cui sei abituato al di fuori della monade sono invece usate per costruire una funzione che farebbe quello che vuoi se potessi (ad esempio) scrivere su un file di output. Quasi (ma non proprio) come se si stesse costruendo il codice in una stringa per essere successivamente valutato.


1
Come nel libro I Robot? Dove lo scienziato chiede a un computer di calcolare i viaggi nello spazio e chiede loro di saltare alcune regole? :) :) :) :)
OscarRyz,

3
Hmm, A Monad può essere usato per l'elaborazione differita e per incapsulare funzioni di effetto collaterale, in effetti quella è stata la prima vera applicazione in Haskell, ma in realtà è un modello molto più generico. Altri usi comuni includono la gestione degli errori e la gestione dello stato. Lo zucchero sintattico (do in Haskell, Espressioni di calcolo in F #, sintassi di Linq in C #) è semplicemente questo e fondamentale per Monads in quanto tale.
Mike Hadlow,

@MikeHadlow: le istanze della monade per la gestione degli errori ( Maybee Either e) e la gestione dello stato ( State s, ST s) mi colpiscono come particolari istanze di "Per favore, calcola cosa accadrebbe se tu facessi [effetti collaterali per me]". Un altro esempio sarebbe il non determinismo ( []).
pyon,

questo è esattamente giusto; con una (bene, due) aggiunta che si tratta di un DSL E , ovvero DSL incorporato , poiché ogni valore "monadico" è un valore valido del linguaggio "puro" stesso, che rappresenta un "calcolo" potenzialmente impuro. Inoltre, esiste un costrutto monadico "vincolante" nel tuo linguaggio puro che ti consente di concatenare costruttori puri di tali valori in cui ciascuno verrà invocato con il risultato del suo calcolo precedente, quando l'intero calcolo combinato viene "eseguito". Ciò significa che abbiamo la possibilità di diramare risultati futuri (o comunque, della sequenza temporale separata "esegui").
Will Ness

ma per un programmatore significa che possiamo programmare in EDSL mescolandolo con i calcoli puri del nostro linguaggio puro. una pila di sandwich multistrato è un sandwich multistrato. è così semplice.
Will Ness,

4

Sono sicuro che altri utenti pubblicheranno in modo approfondito, ma ho trovato questo video utile in una certa misura, ma dirò che non sono ancora abbastanza fluente con il concetto in modo tale che potrei (o dovrei) iniziare a risolvere problemi intuitivi con Monads.


1
Ciò che ho trovato ancora più utile è stato il commento contenente un esempio C # sotto il video.
jalf

Non so di più utile, ma certamente mette in pratica le idee.
TheMissingLINQ

0

Puoi pensare a una monade come a un C # interfaceche le classi devono implementare . Questa è una risposta pragmatica che ignora tutta la categoria teorica matematica alla base del motivo per cui vorresti scegliere di avere queste dichiarazioni nella tua interfaccia e ignora tutti i motivi per cui vorresti avere monadi in un linguaggio che cerca di evitare effetti collaterali, ma ho trovato un buon inizio come qualcuno che capisce le interfacce (C #).


Puoi elaborare? Cos'è un'interfaccia che la collega alle monadi?
Joel Coehoorn,

2
Penso che il post sul blog spenda diversi paragrafi dedicati a questa domanda.
hao,

0

Vedi la mia risposta a "Cos'è una monade?"

Inizia con un esempio motivante, funziona attraverso l'esempio, deriva un esempio di monade e definisce formalmente "monade".

Non presuppone alcuna conoscenza della programmazione funzionale e utilizza lo pseudocodice con function(argument) := expressionsintassi con le espressioni più semplici possibili.

Questo programma C # è un'implementazione della monade pseudocodice. (Per riferimento: Mè il tipo costruttore, feedè l'operazione "bind", ed wrapè l'operazione "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);
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.