Monade in inglese semplice? (Per il programmatore OOP senza background FP)


743

In termini che un programmatore OOP avrebbe capito (senza alcun background di programmazione funzionale), che cos'è una monade?

Quale problema risolve e quali sono i luoghi più comuni in cui viene utilizzato?

MODIFICARE:

Per chiarire il tipo di comprensione che stavo cercando, diciamo che stavi convertendo un'applicazione FP che aveva monadi in un'applicazione OOP. Cosa faresti per trasferire le responsabilità delle monadi all'app OOP?




10
@Pavel: La risposta che abbiamo di seguito da Eric è molto migliore di quelle in quegli altri Q suggeriti per le persone con un background OO (al contrario di un background FP).
Donal Fellows,

5
@Donal: se questo è un duplicato (di cui non ho opinioni), la buona risposta dovrebbe essere aggiunta all'originale. Cioè: una buona risposta non preclude la chiusura come duplicato. Se si tratta di un duplicato abbastanza vicino, questo può essere realizzato da un moderatore come unione.
dmckee --- ex gattino moderatore

Risposte:


732

AGGIORNAMENTO: Questa domanda è stata oggetto di una serie di blog immensamente lunga, che puoi leggere su Monads - grazie per l'ottima domanda!

In termini che un programmatore OOP avrebbe capito (senza alcun background di programmazione funzionale), che cos'è una monade?

Una monade è un "amplificatore" di tipi che obbedisce a determinate regole e che prevede determinate operazioni .

Innanzitutto, cos'è un "amplificatore di tipi"? Con questo intendo un sistema che ti consente di prendere un tipo e trasformarlo in un tipo più speciale. Ad esempio, in C # considera Nullable<T>. Questo è un amplificatore di tipi. Ti permette di prendere un tipo, diciamo int, e aggiungere una nuova funzionalità a quel tipo, vale a dire che ora può essere nullo quando prima non poteva.

Come secondo esempio, considera IEnumerable<T>. È un amplificatore di tipi. Ti permette di prendere un tipo, diciamo, stringe aggiungere una nuova capacità a quel tipo, vale a dire che ora puoi creare una sequenza di stringhe da qualsiasi numero di stringhe singole.

Quali sono le "certe regole"? In breve, esiste un modo ragionevole per le funzioni sul tipo sottostante di lavorare sul tipo amplificato in modo tale da seguire le normali regole di composizione funzionale. Ad esempio, se hai una funzione su numeri interi, diciamo

int M(int x) { return x + N(x * 2); }

quindi la funzione corrispondente attiva Nullable<int>può far lavorare insieme tutti gli operatori e le chiamate "nello stesso modo" che avevano prima.

(È incredibilmente vago e impreciso; hai chiesto una spiegazione che non ha assunto nulla sulla conoscenza della composizione funzionale.)

Quali sono le "operazioni"?

  1. Esiste un'operazione "unit" (a volte confusamente chiamata operazione "return") che prende un valore da un tipo normale e crea il valore monadico equivalente. Questo, in sostanza, fornisce un modo per prendere un valore di tipo non amplificato e trasformarlo in un valore di tipo amplificato. Potrebbe essere implementato come costruttore in un linguaggio OO.

  2. Esiste un'operazione "bind" che accetta un valore monadico e una funzione che può trasformare il valore e restituisce un nuovo valore monadico. Il bind è l'operazione chiave che definisce la semantica della monade. Ci consente di trasformare le operazioni sul tipo non amplificato in operazioni sul tipo amplificato, che obbedisce alle regole di composizione funzionale menzionate in precedenza.

  3. Esiste spesso un modo per ripristinare il tipo non amplificato dal tipo amplificato. A rigor di termini questa operazione non è richiesta per avere una monade. (Anche se è necessario se si desidera avere un comonad . Non li considereremo più avanti in questo articolo.)

Ancora una volta, prendi Nullable<T>ad esempio. Puoi trasformare an intin a Nullable<int>con il costruttore. Il compilatore C # si occupa del "sollevamento" più nullo per te, ma in caso contrario, la trasformazione di sollevamento è semplice: un'operazione, diciamo,

int M(int x) { whatever }

si trasforma in

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

E trasformare una Nullable<int>schiena in un intè fatto con la Valueproprietà.

È la trasformazione della funzione che è il bit chiave. Si noti come la semantica effettiva dell'operazione nullable - che un'operazione su un nullpropaga il null- sia catturata nella trasformazione. Possiamo generalizzare questo.

Supponiamo di avere una funzione da inta int, come il nostro originale M. Puoi facilmente trasformarlo in una funzione che accetta inte restituisce a Nullable<int>perché puoi semplicemente eseguire il risultato attraverso il costruttore nullable. Supponiamo ora di avere questo metodo di ordine superiore:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

Vedi cosa puoi farci? Qualsiasi metodo che accetta un inte restituisce un int, o prende un inte restituisce un Nullable<int>può ora avere la semantica nullable applicata ad esso .

Inoltre: supponi di avere due metodi

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

e vuoi comporli:

Nullable<int> Z(int s) { return X(Y(s)); }

Cioè, Zè la composizione di Xe Y. Ma non puoi farlo perché Xprende un inte Yrestituisce un Nullable<int>. Ma poiché hai l'operazione "bind", puoi farlo funzionare:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

L'operazione di associazione su una monade è ciò che fa funzionare la composizione delle funzioni sui tipi amplificati. Le "regole" di cui ho parlato a mano sopra sono che la monade conserva le regole della normale composizione delle funzioni; che comporre con funzioni identitarie porta alla funzione originale, che la composizione è associativa e così via.

In C #, "Bind" è chiamato "SelectMany". Dai un'occhiata a come funziona sulla monade della sequenza. Dobbiamo avere due cose: trasformare un valore in una sequenza e associare le operazioni alle sequenze. Come bonus, abbiamo anche "trasformare una sequenza in un valore". Tali operazioni sono:

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 regola della monade nullable era "combinare due funzioni che producono insieme nullable, verificare se quella interna risulta nulla; in caso contrario, produrre null, in caso contrario, quindi chiamare quella esterna con il risultato". Questa è la semantica desiderata di nullable.

La regola della monade in sequenza è "combinare due funzioni che producono sequenze insieme, applicare la funzione esterna a ogni elemento prodotto dalla funzione interna e quindi concatenare insieme tutte le sequenze risultanti". La semantica fondamentale delle monadi è catturata nei metodi Bind/ SelectMany; questo è il metodo che ti dice cosa significa veramente la monade .

Possiamo fare ancora meglio. Supponiamo di avere una sequenza di ints e un metodo che accetta ints e genera sequenze di stringhe. Potremmo generalizzare l'operazione di associazione per consentire la composizione di funzioni che accettano e restituiscono diversi tipi amplificati, purché gli ingressi di uno corrispondano a quelli dell'altro:

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

Quindi ora possiamo dire "amplifica questo gruppo di singoli numeri interi in una sequenza di numeri interi. Trasforma questo particolare numero intero in un gruppo di stringhe, amplificato in una sequenza di stringhe. Ora metti insieme entrambe le operazioni: amplifica questo gruppo di numeri interi nella concatenazione di tutte le sequenze di stringhe ". Le monadi ti consentono di comporre le tue amplificazioni.

Quale problema risolve e quali sono i luoghi più comuni in cui viene utilizzato?

È piuttosto come chiedere "quali problemi risolve il modello singleton?", Ma ci proverò.

Le monadi sono in genere utilizzate per risolvere problemi come:

  • Devo creare nuove funzionalità per questo tipo e ancora combinare le vecchie funzioni su questo tipo per utilizzare le nuove funzionalità.
  • Ho bisogno di catturare un sacco di operazioni sui tipi e rappresentare quelle operazioni come oggetti componibili, costruendo composizioni sempre più grandi fino a quando non ho rappresentato la giusta serie di operazioni rappresentate, e quindi ho bisogno di iniziare a ottenere risultati
  • Devo rappresentare le operazioni con effetti collaterali in modo pulito in un linguaggio che odia gli effetti collaterali

C # usa monadi nel suo design. Come già accennato, il modello nullable è molto simile alla "forse monade". LINQ è interamente costruito con monadi; il SelectManymetodo è ciò che fa il lavoro semantico di composizione delle operazioni. (Erik Meijer ama sottolineare che ogni funzione LINQ potrebbe essere effettivamente implementata SelectMany; tutto il resto è solo una comodità.)

Per chiarire il tipo di comprensione che stavo cercando, diciamo che stavi convertendo un'applicazione FP che aveva monadi in un'applicazione OOP. Cosa faresti per trasferire le responsabilità delle monadi nell'app OOP?

La maggior parte dei linguaggi OOP non ha un sistema di tipi abbastanza ricco da rappresentare direttamente il modello di monade stesso; è necessario un sistema di tipi che supporti tipi di tipo superiore rispetto a tipi generici. Quindi non proverei a farlo. Piuttosto, implementerei tipi generici che rappresentano ciascuna monade e implementerei metodi che rappresentano le tre operazioni necessarie: trasformare un valore in un valore amplificato, (forse) trasformare un valore amplificato in un valore e trasformare una funzione su valori non amplificati in una funzione su valori amplificati.

Un buon punto di partenza è come abbiamo implementato LINQ in C #. Studia il SelectManymetodo; è la chiave per capire come funziona la monade della sequenza in C #. È un metodo molto semplice, ma molto potente!


Ulteriori letture consigliate:

  1. Per una spiegazione più approfondita e teoricamente corretta delle monadi in C #, consiglio vivamente l'articolo del mio collega ( Eric Lippert ) Wes Dyer sull'argomento. Questo articolo è ciò che mi ha spiegato le monadi quando alla fine mi hanno "cliccato" per me.
  2. Una buona illustrazione del motivo per cui potresti voler una monade in giro (usa Haskell nei suoi esempi) .
  3. Sorta di "traduzione" dell'articolo precedente su JavaScript.


17
Questa è un'ottima risposta, ma la mia testa è rimasta a bocca aperta. Lo seguirò e lo fisserò questo fine settimana e ti farò domande se le cose non si sistemano e hanno un senso nella mia testa.
Paul Nathan,

5
Ottima spiegazione come al solito Eric. Per una discussione più teorica (ma comunque molto interessante) ho trovato utile il post sul blog di Bart De Smet su MinLINQ utile nel correlare alcuni costrutti di programmazione funzionale con C #. community.bartdesmet.net/blogs/bart/archive/2010/01/01/…
Ron Warholic

41
Per me ha più senso dire che aumenta i tipi piuttosto che amplificarli .
Gabe,

61
@slomojo: e l'ho cambiato in quello che ho scritto e che intendevo scrivere. Se tu e Gabe volete scrivere la vostra risposta, andate avanti.
Eric Lippert,

24
@Eric, A te ovviamente, ma l'amplificatore implica che le proprietà esistenti sono potenziate, il che è fuorviante.
ocodo

341

Perché abbiamo bisogno delle monadi?

  1. Vogliamo programmare solo usando le funzioni . ("programmazione funzionale" dopo tutto -FP).
  2. Quindi, abbiamo un primo grosso problema. Questo è un programma:

    f(x) = 2 * x

    g(x,y) = x / y

    Come possiamo dire cosa deve essere eseguito per primo ? Come possiamo formare una sequenza ordinata di funzioni (ad es. Un programma ) usando nient'altro che funzioni?

    Soluzione: comporre funzioni . Se vuoi prima ge poi f, basta scrivere f(g(x,y)). Ok ma ...

  3. Altri problemi: alcune funzioni potrebbero non funzionare (ovvero g(2,0)dividere per 0). Non abbiamo "eccezioni" in FP . Come lo risolviamo?

    Soluzione: consentiamo alle funzioni di restituire due tipi di cose : invece di avere g : Real,Real -> Real(funzione da due reali in un reale), consentiamo g : Real,Real -> Real | Nothing(funzione da due reali in (reale o nulla)).

  4. Ma le funzioni dovrebbero (per essere più semplici) restituire solo una cosa .

    Soluzione: creiamo un nuovo tipo di dati da restituire, un " tipo di boxe " che racchiuda forse un reale o semplicemente nulla. Quindi, possiamo avere g : Real,Real -> Maybe Real. Ok ma ...

  5. Cosa succede adesso f(g(x,y))? fnon è pronto a consumare a Maybe Real. E, non vogliamo cambiare ogni funzione con cui potremmo connetterci gper consumare a Maybe Real.

    Soluzione: disponiamo di una funzione speciale per "connettere" / "comporre" / "link" funzioni . In questo modo, dietro le quinte, possiamo adattare l'output di una funzione per alimentare quella successiva.

    Nel nostro caso: g >>= f(connettiti / componi ga f). Vogliamo >>=ottenere gl'output, ispezionarlo e, nel caso sia Nothingsolo non chiamare fe restituire Nothing; o al contrario, estrarre la scatola Reale nutrirla fcon essa. (Questo algoritmo è solo l'implementazione di >>=per il Maybetipo).

  6. Sorgono molti altri problemi che possono essere risolti utilizzando questo stesso modello: 1. Utilizzare una "casella" per codificare / memorizzare diversi significati / valori e avere funzioni simili grestituiscono quei "valori in scatola". 2. Avere compositori / linker g >>= fper aiutare a collegare gl'output fall'input, quindi non dobbiamo cambiare faffatto.

  7. I problemi notevoli che possono essere risolti usando questa tecnica sono:

    • avendo uno stato globale che ogni funzione nella sequenza di funzioni ("il programma") può condividere: soluzione StateMonad.

    • Non ci piacciono le "funzioni impure": funzioni che producono output diversi per lo stesso input. Pertanto, contrassegniamo quelle funzioni, facendole restituire un valore taggato / inscatolato: IOmonade.

Felicità totale !!!!


2
@DmitriZaitsev Eccezioni possono verificarsi solo in "codice impuro" (la monade IO) per quanto ne so.
cibercitizen1

3
@DmitriZaitsev Il ruolo di Nothing può essere interpretato da qualsiasi altro tipo (diverso dal Real atteso). Non è questo il punto. Nell'esempio, la questione è come adattare le funzioni in una catena quando la precedente può restituire un tipo di valore imprevisto alla successiva, senza concatenare quest'ultima (accettando solo un reale come input).
cibercitizen1,

3
Un altro punto di confusione è che la parola "monade" appare solo due volte nella tua risposta, e solo in combinazione con altri termini - Statee IO, senza nessuno di essi, così come il significato esatto di "monade"
Dmitri Zaitsev,

31
Per me come persona che proviene da un background OOP questa risposta ha davvero spiegato bene la motivazione che sta dietro l'avere una monade e anche ciò che la monade è in realtà (molto più che una risposta accettata). Quindi lo trovo molto utile. Grazie mille @ cibercitizen1 e +1
akhilless

3
Ho letto e disattivato la programmazione funzionale per circa un anno. Questa risposta, e in particolare i primi due punti, mi ha fatto finalmente capire cosa significhi effettivamente la programmazione imperativa e perché la programmazione funzionale è diversa. Grazie!
jrahhali,

82

Direi che l'analogia OO più vicina alle monadi è il " modello di comando ".

Nel modello di comando si avvolge un'istruzione o un'espressione ordinaria in un oggetto comando . L'oggetto comando espone un metodo execute che esegue l'istruzione wrapped. Quindi le istruzioni vengono trasformate in oggetti di prima classe che possono essere passati ed eseguiti a piacimento. I comandi possono essere composti in modo da poter creare un oggetto programma concatenando e annidando oggetti comando.

I comandi vengono eseguiti da un oggetto separato, l' invocatore . Il vantaggio dell'uso del modello di comando (piuttosto che di eseguire una serie di istruzioni ordinarie) è che diversi invocatori possono applicare una logica diversa a come eseguire i comandi.

Il modello di comando può essere utilizzato per aggiungere (o rimuovere) funzionalità della lingua non supportate dalla lingua host. Ad esempio, in un ipotetico linguaggio OO senza eccezioni, è possibile aggiungere una semantica di eccezione esponendo i metodi "provare" e "lanciare". Quando un comando chiama lanciare, il chiamante fa un passo indietro attraverso l'elenco (o l'albero) dei comandi fino all'ultima chiamata "prova". Al contrario, è possibile rimuovere la semantica delle eccezioni da una lingua (se si ritiene che le eccezioni siano errate ) rilevando tutte le eccezioni generate da ciascun singolo comando e trasformandole in codici di errore che vengono quindi passati al comando successivo.

Anche semantiche di esecuzione più fantasiose come transazioni, esecuzioni non deterministiche o continuazioni possono essere implementate in questo modo in un linguaggio che non lo supporta in modo nativo. È un modello abbastanza potente se ci pensate.

Ora in realtà i modelli di comando non sono usati come una caratteristica del linguaggio generale come questa. Il sovraccarico di trasformare ogni istruzione in una classe separata porterebbe a una quantità insopportabile di codice del boilerplate. Ma in linea di principio può essere usato per risolvere gli stessi problemi che le monadi sono usate per risolvere in fp.


15
Credo che questa sia la prima spiegazione della monade che ho visto che non si basava su concetti di programmazione funzionale e la metteva in termini OOP reali. Davvero una buona risposta.
David K. Hess,

ciò è molto vicino a ciò che sono effettivamente le monadi in FP / Haskell, tranne per il fatto che gli oggetti comando stessi "sanno" a quale "logica di invocazione" appartengono (e solo quelli compatibili possono essere concatenati); invoker fornisce solo il primo valore. Non è che il comando "Stampa" possa essere eseguito da "logica di esecuzione non deterministica". No, deve essere "Logica I / O" (cioè monade IO). Ma a parte questo, è molto vicino. Si potrebbe anche solo dire che le Monadi sono solo programmi (costruiti con dichiarazioni di codice, da eseguire in seguito). All'inizio "bind" veniva definito "punto e virgola programmabile" .
Will Ness,

1
@ DavidK.Hess Sono davvero incredibilmente scettico nei confronti delle risposte che usano FP per spiegare i concetti base di FP, e in particolare le risposte che usano un linguaggio FP come Scala. Ben fatto, JacquesB!
Ripristina Monica il

62

In termini che un programmatore OOP avrebbe capito (senza alcun background di programmazione funzionale), che cos'è una monade?

Quale problema risolve e quali sono i luoghi più comuni in cui viene utilizzato? I luoghi più comuni in cui viene utilizzato?

In termini di programmazione OO, una monade è un'interfaccia (o più probabilmente un mixin), parametrizzata da un tipo, con due metodi returne bindche descrive:

  • Come iniettare un valore per ottenere un valore monadico di quel tipo di valore iniettato;
  • Come usare una funzione che fa un valore monadico da un valore non monadico, su un valore monadico.

Il problema che risolve è lo stesso tipo di problema che ti aspetteresti da qualsiasi interfaccia, vale a dire: "Ho un sacco di classi diverse che fanno cose diverse, ma sembrano fare quelle cose diverse in un modo che ha una somiglianza sottostante. posso descrivere quella somiglianza tra loro, anche se le classi stesse non sono in realtà sottotipi di qualcosa di più vicino della stessa classe "l'Oggetto"? "

Più specificamente, l ' Monad"interfaccia" è simile IEnumeratoro IIteratorin quanto prende un tipo che a sua volta ne assume un tipo. Il principale "punto" di Monadperò è essere in grado di collegare le operazioni in base al tipo interno, fino al punto di avere un nuovo "tipo interno", mantenendo - o addirittura migliorando - la struttura delle informazioni della classe principale.


1
returnin realtà non sarebbe un metodo sulla monade, perché non prende un'istanza della monade come argomento. (cioè: non c'è questo / sé)
Laurence Gonsalves l'

@LaurenceGonsalves: Dal momento che sto esaminando questo argomento per la mia tesi di laurea, penso che ciò che limita maggiormente sia la mancanza di metodi statici nelle interfacce in C # / Java. Potresti fare molta strada nella direzione dell'implementazione dell'intera storia della monade, almeno staticamente vincolata invece che sulla base di macchine da scrivere. È interessante notare che questo funzionerebbe anche nonostante la mancanza di tipi di tipo superiore.
Sebastian Graf,

42

Hai una recente presentazione " Monadologie - aiuto professionale sull'ansia da tipo " di Christopher League (12 luglio 2010), che è piuttosto interessante su temi di continuazione e monade.
Il video che accompagna questa presentazione (slide) è attualmente disponibile su vimeo .
La parte Monade inizia in circa 37 minuti, in questo video di un'ora, e inizia con la diapositiva 42 della sua presentazione 58 diapositive.

Viene presentato come "il modello di progettazione principale per la programmazione funzionale", ma il linguaggio usato negli esempi è Scala, che è sia OOP che funzionale.
Puoi leggere di più su Monad in Scala nel post del blog " Monads - Another way to abstract computations in Scala ", di Debasish Ghosh (27 marzo 2008).

Un costruttore di tipo M è una monade se supporta queste operazioni:

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

Quindi, ad esempio (in Scala):

  • Option è una monade
    def unit [A] (x: A): Opzione [A] = Alcuni (x)

    def flatMap [A, B] (m: Opzione [A]) (f: A => Opzione [B]): Opzione [B] =
      m match {
       case Nessuno => Nessuno
       case Some (x) => f (x)
      }
  • List è Monade
    def unit [A] (x: A): Elenco [A] = Elenco (x)

    def flatMap [A, B] (m: Elenco [A]) (f: A => Elenco [B]): Elenco [B] =
      m match {
        case Nil => Nil
        case x :: xs => f (x) ::: flatMap (xs) (f)
      }

Le Monade sono un grosso problema in Scala a causa della comoda sintassi costruita per sfruttare le strutture Monade:

forcomprensione in Scala :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

è tradotto dal compilatore in:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

L'astrazione chiave è la flatMap, che lega il calcolo attraverso il concatenamento.
Ogni invocazione di flatMaprestituisce lo stesso tipo di struttura dati (ma di diverso valore), che funge da input per il comando successivo nella catena.

Nel frammento precedente, flatMap accetta come input una chiusura (SomeType) => List[AnotherType]e restituisce a List[AnotherType]. Il punto importante da notare è che tutte le flatMap prendono lo stesso tipo di chiusura dell'input e restituiscono lo stesso tipo dell'output.

Questo è ciò che "lega" il thread di calcolo: ogni elemento della sequenza nella comprensione preliminare deve rispettare questo stesso vincolo di tipo.


Se si eseguono due operazioni (che potrebbero non riuscire) e si passa il risultato al terzo, come:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

ma senza sfruttare Monad, ottieni un codice OOP contorto come:

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
  }

mentre con Monad, puoi lavorare con i tipi effettivi ( Venue, User) come tutte le operazioni funzionano e mantenere nascosti gli elementi di verifica delle opzioni, tutto a causa delle mappe piatte della sintassi for:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

La parte di rendimento verrà eseguita solo se tutte e tre le funzioni hanno Some[X]; qualsiasi Nonesarebbe direttamente restituito a confirm.


Così:

Le monadi consentono il calcolo ordinato all'interno della programmazione funzionale, che ci consente di modellare il sequenziamento delle azioni in una bella forma strutturata, un po 'come una DSL.

E il più grande potere deriva dalla capacità di comporre monadi che servono a scopi diversi, in astrazioni estensibili all'interno di un'applicazione.

Questo sequenziamento e threading di azioni da parte di una monade viene eseguito dal compilatore del linguaggio che esegue la trasformazione attraverso la magia delle chiusure.


A proposito, Monad non è solo un modello di calcolo utilizzato in FP:

La teoria delle categorie propone molti modelli di calcolo. Tra loro

  • il modello di calcolo Arrow
  • il modello di calcolo della Monade
  • il modello applicativo di calcoli

2
Adoro questa spiegazione! L'esempio che hai dato dimostra magnificamente il concetto e aggiunge anche ciò che IMHO mancava dal teaser di Eric sul fatto che SelectMany () fosse una Monade. Grazie per questo!
fatto l'

1
IMHO questa è la risposta più elegante
Polymerase,

e prima di tutto, Functor.
Will Ness,

34

Per rispettare i lettori veloci, inizio prima con una definizione precisa, proseguo con una rapida spiegazione più "semplice" in inglese, quindi passo agli esempi.

Ecco una definizione sia concisa che precisa leggermente riformulata:

Una monade (in informatica) è formalmente una mappa che:

  • invia ogni tipo Xdi un determinato linguaggio di programmazione a un nuovo tipo T(X)(chiamato "tipo di T-computer con valori in X");

  • dotato di una regola per comporre due funzioni della forma f:X->T(Y)e g:Y->T(Z)una funzione g∘f:X->T(Z);

  • in modo associativo in senso evidente e unitario rispetto a una determinata funzione unitaria chiamata pure_X:X->T(X), da considerare come valore per il calcolo puro che semplicemente restituisce quel valore.

Quindi, in parole semplici, una monade è una regola per passare da qualsiasi tipo Xa un altro tipoT(X) e una regola per passare da due funzioni f:X->T(Y)e g:Y->T(Z)(che si desidera comporre ma non è possibile) a una nuova funzioneh:X->T(Z) . Che, tuttavia, non è la composizione in senso matematico rigoroso. Fondamentalmente stiamo "piegando" la composizione della funzione o ridefinendo il modo in cui le funzioni sono composte.

Inoltre, abbiamo bisogno della regola di composizione della monade per soddisfare gli "ovvi" assiomi matematici:

  • Associatività : comporre fcon ge poi con h(dall'esterno) dovrebbe essere lo stesso che comporre gcon he poi con f(dall'interno).
  • Proprietà unitaria : la composizione fcon la funzione identità su entrambi i lati dovrebbe produrre f.

Ancora una volta, in parole semplici, non possiamo semplicemente impazzire ridefinendo la composizione della nostra funzione come ci piace:

  • Abbiamo prima bisogno dell'associatività per essere in grado di comporre diverse funzioni di seguito f(g(h(k(x))), ad esempio , e di non preoccuparci di specificare l'ordine delle coppie di funzioni di composizione. Dato che la regola della monade prescrive solo come comporre una coppia di funzioni , senza quell'assioma, dovremmo sapere quale coppia è composta per prima e così via. (Si noti che è diverso dalla proprietà di commutatività che ha fcomposto gerano gli stessi di gcomposti con f, che non è richiesto).
  • E in secondo luogo, abbiamo bisogno della proprietà unitaria, che è semplicemente per dire che le identità compongono banalmente il modo in cui le aspettiamo. In questo modo possiamo refactoring in modo sicuro ogni volta che tali identità possono essere estratte.

Quindi di nuovo in breve: una monade è la regola dell'estensione del tipo e delle funzioni compositive che soddisfano i due assiomi: associatività e proprietà unitaria.

In termini pratici, vuoi che la monade sia implementata per te dal linguaggio, dal compilatore o dal framework che si occuperà di comporre le funzioni per te. Quindi puoi concentrarti sulla scrittura della logica della tua funzione piuttosto che preoccuparti di come viene implementata la loro esecuzione.

Questo è essenzialmente, in poche parole.


Essendo matematico professionista, preferisco evitare di chiamare hla "composizione" di fe g. Perché matematicamente non lo è. Chiamarla "composizione" erroneamente presuppone che hsia la vera composizione matematica, cosa che non lo è. Non è nemmeno determinato in modo univoco da fe g. Invece, è il risultato della nuova "regola di composizione" della nostra monade delle funzioni. Che può essere totalmente diverso dall'effettiva composizione matematica anche se quest'ultima esiste!


Per renderlo meno asciutto, fammi provare ad illustrarlo con l'esempio che sto annotando con piccole sezioni, in modo da poter saltare direttamente al punto.

Eccezione come esempi Monade

Supponiamo di voler comporre due funzioni:

f: x -> 1 / x
g: y -> 2 * y

Ma f(0)non è definito, quindi eviene generata un'eccezione . Quindi come puoi definire il valore compositivo g(f(0))? Lancia di nuovo un'eccezione, ovviamente! Forse lo stesso e. Forse una nuova eccezione aggiornata e1.

Cosa succede esattamente qui? Innanzitutto, abbiamo bisogno di nuovi valori di eccezione (diversi o uguali). È possibile chiamare nothingo nullo qualsiasi altra cosa, ma l'essenza rimane la stessa - dovrebbero essere i nuovi valori, ad esempio, non dovrebbe essere una numbernel nostro esempio qui. Preferisco non chiamarli nullper evitare confusione con come nullpuò essere implementato in qualsiasi lingua specifica. Allo stesso modo preferisco evitare nothingperché è spesso associato null, il che, in linea di principio, è ciò che nulldovrebbe fare, tuttavia, tale principio viene spesso piegato per qualsiasi motivo pratico.

Che cos'è esattamente l'eccezione?

Questa è una cosa banale per qualsiasi programmatore esperto, ma vorrei far cadere alcune parole solo per estinguere qualsiasi worm di confusione:

L'eccezione è un oggetto che incapsula informazioni su come si è verificato il risultato non valido dell'esecuzione.

Questo può variare dal buttare via tutti i dettagli e restituire un singolo valore globale (come NaNo null) o generare un lungo elenco di registri o cosa è successo esattamente, inviarlo a un database e replicare su tutto il livello di archiviazione dei dati distribuiti;)

La differenza importante tra questi due esempi estremi di eccezione è che nel primo caso non ci sono effetti collaterali . Nel secondo ci sono. Il che ci porta alla domanda (da mille dollari):

Sono ammesse eccezioni nelle funzioni pure?

Risposta più breve : Sì, ma solo quando non portano a effetti collaterali.

Risposta più lunga. Per essere puri, l'output della tua funzione deve essere determinato in modo univoco dal suo input. Quindi modifichiamo la nostra funzione finviando 0al nuovo valore astratto eche chiamiamo eccezione. Ci assicuriamo che il valore enon contenga informazioni esterne che non siano determinate in modo univoco dal nostro input, ovvero x. Quindi, ecco un esempio di eccezione senza effetti collaterali:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

Ed eccone uno con effetti collaterali:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

In realtà, ha effetti collaterali solo se quel messaggio può cambiare in futuro. Ma se è garantito che non cambierà mai, quel valore diventa unicamente prevedibile, e quindi non ci sono effetti collaterali.

Per renderlo ancora più sciocco. Una funzione che ritorna 42mai è chiaramente pura. Ma se qualcuno pazzo decide di creare 42una variabile che il valore potrebbe cambiare, la stessa funzione smette di essere pura nelle nuove condizioni.

Si noti che sto usando l'oggetto notazione letterale per semplicità per dimostrare l'essenza. Sfortunatamente le cose sono incasinate in linguaggi come JavaScript, dove errornon c'è un tipo che si comporti come vogliamo qui rispetto alla composizione delle funzioni, mentre i tipi reali gradiscono nullo NaNnon si comportano in questo modo ma piuttosto passano attraverso alcuni artificiali e non sempre intuitivi digitare conversioni.

Digita estensione

Poiché vogliamo variare il messaggio all'interno della nostra eccezione, dichiariamo davvero un nuovo tipo Eper l'intero oggetto eccezione e quindi Questo è ciò che maybe numberfa, a parte il suo nome confuso, che deve essere di tipo numbero del nuovo tipo di eccezione E, quindi è davvero l'unione number | Edi numbere E. In particolare, dipende da come vogliamo costruire E, che non è né suggerito né riflesso nel nome maybe number.

Cos'è la composizione funzionale?

È le funzioni matematiche funzionamento taking f: X -> Yed g: Y -> Ze costruire la loro composizione in funzione h: X -> Zsoddisfacente h(x) = g(f(x)). Il problema con questa definizione si verifica quando il risultato f(x)non è consentito come argomento di g.

In matematica queste funzioni non possono essere composte senza lavoro extra. La soluzione rigorosamente matematica per il nostro esempio sopra di fe gè quella di rimuovere 0dall'insieme di definizione di f. Con quel nuovo insieme di definizioni (nuovo tipo più restrittivo di x), fdiventa componibile con g.

Tuttavia, non è molto pratico in programmazione limitare l'insieme della definizione di fsimile. Invece, è possibile utilizzare le eccezioni.

O come un altro approccio, valori artificiali vengono create come NaN, undefined, null, Infinityecc Quindi valutate 1/0per Infinitye 1/-0per -Infinity. E quindi forzare il nuovo valore nella tua espressione invece di generare un'eccezione. Portando a risultati che potresti trovare o meno prevedibili:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

E torniamo ai numeri regolari pronti per andare avanti;)

JavaScript ci consente di continuare a eseguire espressioni numeriche a qualsiasi costo senza generare errori come nell'esempio sopra. Ciò significa che consente anche di comporre funzioni. Il che è esattamente ciò di cui parla la monade: è una regola comporre funzioni che soddisfino gli assiomi come definiti all'inizio di questa risposta.

Ma la regola di comporre la funzione, derivante dall'implementazione di JavaScript per gestire gli errori numerici, è una monade?

Per rispondere a questa domanda, tutto ciò che serve è controllare gli assiomi (lasciato come esercizio fisico come non parte della domanda qui;).

L'eccezione di lancio può essere usata per costruire una monade?

In effetti, una monade più utile sarebbe invece la regola che prescrive che se fgenera un'eccezione per alcuni x, così fa la sua composizione con qualsiasi g. Inoltre, rendono l'eccezione Eglobalmente unica con un solo valore possibile in assoluto ( oggetto terminale nella teoria delle categorie). Ora i due assiomi sono immediatamente controllabili e otteniamo una monade molto utile. E il risultato è ciò che è noto come forse monade .


3
Buon contributo. +1 Ma forse vorresti eliminare "ho trovato la maggior parte delle spiegazioni troppo a lungo ..." essendo il tuo il più lungo a tutti. Altri giudicheranno se si tratta di "inglese semplice" come richiesto dalla domanda: "inglese semplice == in parole semplici, in modo semplice".
cibercitizen1

@ cibercitizen1 Grazie! In realtà è breve, se non si considera l'esempio. Il punto principale è che non è necessario leggere l'esempio per comprendere la definizione . Sfortunatamente molte spiegazioni mi costringono a leggere prima degli esempi , che spesso non sono necessari ma, ovviamente, potrebbero richiedere un lavoro extra per lo scrittore. Facendo troppo affidamento su esempi specifici, esiste il pericolo che dettagli non importanti oscurino l'immagine e rendano più difficile la comprensione. Detto questo, hai punti validi, vedi l'aggiornamento.
Dmitri Zaitsev

2
troppo lungo e confuso
seenimurugan

1
@seenimurugan Suggerimenti di miglioramento sono ben accetti;)
Dmitri Zaitsev

26

Una monade è un tipo di dati che incapsula un valore e al quale, essenzialmente, possono essere applicate due operazioni:

  • return x crea un valore del tipo di monade che incapsula x
  • m >>= f(leggilo come "l'operatore bind") applica la funzione fal valore nella monadem

Ecco cos'è una monade. Ci sono alcuni tecnicismi in più , ma sostanzialmente queste due operazioni definiscono una monade. La vera domanda è: "Che una monade fa ?", E che dipende la monade - liste sono monadi, Maybes sono monadi, le operazioni di IO sono monadi. Tutto ciò che significa quando diciamo che quelle cose sono monadi è che hanno l'interfaccia monade di returne >>=.


"Cosa fa una monade, e ciò dipende dalla monade": e più precisamente, dipende dalla bindfunzione che deve essere definita per ogni tipo di monade, non è vero? Sarebbe una buona ragione per non confondere il legame con la composizione, poiché esiste una singola definizione per composizione, mentre non può esserci una sola definizione per una funzione di legame, ce n'è una per tipo monadico, se capisco correttamente.
Hibou57,

14

Da Wikipedia :

Nella programmazione funzionale, una monade è un tipo di tipo di dati astratto utilizzato per rappresentare i calcoli (anziché i dati nel modello di dominio). Le monadi consentono al programmatore di concatenare le azioni per costruire una pipeline, in cui ogni azione è decorata con regole di elaborazione aggiuntive fornite dalla monade. I programmi scritti in stile funzionale possono utilizzare le monadi per strutturare procedure che includono operazioni in sequenza, 1 [2] o per definire flussi di controllo arbitrari (come gestire la concorrenza, le continuazioni o le eccezioni).

Formalmente, una monade viene costruita definendo due operazioni (bind e return) e un costruttore di tipo M che deve soddisfare diverse proprietà per consentire la corretta composizione delle funzioni monadiche (cioè funzioni che usano i valori della monade come argomenti). L'operazione di ritorno prende un valore da un tipo semplice e lo inserisce in un contenitore monadico di tipo M. L'operazione di associazione esegue il processo inverso, estraendo il valore originale dal contenitore e passandolo alla successiva funzione associata nella pipeline.

Un programmatore comporrà funzioni monadiche per definire una pipeline di elaborazione dati. La monade funge da framework, in quanto è un comportamento riutilizzabile che decide l'ordine in cui vengono chiamate le specifiche funzioni monadiche nella pipeline e gestisce tutto il lavoro sotto copertura richiesto dal calcolo. [3] Gli operatori di bind e return interlacciati nella pipeline verranno eseguiti dopo che ciascuna funzione monadica restituirà il controllo e si prenderanno cura degli aspetti particolari gestiti dalla monade.

Credo che lo spieghi molto bene.


12

Proverò a fare la definizione più breve che posso gestire usando i termini OOP:

Una classe generica CMonadic<T>è una monade se definisce almeno i seguenti metodi:

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
}

e se le seguenti leggi si applicano a tutti i tipi T e ai loro possibili valori t

identità sinistra:

CMonadic<T>.create(t).flatMap(f) == f(t)

giusta identità

instance.flatMap(CMonadic<T>.create) == instance

associatività:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Esempi :

Una monade elenco può avere:

List<int>.create(1) --> [1]

E flatMap nell'elenco [1,2,3] potrebbe funzionare così:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

Iterabili e osservabili possono anche essere resi monadici, così come promesse e compiti.

Commento :

Le monadi non sono così complicate. La flatMapfunzione è molto simile a quella più comunemente riscontrata map. Riceve un argomento di funzione (noto anche come delegato), che può chiamare (immediatamente o successivamente, zero o più volte) con un valore proveniente dalla classe generica. Si aspetta che la funzione passata includa anche il suo valore restituito nello stesso tipo di classe generica. Per aiutarlo, fornisce createun costruttore che può creare un'istanza di quella classe generica da un valore. Il risultato di restituzione di flatMap è anche una classe generica dello stesso tipo, spesso comprimendo gli stessi valori che erano contenuti nei risultati di restituzione di una o più applicazioni di flatMap ai valori precedentemente contenuti. Ciò ti consente di concatenare flatMap quanto vuoi:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

Accade così che questo tipo di classe generica sia utile come modello base per un numero enorme di cose. Questo (insieme al gergo teorico di categoria) è il motivo per cui le Monadi sembrano così difficili da capire o da spiegare. Sono una cosa molto astratta e diventano ovviamente utili solo quando sono specializzati.

Ad esempio, è possibile modellare le eccezioni utilizzando contenitori monadici. Ciascun contenitore conterrà il risultato dell'operazione o l'errore che si è verificato. La funzione successiva (delegato) nella catena di callback flatMap verrà chiamata solo se la precedente impacchettava un valore nel contenitore. Altrimenti se un errore è stato compresso, l'errore continuerà a propagarsi attraverso i contenitori concatenati fino a quando non viene trovato un contenitore con una funzione di gestione degli errori collegata tramite un metodo chiamato .orElse()(tale metodo sarebbe un'estensione consentita)

Note : I linguaggi funzionali consentono di scrivere funzioni in grado di operare su qualsiasi tipo di classe generica monadica. Perché ciò funzioni, si dovrebbe scrivere un'interfaccia generica per le monadi. Non so se è possibile scrivere una tale interfaccia in C #, ma per quanto ne so non lo è:

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
}

7

Se una monade abbia un'interpretazione "naturale" in OO dipende dalla monade. In una lingua come Java, puoi tradurre forse la monade nel linguaggio di controllo dei puntatori null, in modo che i calcoli che falliscono (cioè producono Nothing in Haskell) emettano puntatori null come risultati. Puoi tradurre la monade di stato nella lingua generata creando una variabile mutabile e metodi per cambiarne lo stato.

Una monade è un monoide nella categoria di endofunctor.

Le informazioni che la frase mette insieme sono molto profonde. E lavori in monade con qualsiasi linguaggio imperativo. Una monade è un linguaggio specifico del dominio "sequenziato". Soddisfa alcune proprietà interessanti, che nel loro insieme rendono una monade un modello matematico di "programmazione imperativa". Haskell semplifica la definizione di linguaggi imperativi piccoli (o grandi), che possono essere combinati in vari modi.

Come programmatore OO, usi la gerarchia di classi della tua lingua per organizzare i tipi di funzioni o procedure che possono essere chiamate in un contesto, ciò che chiami un oggetto. Una monade è anche un'astrazione su questa idea, in quanto diverse monadi possono essere combinate in modi arbitrari, "importando" efficacemente tutti i metodi della sub-monade nell'ambito.

Dal punto di vista architettonico, si usano quindi le firme dei tipi per esprimere esplicitamente quali contesti possono essere utilizzati per calcolare un valore.

A questo scopo si possono usare trasformatori di monade e esiste una raccolta di alta qualità di tutte le monadi "standard":

  • Elenchi (calcoli non deterministici, trattando un elenco come dominio)
  • Forse (calcoli che possono fallire, ma per i quali la segnalazione non è importante)
  • Errore (calcoli che possono fallire e richiedere la gestione delle eccezioni
  • Reader (calcoli che possono essere rappresentati da composizioni di semplici funzioni di Haskell)
  • Writer (calcoli con "rendering" / "logging" sequenziale (su stringhe, HTML ecc.)
  • Cont (continuazioni)
  • IO (calcoli che dipendono dal sistema informatico sottostante)
  • Stato (calcoli il cui contesto contiene un valore modificabile)

con corrispondenti trasformatori di monade e classi di tipi. Le classi di tipo consentono un approccio complementare alla combinazione di monadi unificando le loro interfacce, in modo che le monadi concrete possano implementare un'interfaccia standard per il "tipo" di monade. Ad esempio, il modulo Control.Monad.State contiene una classe MonadState sm e (State s) è un'istanza del modulo

instance MonadState s (State s) where
    put = ...
    get = ...

La lunga storia è che una monade è un funzione che attacca il "contesto" a un valore, che ha un modo per iniettare un valore nella monade e che ha un modo per valutare i valori rispetto al contesto ad esso collegato, almeno in modo limitato.

Così:

return :: a -> m a

è una funzione che inietta un valore di tipo a in una "azione" di monade di tipo m a.

(>>=) :: m a -> (a -> m b) -> m b

è una funzione che esegue un'azione monade, ne valuta il risultato e applica una funzione al risultato. La cosa bella di (>> =) è che il risultato è nella stessa monade. In altre parole, in m >> = f, (>> =) estrae il risultato da m e lo lega a f, in modo che il risultato sia nella monade. (In alternativa, possiamo dire che (>> =) tira f in m e lo applica al risultato.) Di conseguenza, se abbiamo f :: a -> mb e g :: b -> mc, possiamo azioni "sequenziali":

m >>= f >>= g

Oppure, usando "notazione"

do x <- m
   y <- f x
   g y

Il tipo per (>>) potrebbe essere illuminante. È

(>>) :: m a -> m b -> m b

Corrisponde all'operatore (;) in linguaggi procedurali come C. Permette di fare notazioni come:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

Nella logica matematica e filosofica, abbiamo cornici e modelli, che sono "naturalmente" modellati con il monadismo. Un'interpretazione è una funzione che esamina il dominio del modello e calcola il valore di verità (o generalizzazioni) di una proposizione (o formula, sotto generalizzazioni). In una logica modale per necessità, potremmo dire che una proposizione è necessaria se è vera in "ogni mondo possibile" - se è vera rispetto ad ogni dominio ammissibile. Ciò significa che un modello in una lingua per una proposizione può essere reificato come un modello il cui dominio è costituito dalla raccolta di modelli distinti (uno corrispondente a ciascun mondo possibile). Ogni monade ha un metodo chiamato "join" che appiattisce i livelli, il che implica che ogni azione monade il cui risultato è un'azione monade può essere incorporata nella monade.

join :: m (m a) -> m a

Ancora più importante, significa che la monade è chiusa sotto l'operazione di "sovrapposizione dei livelli". Ecco come funzionano i trasformatori di monade: combinano monadi fornendo metodi "simili a join" per tipi come

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

in modo che possiamo trasformare un'azione in (MaybeT m) in un'azione in m, facendo collassare efficacemente i livelli. In questo caso, runMaybeT :: MaybeT ma -> m (Forse a) è il nostro metodo simile a join. (MaybeT m) è una monade e MaybeT :: m (Maybe a) -> MaybeT ma è effettivamente un costruttore di un nuovo tipo di azione monade in m.

Una monade libera per un funzione è la monade generata impilando f, con l'implicazione che ogni sequenza di costruttori per f è un elemento della monade libera (o, più esattamente, qualcosa con la stessa forma dell'albero delle sequenze di costruttori per f). Le monadi libere sono una tecnica utile per costruire monadi flessibili con una quantità minima di piastra della caldaia. In un programma Haskell, potrei usare monadi libere per definire monadi semplici per "programmazione di sistemi di alto livello" per aiutare a mantenere la sicurezza dei tipi (sto solo usando i tipi e le loro dichiarazioni. Le implementazioni sono dirette con l'uso di combinatori):

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

Il monadismo è l'architettura sottostante per quello che potreste chiamare il modello "interprete" o "comando", astratto nella sua forma più chiara, poiché ogni calcolo monadico deve essere "eseguito", almeno banalmente. (Il sistema di runtime esegue la monade IO per noi ed è il punto di accesso a qualsiasi programma Haskell. IO "guida" il resto dei calcoli, eseguendo le azioni IO in ordine).

Il tipo di join è anche il punto in cui otteniamo l'affermazione che una monade è un monoide nella categoria degli endofunctor. Unire è in genere più importante per scopi teorici, in virtù del suo tipo. Ma capire il tipo significa capire le monadi. I tipi simili a join di join e trasformatori di monade sono in effetti composizioni di endofunctor, nel senso di composizione delle funzioni. Per dirla in uno pseudo-linguaggio simile a Haskell,

Foo :: m (ma) <-> (m. M) a


3

Una monade è una matrice di funzioni

(Pst: un array di funzioni è solo un calcolo).

In realtà, invece di un vero array (una funzione in un array di celle) hai quelle funzioni concatenate da un'altra funzione >> =. >> = consente di adattare i risultati dalla funzione i alla funzione i + 1, eseguire calcoli tra loro o, persino, non chiamare la funzione i + 1.

I tipi utilizzati qui sono "tipi con contesto". Questo è un valore con un "tag". Le funzioni concatenate devono assumere un "valore nudo" e restituire un risultato con tag. Uno dei doveri di >> = è quello di estrarre un valore nudo dal suo contesto. C'è anche la funzione "return", che prende un valore nudo e lo mette con un tag.

Un esempio con Forse . Usiamolo per memorizzare un intero semplice su cui effettuare calcoli.

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

Giusto per mostrare che le monadi sono una matrice di funzioni con operazioni di aiuto, considera l'equivalente dell'esempio sopra, usando solo una vera matrice di funzioni

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

E sarebbe usato così:

print (runMyMonad (Just 160) myArray1)

1
Super-pulito! Quindi il bind è solo un modo per valutare una serie di funzioni con contesto, in sequenza, su un input con contesto :)
Musa Al-hassy,

>>=è un operatore
user2418306

1
Penso che l'analogia della "matrice di funzioni" non chiarisca molto. If \x -> x >>= k >>= l >>= mè un array di funzioni, quindi è h . g . f, che non coinvolge affatto le monadi.
Duplode

potremmo dire che funtori , se monadica, applicativo o di pianura, sono circa "l'applicazione abbellito" . 'applicativo' aggiunge il concatenamento e 'monade' aggiunge dipendenza (cioè creando il prossimo passo di calcolo in base ai risultati di un precedente passo di calcolo).
Will Ness,

3

In termini di OO, una monade è un contenitore fluente.

Il requisito minimo è una definizione class <A> Somethingche supporti un costruttore Something(A a)e almeno un metodoSomething<B> flatMap(Function<A, Something<B>>)

Probabilmente, conta anche se la tua classe monade ha dei metodi con firma Something<B> work()che preservano le regole della classe: il compilatore si lancia in flatMap al momento della compilazione.

Perché una monade è utile? Perché è un contenitore che consente operazioni a catena che preservano la semantica. Ad esempio, Optional<?>mantiene la semantica di isPresent per Optional<String>, Optional<Integer>,Optional<MyClass> , etc.

A titolo di esempio,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Nota iniziamo con una stringa e finiamo con un numero intero. Abbastanza bello

In OO, potrebbe essere necessario un piccolo cenno della mano, ma qualsiasi metodo su Something che restituisce un'altra sottoclasse di Something soddisfa il criterio di una funzione contenitore che restituisce un contenitore del tipo originale.

È così che preservi la semantica - ovvero il significato e le operazioni del contenitore non cambiano, ma semplicemente avvolgono e migliorano l'oggetto all'interno del contenitore.


2

Le monadi nell'uso tipico sono l'equivalente funzionale dei meccanismi di gestione delle eccezioni della programmazione procedurale.

Nei moderni linguaggi procedurali, si mette un gestore di eccezioni attorno a una sequenza di dichiarazioni, ognuna delle quali può generare un'eccezione. Se una delle istruzioni genera un'eccezione, l'esecuzione normale della sequenza delle istruzioni si interrompe e si trasferisce a un gestore di eccezioni.

I linguaggi di programmazione funzionale, tuttavia, evitano filosoficamente le funzioni di gestione delle eccezioni a causa della loro natura "goto". La prospettiva di programmazione funzionale è che le funzioni non dovrebbero avere "effetti collaterali" come le eccezioni che interrompono il flusso del programma.

In realtà, gli effetti collaterali non possono essere esclusi nel mondo reale principalmente a causa dell'I / O. Le monadi nella programmazione funzionale vengono utilizzate per gestire ciò prendendo una serie di chiamate di funzione concatenate (ognuna delle quali potrebbe produrre un risultato imprevisto) e trasformando qualsiasi risultato imprevisto in dati incapsulati che possono ancora fluire in modo sicuro attraverso le rimanenti chiamate di funzione.

Il flusso di controllo viene preservato ma l'evento imprevisto viene incapsulato e gestito in modo sicuro.


2

Una semplice spiegazione delle Monadi con un caso di studio della Marvel è qui .

Le monadi sono astrazioni utilizzate per sequenziare funzioni dipendenti efficaci. Efficace qui significa che restituiscono un tipo in forma F [A] per esempio Opzione [A] dove Opzione è F, chiamato costruttore di tipo. Vediamo questo in 2 semplici passaggi

  1. Sotto la composizione della funzione è transitiva. Quindi per andare da A a CI puoi comporre A => B e B => C.
 A => C   =   A => B  andThen  B => C

inserisci qui la descrizione dell'immagine

  1. Tuttavia, se la funzione restituisce un tipo di effetto come Opzione [A] cioè A => F [B] la composizione non funziona come per andare su B abbiamo bisogno di A => B ma abbiamo A => F [B].
    inserisci qui la descrizione dell'immagine

    Abbiamo bisogno di un operatore speciale, "bind" che sappia fondere queste funzioni che restituiscono F [A].

 A => F[C]   =   A => F[B]  bind  B => F[C]

La funzione "bind" è definita per la F specifica .

Esiste anche "return" , di tipo A => F [A] per qualsiasi A , definito anche per quella F specifica . Per essere una Monade, F deve avere queste due funzioni definite per essa.

Quindi possiamo costruire una funzione efficace A => F [B] da qualsiasi funzione pura A => B ,

 A => F[B]   =   A => B  andThen  return

ma una data F può anche definire le proprie funzioni speciali "incorporate" opache di tali tipi che un utente non può definirsi (in un linguaggio puro ), come

  • "random" ( Range => Random [Int] )
  • "print" ( String => IO [()] )
  • "prova ... cattura", ecc.

2

Condivido la mia comprensione di Monads, che potrebbe non essere teoricamente perfetta. Le monadi riguardano la propagazione del contesto . Monad è che si definisce un contesto per alcuni dati (o tipi di dati) e quindi si definisce come tale contesto verrà trasportato con i dati durante la sua pipeline di elaborazione. E definire la propagazione del contesto riguarda principalmente la definizione di come unire più contesti (dello stesso tipo). L'uso delle Monadi significa anche garantire che questi contesti non vengano accidentalmente cancellati dai dati. D'altra parte, altri dati senza contesto possono essere portati in un contesto nuovo o esistente. Quindi questo semplice concetto può essere utilizzato per garantire la correttezza del tempo di compilazione di un programma.



1

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 pseudocodice con function(argument) := expression sintassi 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".)

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

0

Da un punto di vista pratico (riassumendo ciò che è stato detto in molte risposte precedenti e articoli correlati), mi sembra che uno degli "scopi" (o utilità) fondamentali della monade sia di sfruttare le dipendenze implicite nelle invocazioni del metodo ricorsivo ovvero la composizione della funzione (ovvero quando f1 chiama f2 chiama f3, f3 deve essere valutata prima di f2 prima di f1) per rappresentare la composizione sequenziale in modo naturale, specialmente nel contesto di un modello di valutazione pigro (ovvero, la composizione sequenziale come una sequenza semplice , ad esempio "f3 (); f2 (); f1 ();" in C - il trucco è particolarmente ovvio se si pensa a un caso in cui f3, f2 e f1 in realtà non restituiscono nulla [il loro concatenamento come f1 (f2 (f3)) è artificiale, puramente inteso a creare sequenza]).

Ciò è particolarmente rilevante quando sono coinvolti effetti collaterali, cioè quando uno stato viene alterato (se f1, f2, f3 non ha avuto effetti collaterali, non importa in quale ordine vengono valutati; che è una grande proprietà di puro linguaggi funzionali, per poter parallelizzare quei calcoli per esempio). Più funzioni pure, meglio è.

Penso da quel punto di vista ristretto, le monadi potrebbero essere viste come zucchero sintattico per le lingue che favoriscono la valutazione pigra (che valuta le cose solo quando assolutamente necessario, seguendo un ordine che non si basa sulla presentazione del codice) e che non hanno altri mezzi per rappresentare la composizione sequenziale. Il risultato netto è che sezioni di codice che sono "impure" (cioè che hanno effetti collaterali) possono essere presentate in modo naturale, in modo imperativo, ma sono nettamente separate da funzioni pure (senza effetti collaterali), che possono essere valutato pigramente.

Questo è solo un aspetto, come avvertito qui .


0

La spiegazione più semplice che mi viene in mente è che le monadi sono un modo di comporre funzioni con risultati impreziositi (aka composizione Kleisli). Una funzione "abbellita" ha la firma a -> (b, smth)dove ae bsono tipi (pensa Int, Bool) che potrebbero essere diversi l'uno dall'altro, ma non necessariamente - ed smthè il "contesto" o l '"abbellimento".

Questo tipo di funzioni può anche essere scritto a -> m bdove mè equivalente all '"abbellimento" smth. Quindi queste sono funzioni che restituiscono valori nel contesto (pensa a funzioni che registrano le loro azioni, dov'è smthil messaggio di registrazione; o funzioni che eseguono input \ output e i loro risultati dipendono dal risultato dell'azione IO).

Una monade è un'interfaccia ("typeclass") che fa dire all'implementatore come comporre tali funzioni. L'implementatore deve definire una funzione di composizione (a -> m b) -> (b -> m c) -> (a -> m c)per qualsiasi tipo mche desideri implementare l'interfaccia (questa è la composizione di Kleisli).

Quindi, se diciamo che abbiamo un tipo di tupla che (Int, String)rappresenta i risultati di calcoli su Ints che registrano anche le loro azioni, con l' (_, String)essere "abbellimento" - il registro dell'azione - e due funzioni increment :: Int -> (Int, String)e twoTimes :: Int -> (Int, String)vogliamo ottenere una funzione incrementThenDouble :: Int -> (Int, String)che è la composizione delle due funzioni che tiene conto anche dei registri.

Nell'esempio dato, un'implementazione monade delle due funzioni si applica al valore intero 2 incrementThenDouble 2(che è uguale a twoTimes (increment 2)) restituirebbe (6, " Adding 1. Doubling 3.")per risultati intermedi increment 2uguali (3, " Adding 1.")e twoTimes 3uguali a(6, " Doubling 3.")

Da questa funzione di composizione di Kleisli si possono derivare le solite funzioni monadiche.

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.