Monade în engleză simplă? (Pentru programatorul OOP fără experiență în FP) (Programare, Oop, Programare Funcțională, Monade)

user65663 a intrebat.

În termeni pe care un programator OOP i-ar înțelege (fără niciun fel de cunoștințe de programare funcțională), ce este o monadă?

Ce problemă rezolvă și care sunt cele mai frecvente locuri în care este folosită?

Actualizare

Pentru a clarifica tipul de înțelegere pe care îl căutam, să spunem că ați convertit o aplicație FP care avea monade într-o aplicație OOP. Ce ați face pentru a transfera responsabilitățile monadelor în aplicația OOP?

Comentarii

  • Această postare pe blog este foarte bună: blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html –  > Por Pascal Cuoq.
  • posibile dubluri: stackoverflow.com/questions/129534/what-are-monads stackoverflow.com/questions/2366/can-anyone-explain-monads –  > Por the_drow.
  • @Pavel: Răspunsul pe care l-am primit mai jos de la Eric este mult mai bun decât cele din acele alte întrebări sugerate pentru persoanele cu un background OO (spre deosebire de un background FP). –  > Por Donal Fellows.
  • @Donal: Dacă acest lucru este un duplicat (despre care nu am nicio părere), răspunsul bun ar trebui adăugat la cel original. Adică: un răspuns bun nu exclude închiderea ca fiind un duplicat. Dacă este un duplicat suficient de apropiat, acest lucru poate fi realizat de un moderator ca o fuziune. –  > Por dmckee — fostul moderator pisicuță.
  • Vezi și: stackoverflow.com/questions/674855/… –  > Por sth.
19 răspunsuri

UPDATE: Această întrebare a fost subiectul unei serii de bloguri extrem de lungi, pe care o puteți citi la adresa Monadele – mulțumesc pentru întrebarea excelentă!

În termeni pe care un programator OOP i-ar înțelege (fără niciun fel de cunoștințe de programare funcțională), ce este o monadă?

O monadă este o „amplificator” de tipuri care se supune anumitor reguli și care are anumite operații prevăzute.

În primul rând, ce este un „amplificator de tipuri”? Prin aceasta înțeleg un sistem care vă permite să luați un tip și să îl transformați într-un tip mai special. De exemplu, în C# considerați Nullable<T>. Acesta este un amplificator de tipuri. Acesta vă permite să luați un tip, de exemplu int, și să adăugați o nouă capabilitate la acel tip, și anume că acum poate fi nul, când înainte nu putea fi.

Ca un al doilea exemplu, luați în considerare IEnumerable<T>. Acesta este un amplificator de tipuri. Acesta vă permite să luați un tip, să zicem, string, și să adăugați o nouă capacitate la acel tip, și anume că acum puteți crea o secvență de șiruri de caractere din orice număr de șiruri simple.

Care sunt „anumite reguli”? Pe scurt, că există o modalitate rezonabilă prin care funcțiile de tip subiacent să funcționeze pe tipul amplificat, astfel încât să respecte regulile normale de compoziție funcțională. De exemplu, dacă aveți o funcție asupra numerelor întregi, de exemplu

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

atunci funcția corespunzătoare pe Nullable<int> poate face ca toți operatorii și apelurile de acolo să funcționeze împreună „în același mod” în care o făceau înainte.

(Acest lucru este incredibil de vag și imprecis; ați cerut o explicație care să nu presupună nimic despre cunoașterea compoziției funcționale).

Care sunt „operațiile”?

  1. Există o operație „unitară” (numită uneori, în mod confuz, operația „return”) care ia o valoare dintr-un tip simplu și creează valoarea monadică echivalentă. Aceasta, în esență, oferă o modalitate de a lua o valoare de tip neamplificat și de a o transforma într-o valoare de tip amplificat. Ar putea fi implementat ca un constructor într-un limbaj OO.

  2. Există o operațiune „bind” care ia o valoare monadică și o funcție care poate transforma valoarea și returnează o nouă valoare monadică. „Bind” este operația cheie care definește semantica monadei. Aceasta ne permite să transformăm operațiile asupra tipului neamplificat în operații asupra tipului amplificat, care respectă regulile de compoziție funcțională menționate anterior.

  3. Există adesea o modalitate de a obține tipul neamplificat înapoi din tipul amplificat. Strict vorbind, nu este necesar ca această operație să aibă o monadă. (Deși este necesară dacă doriți să aveți o comonadă. Nu le vom lua în considerare mai departe în acest articol).

Din nou, luați Nullable<T> ca exemplu. Puteți transforma un int într-un Nullable<int> cu ajutorul constructorului. Compilatorul C# are grijă de majoritatea „ridicării” de nulitate pentru dvs., dar dacă nu o face, transformarea de ridicare este simplă: o operație, să zicem,

int M(int x) { whatever }

este transformată în

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

Iar transformarea unui Nullable<int> înapoi într-un int se face cu ajutorul funcției Value proprietate.

Transformarea funcției este elementul cheie. Observați cum semantica reală a operației „nullable” – faptul că o operație asupra unui element null propagă efectul null – este captată în transformare. Putem generaliza acest lucru.

Să presupunem că aveți o funcție din int către int, precum cea originală M. Puteți transforma cu ușurință această funcție într-o funcție care ia un element int și returnează un Nullable<int> deoarece puteți trece rezultatul prin constructorul nul. Să presupunem acum că aveți această metodă de ordin superior:

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

Vedeți ce puteți face cu ea? Orice metodă care ia un an int și returnează un intsau ia un int și returnează un Nullable<int> i se poate aplica acum semantica „nullable”..

În plus: să presupunem că aveți două metode

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

și doriți să le compuneți:

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

Adică, Z este compoziția X și Y. Dar nu se poate face acest lucru deoarece X ia un int, și Y returnează un Nullable<int>. Dar, din moment ce aveți operația „bind”, puteți face acest lucru să funcționeze:

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

Operația „bind” pe o monadă este cea care face ca compoziția funcțiilor pe tipuri amplificate să funcționeze. „Regulile” despre care am vorbit mai sus sunt că monada păstrează regulile de compunere normală a funcțiilor: compunerea cu funcții identice are ca rezultat funcția originală, compoziția este asociativă și așa mai departe.

În C#, „Bind” se numește „SelectMany”. Aruncați o privire la modul în care funcționează pe monada secvență. Avem nevoie de două lucruri: să transformăm o valoare într-o secvență și să legăm operațiile de legare a secvențelor. Ca bonus, avem și „transformă o secvență înapoi într-o valoare”. Aceste operații sunt:

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

Regula monadei nullable era „pentru a combina două funcții care produc nullables împreună, verificați dacă cea interioară are ca rezultat null; dacă da, produce null, dacă nu, atunci apelați-o pe cea exterioară cu rezultatul”. Aceasta este semantica dorită pentru nullable.

Regula monadei de secvențe este „pentru a combina două funcții care produc secvențe împreună, aplicați funcția exterioară fiecărui element produs de funcția interioară și apoi concatenați toate secvențele rezultate împreună”. Semantica fundamentală a monadelor este captată în regula Bind/SelectMany metode; aceasta este metoda care vă spune ce înseamnă cu adevărat monada înseamnă în realitate monada.

Putem face chiar mai bine. Să presupunem că aveți o secvență de ints și o metodă care ia ints și are ca rezultat secvențe de șiruri de caractere. Am putea generaliza operația de legare pentru a permite compunerea funcțiilor care preiau și returnează tipuri amplificate diferite, atâta timp cât intrările uneia corespund ieșirilor celeilalte:

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

Astfel, acum putem spune „amplifică acest grup de numere întregi individuale într-o secvență de numere întregi. Transformați acest număr întreg particular într-un grup de șiruri de caractere, amplificat într-o secvență de șiruri de caractere. Acum puneți ambele operații împreună: amplificați acest mănunchi de numere întregi în concatenarea tuturor secvențelor de șiruri de caractere”. Monadele vă permit să să compuneți amplificările dumneavoastră.

Ce problemă rezolvă și care sunt cele mai frecvente locuri în care este folosită?

Este mai degrabă ca și cum ai întreba „ce probleme rezolvă modelul singleton?”, dar voi face o încercare.

Monadele sunt utilizate în mod obișnuit pentru a rezolva probleme precum:

  • Trebuie să creez noi capacități pentru acest tip și să combin în continuare funcțiile vechi ale acestui tip pentru a utiliza noile capacități.
  • Trebuie să capturez o serie de operații asupra tipurilor și să reprezint aceste operații ca obiecte care pot fi compuse, construind compoziții din ce în ce mai mari până când am reprezentat exact seria potrivită de operații, iar apoi trebuie să încep să obțin rezultate din acest obiect.
  • Trebuie să reprezint în mod curat operațiile cu efecte secundare într-un limbaj care urăște efectele secundare.

C# folosește monade în proiectarea sa. După cum s-a menționat deja, modelul nullable este foarte asemănător cu „maybe monad”. LINQ este construit în întregime din monade; modelul SelectMany metoda este cea care face munca semantică de compunere a operațiilor. (Erik Meijer este pasionat de a sublinia că fiecare funcție LINQ ar putea fi de fapt implementată de către SelectMany; orice altceva este doar o comoditate).

Pentru a clarifica tipul de înțelegere pe care îl căutam, să spunem că ați converti o aplicație FP care avea monade într-o aplicație OOP. Ce ați face pentru a transpune responsabilitățile monadelor în aplicația OOP?

Cele mai multe limbaje OOP nu au un sistem de tipuri suficient de bogat pentru a reprezenta direct modelul monadelor în sine; aveți nevoie de un sistem de tipuri care să suporte tipuri care sunt tipuri superioare celor generice. Așadar, nu aș încerca să fac asta. Mai degrabă, aș implementa tipuri generice care să reprezinte fiecare monadă și aș implementa metode care să reprezinte cele trei operații de care aveți nevoie: transformarea unei valori într-o valoare amplificată, (poate) transformarea unei valori amplificate într-o valoare și transformarea unei funcții pe valori neamplificate într-o funcție pe valori amplificate.

Un bun punct de plecare este modul în care am implementat LINQ în C#. Studiați pagina SelectMany metoda; aceasta este cheia pentru a înțelege cum funcționează monada secvențială în C#. Este o metodă foarte simplă, dar foarte puternică!


Sugestii de lectură suplimentară, sugerate:

  1. Pentru o explicație mai aprofundată și mai solidă din punct de vedere teoretic a monadelor în C#, vă recomand cu căldură articolul colegului meu (Eric Lippert) Wes Dyer pe această temă. Acest articol este cel care mi-a explicat monadele atunci când, în sfârșit, acestea au făcut „clic” pentru mine.
  2. O bună ilustrare a motivelor pentru care ați putea dori să aveți o monadă în preajmă (folosește Haskell în exemplele sale).
  3. Un fel de „traducere” a articolului anterior în JavaScript.

Comentarii

    18

  • Acesta este un răspuns grozav, dar mi s-a făcut ascotit capul. Voi urmări și mă voi holba la el în acest weekend & vă voi pune întrebări dacă lucrurile nu se liniștesc și nu au sens în capul meu. –  > Por Paul Nathan.
  • Excelentă explicație, ca de obicei, Eric. Pentru o discuție mai teoretică (dar totuși extrem de interesantă), am găsit utilă postarea lui Bart De Smet pe blogul său despre MinLINQ pentru a relaționa unele construcții de programare funcțională cu C#. community.bartdesmet.net/blogs/bart/archive/2010/01/01/01/… –  > Por Ron Warholic.
  • 43

  • Mie mi se pare mai logic să spun că sporește tipurile, mai degrabă decât amplifică le amplifică. –  > Por Gabe.
  • 61

  • @slomojo: și am schimbat-o înapoi la ceea ce am scris și am intenționat să scriu. Dacă tu și Gabe vreți să vă scrieți propriul răspuns, mergeți înainte. –  > Por Eric Lippert.
  • 26

  • @Eric, Depinde de tine, desigur, dar Amplifier implică faptul că proprietățile existente sunt amplificate, ceea ce este înșelător. –  > Por ocodo.
cibercitizen1

De ce avem nevoie de monade?

  1. Vrem să programăm numai cu ajutorul funcțiilor. („programare funcțională”, la urma urmei -FP).
  2. Atunci, avem o primă mare problemă. Acesta este un program:

    f(x) = 2 * x

    g(x,y) = x / y

    Cum putem spune ce trebuie să fie executat mai întâi? Cum putem forma o secvență ordonată de funcții (de ex. un program) care nu folosește decât funcții?

    Soluție: compunem funcții. Dacă doriți mai întâi g și apoi f, trebuie doar să scrieți f(g(x,y)). Bine, dar…

  3. Alte probleme: unele funcții ar putea eșua (de ex. g(2,0), împărțiți la 0). Avem excepții” în FP. Cum rezolvăm această problemă?

    Soluție: Să să permitem funcțiilor să returneze două tipuri de lucruri: în loc de a avea g : Real,Real -> Real (funcție de la două reale la un real), să permitem ca g : Real,Real -> Real | Nothing (funcție din două reale în (real sau nimic)).

  4. Dar funcțiile ar trebui (pentru a fi mai simple) să returneze doar un singur lucru.

    Soluție: să creăm un nou tip de date care să fie returnate, un „tip de boxe„, care poate cuprinde un real sau pur și simplu nimic. Prin urmare, putem avea g : Real,Real -> Maybe Real. Bine, dar …

  5. Ce se întâmplă acum cu f(g(x,y))? f nu este gata să consume un Maybe Real. Și, nu vrem să schimbăm fiecare funcție pe care am putea să o conectăm cu g pentru a consuma a Maybe Real.

    Soluție: să să avem o funcție specială pentru a „conecta”/”compune”/”lega” funcții. În acest fel, putem, în spatele scenei, să adaptăm ieșirea unei funcții pentru a o alimenta pe următoarea.

    În cazul nostru: g >>= f (connect/compose g la f). Ne dorim ca >>= să obținem gsă o inspectăm și, în cazul în care aceasta este Nothing pur și simplu să nu apelăm f și să returnăm Nothing; sau, dimpotrivă, să extragem caseta Real și introduceți f cu ea. (Acest algoritm este doar implementarea lui >>= pentru Maybe tip).

  6. Apar multe alte probleme care pot fi rezolvate folosind același model: 1. Folosiți o „cutie” pentru a codifica/stoca diferite semnificații/valori și aveți funcții precum g care să returneze acele „valori cuplate”. 2. Puneți compozitori/legători g >>= f pentru a ajuta la conectarea g‘s output to f‘s input, astfel încât să nu trebuiască să modificăm f deloc.

  7. Problemele remarcabile care pot fi rezolvate cu ajutorul acestei tehnici sunt:

    • având o stare globală pe care fiecare funcție din secvența de funcții („programul”) o poate împărtăși: soluție StateMonad.

    • Nu ne plac „funcțiile impure”: funcții care produc diferite ieșire pentru același intrare. Prin urmare, haideți să marcăm aceste funcții, făcându-le să returneze o valoare etichetată/cutie: IO monadă.

Fericire totală !!!!

Comentarii

  • @DmitriZaitsev Din câte știu, excepțiile pot apărea doar în „codul impur” (monada IO). –  > Por cibercitizen1.
  • @DmitriZaitsev Rolul de Nothing poate fi jucat de orice alt tip (diferit de Realul așteptat). Nu aceasta este ideea. În exemplu, problema este cum să se adapteze funcțiile dintr-un lanț atunci când cea anterioară poate returna un tip de valoare neașteptat pentru următoarea, fără a o înlănțui pe aceasta din urmă (acceptând doar un Real ca intrare). –  > Por cibercitizen1.
  • Un alt punct de confuzie este faptul că cuvântul „monadă” apare doar de două ori în răspunsul dvs. și doar în combinație cu alți termeni -. State și IO, fără ca niciunul dintre ei, precum și sensul exact al cuvântului „monadă” să fie dat –  > Por Dmitri Zaitsev.
  • 36

  • Pentru mine, ca persoană care provine dintr-un mediu OOP, acest răspuns a explicat foarte bine motivația care stă la baza existenței unei monade și, de asemenea, ce este de fapt o monadă (mult mai mult decât un răspuns acceptat). Așadar, mi se pare foarte util. Mulțumesc mult @cibercitizen1 și +1 –  > Por akhilless.
  • Am citit despre programarea funcțională din când în când de aproximativ un an. Acest răspuns, și în special primele două puncte, m-au făcut să înțeleg în sfârșit ce înseamnă de fapt programarea imperativă și de ce programarea funcțională este diferită. Vă mulțumesc! –  > Por jrahhali.
JacquesB

Aș spune că cea mai apropiată analogie OO cu monadele este „model de comandă„.

În tiparul de comandă, înfășurați o instrucțiune sau o expresie obișnuită într-o expresie comandă obiect de comandă. Obiectul de comandă expune un obiect execute care execută instrucțiunea înfășurată. Astfel, declarația este transformată în obiecte de primă clasă, care pot fi transmise și executate în voie. Comenzile pot fi compuse astfel încât se poate crea un obiect-program prin înlănțuirea și îmbinarea obiectelor-comandă.

Comenzile sunt executate de către un obiect separat, obiectul invoker. Avantajul utilizării modelului de comandă (mai degrabă decât executarea unei serii de instrucțiuni obișnuite) este că diferiți invocatori pot aplica o logică diferită pentru modul în care ar trebui executate comenzile.

Modelul de comandă ar putea fi utilizat pentru a adăuga (sau elimina) caracteristici ale limbajului care nu sunt acceptate de limbajul gazdă. De exemplu, într-un limbaj OO ipotetic fără excepții, ați putea adăuga o semantică a excepțiilor prin expunerea metodelor „try” și „throw” la comenzi. Atunci când o comandă apelează „throw”, cel care o invocă se întoarce înapoi prin lista (sau arborele) de comenzi până la ultimul apel „try”. Invers, ați putea elimina semantica excepțiilor dintr-un limbaj (dacă credeți că excepțiile sunt rele) prin capturarea tuturor excepțiilor aruncate de fiecare comandă în parte și transformarea lor în coduri de eroare care sunt apoi transmise la următoarea comandă.

Chiar și o semantică de execuție mai fantezistă, cum ar fi tranzacțiile, execuția nedeterministă sau continuările, poate fi implementată astfel într-un limbaj care nu o suportă în mod nativ. Este un model destul de puternic dacă vă gândiți la asta.

Acum, în realitate, modelul de comandă nu este utilizat ca o caracteristică generală a limbajului ca aceasta. Suprasolicitarea de a transforma fiecare instrucțiune într-o clasă separată ar duce la o cantitate insuportabilă de cod boilerplate. Dar, în principiu, acesta poate fi folosit pentru a rezolva aceleași probleme ca și monadele folosite în fp.

Comentarii

    17

  • Cred că aceasta este prima explicație a monadelor pe care am văzut-o care nu se bazează pe concepte de programare funcțională și o prezintă în termeni reali de OOP. Un răspuns foarte bun. –  > Por David K. Hess.
  • acest lucru este foarte aproape 2 de ceea ce sunt de fapt monadele în FP/Haskell, cu excepția faptului că obiectele de comandă în sine „știu” de care „logică de invocare” aparțin (și numai cele compatibile pot fi înlănțuite împreună); invocatorul doar furnizează prima valoare. Nu este ca și cum comanda „Print” ar putea fi executată de o „logică de execuție nedeterministă”. Nu, trebuie să fie o „logică I/O” (adică monada IO). Dar, în afară de asta, este foarte aproape. Ați putea chiar să spuneți că Monadele sunt doar programe (construite din declarații de cod, care urmează să fie executate ulterior). În primele zile, se vorbea despre „bind” ca fiind „punct și virgulă programabil”. –  > Por Will Ness.
  • @DavidK.Hess Într-adevăr, sunt incredibil de sceptic cu privire la răspunsurile care folosesc FP pentru a explica conceptele de bază ale FP și, mai ales, la răspunsurile care folosesc un limbaj FP precum Scala. Foarte bine, JacquesB! –  > Por Kuba nu a uitat-o pe Monica.
  • Da, cele mai multe dintre celelalte răspunsuri și postările de pe bloguri legate și altele par să presupună o cunoaștere fundamentală a lui Haskell și a sintaxei sale, ceea ce nu este deosebit de util, așa că acest lucru dacă este foarte apreciat! –  > Por Tasgall.
BMeph

În termeni pe care un programator OOP i-ar înțelege (fără niciun fel de cunoștințe de programare funcțională), ce este o monadă?

Ce problemă rezolvă și care sunt cele mai frecvente locuri în care este folosită?sunt cele mai frecvente locuri în care este folosită?

În ceea ce privește programarea OO, o monadă este o interfață (sau mai degrabă un mixin), parametrizată de un tip, cu două metode, return și bind care descriu:

  • Cum se injectează o valoare pentru a obține o valoare amonadică a acelui tip de valoare injectat;
  • cum se utilizează o funcție care transformă o valoare monadică în valoare monadică dintr-una nemonadică, pe o valoare monadică.

Problema pe care o rezolvă este același tip de problemă pe care o așteptați de la orice interfață, și anume: „Am o grămadă de clase diferite care fac lucruri diferite, dar care par să facă aceste lucruri diferite într-un mod care are o similitudine de bază. Cum pot să descriu această similitudine între ele, chiar dacă clasele în sine nu sunt subtipuri ale nimic mai apropiat decât clasa „Obiect” în sine?”.

Mai exact, în cazul în care Monad „interfața” este similară cu IEnumerator sau IIterator în sensul că ia un tip care la rândul său ia un tip. Principalul „punct” al Monad este însă posibilitatea de a conecta operațiile bazate pe tipul interior, chiar până la punctul de a avea un nou „tip intern”, păstrând – sau chiar îmbunătățind – structura informațională a clasei principale.

Comentarii

  • return nu ar fi, de fapt, o metodă a monadei, deoarece nu acceptă ca argument o instanță de monadă. (adică: nu există „this/self”) –  > Por Laurence Gonsalves.
  • @LaurenceGonsalves: Din moment ce în prezent analizez acest lucru pentru teza mea de licență, cred că ceea ce este cel mai mult limitativ este lipsa metodelor statice în interfețele din C#/Java. Ați putea ajunge departe în direcția implementării întregii povești cu monade, cel puțin legate static în loc să se bazeze pe clasele tipice. Interesant este că acest lucru ar funcționa chiar și în ciuda lipsei tipurilor de tip superior. –  > Por Sebastian Graf.
VonC

Aveți o prezentare recentă „Monadologie — ajutor profesional privind anxietatea de tip” realizată de Christopher League (12 iulie 2010), care este destul de interesantă pe teme de continuare și monadă.
Videoclipul care însoțește această prezentare (slideshare) este de fapt disponibil la vimeo.
Partea referitoare la monade începe în jurul a 37 de minute, în acest videoclip de o oră, și începe cu diapozitivul 42 din prezentarea sa de 58 de diapozitive.

Acesta este prezentat ca fiind „modelul de proiectare principal pentru programarea funcțională”, dar limbajul utilizat în exemple este Scala, care este atât OOP, cât și funcțional.
Puteți citi mai multe despre Monad în Scala în articolul de pe blog „Monadele – o altă modalitate de a abstractiza calculele în Scala„, din Debasish Ghosh (27 martie 2008).

Un tip constructor M este o monadă dacă acceptă aceste operații:

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

Deci, de exemplu (în Scala):

  • Option este o monadă
    def unit[A] (x: A): Option[A] = Some(x)

    def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] =
      m match {
       case None => None
       case Some(x) => f(x)
      }
  • List este o monadă
    def unit[A] (x: A): List[A] = List(x)

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

Monadele sunt foarte importante în Scala datorită sintaxei convenabile construite pentru a profita de structurile Monad:

for înțelegere în Scala:

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

este tradusă de compilator în:

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

Abstracțiunea cheie este flatMap, care leagă calculul prin înlănțuire.
Fiecare invocare a flatMap returnează același tip de structură de date (dar cu valoare diferită), care servește ca intrare pentru următoarea comandă din lanț.

În fragmentul de mai sus, flatMap primește ca intrare o închidere (SomeType) => List[AnotherType] și returnează un List[AnotherType]. Este important de reținut că toate flatMaps iau ca intrare același tip de închidere și returnează același tip ca ieșire.

Aceasta este ceea ce „leagă” firul de calcul – fiecare element al secvenței din compunerea for-comprehensiune trebuie să onoreze aceeași constrângere de tip.


Dacă se iau două operații (care pot eșua) și se trece rezultatul la a treia, de exemplu:

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

dar fără a profita de Monad, veți obține un cod OOP alambicat de tipul:

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
  }

în timp ce cu Monad, puteți lucra cu tipurile reale (Venue, User), așa cum funcționează toate operațiile, și să păstrați ascunse chestiunile de verificare a opțiunii, totul datorită hărților plane ale sintaxei for:

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

Partea de randament va fi executată numai dacă toate cele trei funcții au Some[X]; any None ar fi returnat direct către confirm.


Deci:

Monadele permit calculul ordonat în cadrul programării funcționale, ceea ce ne permite să modelăm succesiunea de acțiuni într-o formă bine structurată, oarecum ca un DSL.

Iar cea mai mare putere vine din capacitatea de a compune monade care servesc unor scopuri diferite, în abstracțiuni extensibile în cadrul unei aplicații.

Această secvențiere și înșiruire a acțiunilor de către o monadă este realizată de compilatorul limbajului care face transformarea prin magia închiderilor.


Apropo, monada nu este singurul model de calcul utilizat în FP:

Teoria categoriilor propune numeroase modele de calcul. Printre acestea se numără

  • modelul Arrow al calculelor
  • modelul Monad al calculelor
  • modelul aplicativ al calculelor

Comentarii

  • Îmi place această explicație! Exemplul pe care l-ați dat demonstrează frumos conceptul și, de asemenea, adaugă ceea ce, în opinia mea, lipsea din teaserul lui Eric despre SelectMany() ca fiind o monadă. Mulțumesc pentru acest lucru! –  > Por aoven.
  • IMHO acesta este cel mai elegant răspuns –  > Por Polimerază.
  • și, înainte de orice altceva, Functor. –  > Por Will Ness.
Dmitri Zaitsev

Pentru a respecta cititorii rapizi, încep mai întâi cu o definiție precisă,continui cu o explicație rapidă mai „plain english” și apoi trec la exemple.

Iată o definiție atât concisă, cât și precisă ușor reformulată:

A monadă (în informatică) este în mod formal o hartă care:

  • trimite fiecare tip X dintr-un anumit limbaj de programare la un nou tip T(X) (numit „tip de T-computări cu valori în X„);

  • echipat cu o regulă de compunere a două funcții de forma f:X->T(Y) și g:Y->T(Z) la o funcție g∘f:X->T(Z);

  • într-un mod care este asociativ în sens evident și unital în raport cu o funcție unitate dată numită pure_X:X->T(X), care poate fi considerată ca fiind o funcție care duce o valoare la calculul pur care returnează pur și simplu această valoare.

Deci, în cuvinte simple, un monadă este o regulă de trecere de la orice tip X la un alt tip T(X), iar o regulă de trecere de la două funcții f:X->T(Y) și g:Y->T(Z) (pe care ați dori să le compuneți, dar nu puteți) către o nouă funcție h:X->T(Z). Care, însă, nu este, nu este compoziția în sens strict matematic. Practic, „îndoim” compoziția funcției sau redefinim modul în care se compun funcțiile.

În plus, avem nevoie ca regula de compunere a monadei să satisfacă axiomele matematice „evidente”:

  • Asociativitate: Compunere f cu g și apoi cu h (din exterior) ar trebui să fie aceeași cu compunerea g cu h și apoi cu f (din interior).
  • Proprietatea unitală: Compunerea f cu identitate de o parte și de alta ar trebui să dea f.

Din nou, cu alte cuvinte, nu putem să ne înnebunim și să redefinim compoziția funcțiilor după bunul plac:

  • În primul rând, avem nevoie de asociativitate pentru a putea compune mai multe funcții la rând, de ex. f(g(h(k(x))), și să nu ne facem griji cu privire la specificarea ordinii de compunere a perechilor de funcții. Deoarece regula monadei prescrie doar modul de compunere a unei funcții pereche de funcții, fără această axiomă, ar trebui să știm ce pereche este compusă mai întâi și așa mai departe. (Rețineți că este diferită de proprietatea de comutativitate care f compusă cu g ar fi aceeași cu g compusă cu f, care nu este necesară).
  • Și în al doilea rând, avem nevoie de proprietatea unitală, care este pur și simplu pentru a spune că identitățile se compun trivial așa cum ne așteptăm. Astfel, putem refactoriza în siguranță funcțiile ori de câte ori aceste identități pot fi extrase.

Deci, din nou, pe scurt: O monadă este regula de extindere a tipurilor și de compunere a funcțiilor care satisface cele două axiome – asociativitatea și proprietatea unitală.

În termeni practici, doriți ca monada să fie implementată pentru dvs. de către limbajul, compilatorul sau cadrul care se ocupă de compunerea funcțiilor pentru dvs. Astfel, vă puteți concentra asupra scrierii logicii funcțiilor dumneavoastră, mai degrabă decât să vă faceți griji cu privire la modul în care este implementată execuția acestora.

Aceasta este, în esență, pe scurt.


Fiind matematician de profesie, prefer să evit apelarea funcțiilor h „compoziția” de f și g. Pentru că, din punct de vedere matematic, nu este așa. A o numi „compoziție” presupune în mod incorect că h este adevărata compoziție matematică, ceea ce nu este. Nici măcar nu este determinată în mod unic de f și g. În schimb, este rezultatul noii „reguli de compunere” a funcțiilor din monada noastră. Care poate fi total diferită de adevărata compoziție matematică, chiar dacă aceasta din urmă există!


Pentru a fi mai puțin aridă, permiteți-mi să încerc să o ilustrez printr-un exemplu pe care îl voi adnota cu mici secțiuni, astfel încât să puteți trece direct la subiect.

Aruncarea excepțiilor ca exemple de monade

Să presupunem că dorim să compunem două funcții:

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

Dar f(0) nu este definită, astfel încât o excepție e este aruncată o excepție. Atunci cum se poate defini valoarea de compunere g(f(0))? Aruncând din nou o excepție, bineînțeles! Poate că aceeași e. Poate o nouă excepție actualizată e1.

Ce se întâmplă mai exact aici? În primul rând, avem nevoie de noi valori de excepție (diferite sau identice). Puteți să le numiți nothing sau null sau orice altceva, dar esența rămâne aceeași – ar trebui să fie valori noi, de exemplu, nu ar trebui să fie un number în exemplul nostru de aici. Eu prefer să nu le numesc null pentru a evita confuzia cu modul în care null poate fi implementat în orice limbaj specific. În egală măsură, prefer să evit nothing deoarece este adesea asociat cu null, care, în principiu, este ceea ce null ar trebui să facă, însă acest principiu este adesea încălcat din orice motive practice.

Ce este mai exact excepția?

Aceasta este o chestiune banală pentru orice programator experimentat, dar aș dori să las câteva cuvinte doar pentru a stinge orice vierme de confuzie:

Excepția este un obiect care conține informații despre modul în care s-a produs rezultatul invalid al execuției.

Acest lucru poate varia de la aruncarea oricăror detalii și returnarea unei singure valori globale (cum ar fi NaN sau null) sau generând o listă lungă de jurnal sau ce s-a întâmplat exact, trimițând-o la o bază de date și replicând-o pe tot stratul de stocare distribuită a datelor 😉

Diferența importantă între aceste două exemple extreme de excepție este că în primul caz există nu există efecte secundare. În cel de-al doilea există. Ceea ce ne aduce la întrebarea (de o mie de dolari):

Sunt permise excepțiile în funcțiile pure?

Un răspuns mai scurt: Da, dar numai atunci când nu duc la efecte secundare.

Răspunsul mai lung. Pentru a fi pură, ieșirea funcției dvs. trebuie să fie determinată în mod unic de intrarea sa. Deci, modificăm funcția noastră f trimițând 0 la noua valoare abstractă e pe care o numim excepție. Ne asigurăm că valoarea e nu conține nicio informație exterioară care să nu fie determinată în mod unic de intrarea noastră, care este x. Iată deci un exemplu de excepție fără efect secundar:

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

Și iată unul cu efect secundar:

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

De fapt, are efecte secundare doar dacă mesajul se poate schimba în viitor. Dar dacă este garantat că nu se va schimba niciodată, valoarea respectivă devine unic predictibilă și, prin urmare, nu există niciun efect secundar.

Pentru a face lucrurile și mai prostești. O funcție care returnează 42 ever este în mod clar pură. Dar dacă cineva nebun decide să facă 42 o variabilă a cărei valoare s-ar putea schimba, aceeași funcție încetează să mai fie pură în noile condiții.

Rețineți că folosesc notarea literală a obiectului pentru simplificare, pentru a demonstra esența. Din păcate, lucrurile sunt încurcate în limbaje precum JavaScript, unde error nu este un tip care se comportă așa cum dorim aici în ceea ce privește compunerea funcțiilor, în timp ce tipurile reale precum null sau NaN nu se comportă în acest fel, ci mai degrabă trec prin niște conversii de tip artificiale și nu întotdeauna intuitive.

Extinderea tipurilor

Deoarece dorim să modificăm mesajul din interiorul excepției noastre, declarăm de fapt un nou tip E pentru întregul obiect de excepție, iar apoi, în acest fel, se creează un nou tip pentru întregul obiect de excepție. maybe number face, în afară de numele său confuz, care trebuie să fie fie fie de tip number fie de noul tip de excepție Edeci este de fapt o uniune number | E de number și E. În special, depinde de modul în care dorim să construim E, ceea ce nu este sugerat și nici nu se reflectă în denumirea maybe number.

Ce este compoziția funcțională?

Este operația matematică prin care se iau funcțiif: X -> Y și g: Y -> Z și construirea compunerii lor ca funcție h: X -> Z care satisface h(x) = g(f(x)).Problema cu această definiție apare atunci când rezultatul f(x) nu este permis ca argument al g.

În matematică, aceste funcții nu pot fi compuse fără muncă suplimentară.Soluția strict matematică pentru exemplul nostru de mai sus de f și g este de a elimina 0 din setul de definiții ale lui f. Cu acest nou set de definiții (noul tip mai restrictiv de x), f devine compatibil cu g.

Cu toate acestea, nu este foarte practic în programare să restricționăm setul de definiții ale lui f așa. În schimb, se pot folosi excepții.

Sau, ca o altă abordare, se creează valori artificiale de tipul NaN, undefined, null, Infinity etc. Astfel, se evaluează 1/0 la Infinity și 1/-0 la -Infinity. Și apoi forțați noua valoare înapoi în expresie în loc să aruncați o excepție. Ceea ce duce la rezultate pe care le puteți considera sau nu previzibile:

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

Și ne-am întors la numere obișnuite, gata să trecem mai departe 😉

JavaScript ne permite să continuăm să executăm expresii numerice cu orice preț fără a arunca erori ca în exemplul de mai sus. Asta înseamnă că permite, de asemenea, să compunem funcții. Ceea ce este exact ceea ce reprezintă monada – este o regulă de compunere a funcțiilor care satisfac axiomele definite la începutul acestui răspuns.

Dar este regula de compunere a funcțiilor, care rezultă din implementarea JavaScript pentru tratarea erorilor numerice, o monadă?

Pentru a răspunde la această întrebare, tot ce trebuie să faceți este să verificați axiomele (lăsate ca exercițiu, deoarece nu fac parte din întrebarea de aici;).

Poate fi folosită aruncarea unei excepții pentru a construi o monadă?

Într-adevăr, o monadă mai utilă ar fi în schimb regula care prescrie că dacă f aruncă o excepție pentru un anumit x, la fel și compunerea sa cu orice g. În plus, face ca excepția E unică la nivel global, cu o singură valoare posibilă vreodată (obiect terminal în teoria categoriilor). Acum, cele două axiome sunt verificabile instantaneu și obținem o monadă foarte utilă. Iar rezultatul este ceea ce este bine cunoscut sub numele de monada poate.

Comentarii

  • O contribuție bună. +1 Dar poate ai vrea să ștergi ” au găsit majoritatea explicațiilor prea lungi …” fiind a ta cea mai lungă deloc. Alții vor judeca dacă este „plain english” așa cum cere întrebarea: „plain english == în cuvinte simple, într-un mod simplu”. –  > Por cibercitizen1.
  • @cibercitizen1 Mulțumesc! De fapt este scurt, dacă nu punem la socoteală exemplul. Ideea principală este că nu trebuie să citiți exemplul pentru a înțelege definiția. Din păcate, multe explicații mă obligă să citesc mai întâi exemplele, ceea ce de multe ori nu este necesar, dar, desigur, poate necesita o muncă suplimentară pentru autor. Dacă ne bazăm prea mult pe exemple specifice, există pericolul ca detaliile neimportante să întunece imaginea și să o facă mai greu de înțeles. Acestea fiind spuse, aveți puncte de vedere valabile, vedeți actualizarea. –  > Por Dmitri Zaitsev.
  • prea lung și confuz –  > Por seenimurugan.
  • @seenimurugan Sugestiile de îmbunătățire sunt binevenite 😉 –  > Por Dmitri Zaitsev.
  • Acest lucru este foarte util. Am citit multe explicații despre monade, iar aceasta este una dintre cele mai clare. Apreciez efortul pe care l-ați depus pentru a pune o definiție precisă și concisă în partea de sus. Dar aveam nevoie și de explicația lungă 🙂 –  > Por LarsH.
Chuck

O monadă este un tip de date care înglobează o valoare și căreia, în esență, i se pot aplica două operații:

  • return x creează o valoare a tipului de monadă care încapsulează x
  • m >>= f (citiți-o ca „operatorul de legare”) aplică funcția f la valoarea din monadă m

Iată ce este o monadă. Există câteva aspecte tehnice suplimentare, dar, în principiu, aceste două operații definesc o monadă. Adevărata întrebare este: „Ce este o monadă face?”, iar acest lucru depinde de monadă – listele sunt monade, Maybes sunt monade, operațiile IO sunt monade. Tot ceea ce înseamnă atunci când spunem că aceste lucruri sunt monade este că au interfața de monadă a return și >>=.

user8554766

Comentarii

  • „ceea ce face o monadă, iar asta depinde de monadă”: și mai precis, asta depinde de bind funcția care trebuie definită pentru fiecare tip monadic, nu-i așa? Acesta ar fi un motiv bun pentru a nu confunda bind cu composition, deoarece există o singură definiție pentru composition, în timp ce nu poate exista o singură definiție pentru funcția bind, ci una pentru fiecare tip monadic, dacă am înțeles bine. –  > Por Hibou57.
the_drow

Adus de la wikipedia:

În programarea funcțională, o monadă este un fel de tip de date abstracte utilizate pentru a reprezenta calculele (în loc de date în modelul de domeniu). Monadele permit programatorului să înlănțuiască acțiuni pentru a construi un pipeline, în care fiecare acțiune este decorată cu reguli de procesare suplimentare furnizate de monadă. Programele scrise în stil funcțional pot utiliza monadele pentru a structura proceduri care includ operații secvențiale,12] sau pentru a defini fluxuri de control arbitrare (cum ar fi gestionarea simultaneității, a continuărilor sau a excepțiilor).

În mod formal, o monadă este construită prin definirea a două operații (bind și return) și a unui constructor de tip M care trebuie să îndeplinească mai multe proprietăți pentru a permite compunerea corectă a funcțiilor monadice (adică a funcțiilor care utilizează valori din monadă ca argumente). Operația return preia o valoare dintr-un tip simplu și o introduce într-un container monadic de tip M. Operația bind efectuează procesul invers, extrăgând valoarea originală din container și transmițând-o următoarei funcții asociate din pipeline.

Un programator va compune funcții monadice pentru a defini o conductă de procesare a datelor. Monada acționează ca un cadru, deoarece este un comportament reutilizabil care decide ordinea în care sunt apelate funcțiile monadice specifice din pipeline și gestionează toată munca sub acoperire necesară calculului[3] Operatorii bind și return intercalați în pipeline vor fi executați după ce fiecare funcție monadică returnează controlul și se vor ocupa de aspectele particulare gestionate de monadă.

Cred că se explică foarte bine.

Voi încerca să fac cea mai scurtă definiție pe care o pot gestiona folosind termeni OOP:

O clasă generică CMonadic<T> este o monadă dacă definește cel puțin următoarele metode:

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
}

și dacă se aplică următoarele legi pentru toate tipurile T și valorile lor posibile t

identitate stânga:

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

identitate dreapta

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

asociativitate:

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

Exemple:

O monadă List poate avea:

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

Și flatMap pe lista [1,2,3] ar putea funcționa astfel:

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

Iterables și Observables pot fi, de asemenea, transformate în monade, precum și Promises și Tasks.

Comentariu:

Monadele nu sunt atât de complicate. Site-ul flatMap seamănă foarte mult cu funcția mai des întâlnită map. Ea primește ca argument o funcție (cunoscută și sub numele de delegat), pe care o poate apela (imediat sau mai târziu, de zero sau mai multe ori) cu o valoare provenită din clasa generică. Ea se așteaptă ca funcția transmisă să își înfășoare și valoarea de returnare în același tip de clasă generică. Pentru a ajuta în acest sens, oferă create, un constructor care poate crea o instanță a acelei clase generice dintr-o valoare. Rezultatul returnat de flatMap este, de asemenea, o clasă generică de același tip, care adesea împachetează aceleași valori care au fost conținute în rezultatele returnate de una sau mai multe aplicații ale flatMap la valorile conținute anterior. Acest lucru vă permite să înlănțuiți flatMap oricât de mult doriți:

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

Se întâmplă ca acest tip de clasă generică să fie util ca model de bază pentru un număr imens de lucruri. Acesta (împreună cu jargonismele din teoria categoriilor) este motivul pentru care Monadele par atât de greu de înțeles sau de explicat. Ele sunt un lucru foarte abstract și devin evident utile doar după ce sunt specializate.

De exemplu, puteți modela excepțiile folosind containere monadice. Fiecare container va conține fie rezultatul operației, fie eroarea care a avut loc. Următoarea funcție (delegat) din lanțul de apeluri flatMap va fi apelată numai dacă cea anterioară a introdus o valoare în container. În caz contrar, dacă s-a produs o eroare, eroarea va continua să se propage prin containerele înlănțuite până când se găsește un container la care este atașată o funcție de gestionare a erorilor prin intermediul unei metode numite .orElse() (o astfel de metodă ar fi o extensie permisă).

Note: Limbajele funcționale vă permit să scrieți funcții care pot opera asupra oricărui tip de clasă generică monadică. Pentru ca acest lucru să funcționeze, ar trebui să se scrie o interfață generică pentru monade. Nu știu dacă este posibil să se scrie o astfel de interfață în C#, dar din câte știu eu nu este:

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
}

nomen

Faptul că o monadă are o interpretare „naturală” în OO depinde de monadă. Într-un limbaj precum Java, puteți traduce monada maybe în limbajul de verificare a indicatoarelor nule, astfel încât calculele care eșuează (adică produc Nothing în Haskell) să emită indicatoare nule ca rezultate. Puteți traduce monada de stare în limbajul generat de crearea unei variabile mutabile și a unor metode de schimbare a stării acesteia.

O monadă este un monoid din categoria endofuncționalilor.

Informațiile pe care le reunește această propoziție sunt foarte profunde. Și se lucrează într-o monadă cu orice limbaj imperativ. O monadă este un limbaj specific domeniului „secvențial”. Acesta satisface anumite proprietăți interesante, care, luate împreună, fac dintr-o monadă un model matematic de „programare imperativă”. Haskell facilitează definirea unor limbaje imperative mici (sau mari), care pot fi combinate într-o varietate de moduri.

În calitate de programator OO, utilizați ierarhia de clase a limbajului dumneavoastră pentru a organiza tipurile de funcții sau proceduri care pot fi apelate într-un context, ceea ce numiți obiect. O monadă este, de asemenea, o abstractizare a acestei idei, în măsura în care diferite monade pot fi combinate în mod arbitrar, „importând” efectiv toate metodele submonadei în domeniul de aplicare.

Din punct de vedere arhitectural, se utilizează apoi semnăturile de tip pentru a exprima în mod explicit contextele care pot fi utilizate pentru calcularea unei valori.

În acest scop, se pot utiliza transformatoare de monade și există o colecție de înaltă calitate a tuturor monadelor „standard”:

  • Liste (calcule nedeterministe, prin tratarea unei liste ca domeniu)
  • Maybe (calcule care pot eșua, dar pentru care raportarea nu este importantă)
  • Eroare (calcule care pot eșua și care necesită tratarea excepțiilor).
  • Reader (calcule care pot fi reprezentate prin compuneri de funcții Haskell simple).
  • Writer (calcule cu „redare”/”înregistrare” secvențială (în șiruri de caractere, html etc.).
  • Cont (continuări)
  • IO (calcule care depind de sistemul informatic de bază)
  • State (calcule al căror context conține o valoare modificabilă)

cu transformatoarele de monade și clasele de tip corespunzătoare. Clasele de tip permit o abordare complementară a combinării monadelor prin unificarea interfețelor acestora, astfel încât monadele concrete să poată implementa o interfață standard pentru monada „tip”. De exemplu, modulul Control.Monad.State conține o clasă MonadState s m, iar (State s) este o instanță de forma

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

Pe scurt, o monadă este un functor care atașează „contextul” unei valori, care are o modalitate de a injecta o valoare în monadă și care are o modalitate de a evalua valorile în raport cu contextul atașat, cel puțin într-un mod restrâns.

Așadar:

return :: a -> m a

este o funcție care injectează o valoare de tip a într-o monadă „acțiune” de tip m a.

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

este o funcție care preia o acțiune de monadă, evaluează rezultatul acesteia și aplică o funcție rezultatului. Ceea ce este interesant la (>>=) este că rezultatul se află în aceeași monadă. Cu alte cuvinte, în m >>= f, (>>=) extrage rezultatul din m și îl leagă de f, astfel încât rezultatul se află în monadă. (Alternativ, putem spune că (>>=) trage f în m și o aplică rezultatului). În consecință, dacă avem f :: a -> m b, și g :: b -> m c, putem „secvenția” acțiunile:

m >>= f >>= g

Sau, folosind „notația do”

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

Tipul pentru (>>) ar putea fi lămuritor. Acesta este

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

corespunde operatorului (;) din limbajele procedurale, cum ar fi C. Permite notații de tip „do”, cum ar fi:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

În logica matematică și filozofică, avem cadre și modele, care sunt modelate „în mod natural” cu ajutorul monadismului. O interpretare este o funcție care se uită în domeniul modelului și calculează valoarea de adevăr (sau generalizări) a unei propoziții (sau formule, sub generalizări). Într-o logică modală pentru necesitate, am putea spune că o propoziție este necesară dacă este adevărată în „orice lume posibilă” – dacă este adevărată cu privire la orice domeniu admisibil. Aceasta înseamnă că un model într-un limbaj pentru o propoziție poate fi reificat ca un model al cărui domeniu constă într-o colecție de modele distincte (unul corespunzător fiecărei lumi posibile). Fiecare monadă are o metodă numită „join” care aplatizează straturile, ceea ce implică faptul că fiecare acțiune de monadă al cărei rezultat este o acțiune de monadă poate fi încorporată în monadă.

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

Mai important, aceasta înseamnă că monada este închisă în cadrul operației de „stivuire a straturilor”. Acesta este modul în care funcționează transformatoarele de monade: ele combină monadele prin furnizarea de metode de tip „join” pentru tipuri precum

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

astfel încât să putem transforma o acțiune din (MaybeT m) într-o acțiune din m, ceea ce înseamnă că, în mod eficient, se suprapun straturile. În acest caz, runMaybeT :: MaybeT m a -> m (Maybe a) este metoda noastră de tip join. (MaybeT m) este o monadă, iar MaybeT :: m (Maybe a) -> MaybeT m a este efectiv un constructor pentru un nou tip de acțiune de monadă în m.

O monadă liberă pentru un functor este monada generată de stivuirea lui f, cu implicația că fiecare secvență de constructori pentru f este un element al monadei libere (sau, mai exact, ceva cu aceeași formă ca arborele de secvențe de constructori pentru f). Monadele libere reprezintă o tehnică utilă pentru construirea de monade flexibile cu o cantitate minimă de elemente de bază. Într-un program Haskell, aș putea folosi monadele libere pentru a defini monade simple pentru „programarea sistemului de nivel înalt” pentru a ajuta la menținerea siguranței tipurilor (folosesc doar tipuri și declarațiile acestora. Implementările sunt simple cu ajutorul combinatorilor):

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

Monadismul este arhitectura care stă la baza a ceea ce s-ar putea numi modelul „interpretor” sau „comandă”, abstractizat în forma sa cea mai clară, deoarece fiecare calcul monadic trebuie să fie „executat”, cel puțin trivial. (Sistemul de execuție execută monada IO pentru noi și este punctul de intrare în orice program Haskell. IO „conduce” restul calculelor, prin rularea în ordine a acțiunilor IO).

Tipul pentru join este, de asemenea, locul de unde obținem afirmația că o monadă este un monoid în categoria endofuncțiilor. Join este de obicei mai important în scopuri teoretice, în virtutea tipului său. Dar înțelegerea tipului înseamnă înțelegerea monadelor. Join și tipurile asemănătoare join ale transformatorului de monade sunt, de fapt, compoziții de endofunctori, în sensul compoziției funcțiilor. Pentru a o exprima într-un pseudo-lingvism de tip Haskell,

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

David K. Hess

Monadele, în utilizarea tipică, sunt echivalentul funcțional al mecanismelor de tratare a excepțiilor din programarea procedurală.

În limbajele procedurale moderne, se plasează un gestionar de excepții în jurul unei secvențe de instrucțiuni, oricare dintre acestea putând arunca o excepție. Dacă una dintre instrucțiuni aruncă o excepție, execuția normală a secvenței de instrucțiuni se oprește și se transferă către un gestionar de excepții.

Cu toate acestea, limbajele de programare funcționale evită din punct de vedere filozofic funcțiile de tratare a excepțiilor din cauza naturii lor asemănătoare cu „goto”. Perspectiva programării funcționale este că funcțiile nu ar trebui să aibă „efecte secundare”, cum ar fi excepțiile, care să perturbe desfășurarea programului.

În realitate, efectele secundare nu pot fi excluse în lumea reală, în primul rând din cauza I/O. Monadele din programarea funcțională sunt utilizate pentru a gestiona acest lucru prin luarea unui set de apeluri de funcții înlănțuite (dintre care oricare ar putea produce un rezultat neașteptat) și transformarea oricărui rezultat neașteptat în date încapsulate care pot continua să circule în siguranță prin apelurile de funcții rămase.

Fluxul de control este păstrat, dar evenimentul neașteptat este încapsulat și tratat în siguranță.

cibercitizen1

O monadă este o matrice de funcții

(Pst: o matrice de funcții este doar un calcul).

De fapt, în loc de un adevărat array (o funcție într-un array de celule), aveți acele funcții înlănțuite de o altă funcție >>=. >>= permite să adaptați rezultatele din funcția i pentru a alimenta funcția i+1, să efectuați calcule între ele sau, chiar, să nu apelați funcția i+1.

Tipurile utilizate aici sunt „tipuri cu context”. Adică, o valoare cu o „etichetă”. funcțiile care se înlănțuie trebuie să ia o „valoare goală” și să returneze un rezultat etichetat. una dintre sarcinile lui >>= este de a extrage o valoare goală din contextul său. există și funcția „return”, care ia o valoare goală și o pune cu o etichetă.

Un exemplu cu Maybe. Să-l folosim pentru a stoca un număr întreg simplu pe care să facem calcule.

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

Doar pentru a arăta că monadele sunt matrice de funcții cu operații auxiliare, considerați echivalentul exemplului de mai sus, doar că se folosește o matrice reală de funcții

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

Și ar fi folosit astfel:

print (runMyMonad (Just 160) myArray1)

Comentarii

  • Super-neat! Așadar, bind este doar o modalitate de a evalua un array de funcții cu context, în succesiune, pe o intrare cu context 🙂 –  > Por Musa Al-hassy.
  • >>= este un operator –  > Por user2418306.
  • Cred că analogia cu „matricea de funcții” nu clarifică prea multe. Dacă x -> x >>= k >>= l >>= m este o matrice de funcții, la fel este și h . g . f, care nu implică deloc monade. –  > Por duplode.
  • am putea spune că functori, fie că sunt monadici, aplicativi sau simpli, se referă la „aplicații înfrumusețate”. „aplicativ” adaugă înlănțuirea, iar „monada” adaugă dependența (adică crearea următoarei etape de calcul în funcție de rezultatele unei etape de calcul anterioare). –  > Por Will Ness.
sea-rob

În termeni OO, o monadă este un container fluent.

Cerința minimă este o definiție a class <A> Something care să suporte un constructor Something(A a) și cel puțin o metodă Something<B> flatMap(Function<A, Something<B>>)

Se poate spune că se pune la socoteală și dacă clasa monadei tale are metode cu semnătura Something<B> work() care păstrează regulile clasei – compilatorul integrează flatMap la compilare.

De ce este utilă o monadă? Pentru că este un container care permite operații înlănțuite care păstrează semantica. De exemplu, Optional<?> păstrează semantica lui isPresent pentru Optional<String>, Optional<Integer>, Optional<MyClass>, etc.

Ca un exemplu aproximativ,

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

Rețineți că începem cu un șir de caractere și terminăm cu un număr întreg. Destul de mișto.

În OO, s-ar putea să fie nevoie de puțină manipulare, dar orice metodă asupra lui Something care returnează o altă subclasă a lui Something îndeplinește criteriul unei funcții container care returnează un container de tip original.

Așa se păstrează semantica – adică sensul și operațiile containerului nu se schimbă, ci doar înfășoară și îmbunătățesc obiectul din container.

Gulshan

Vă împărtășesc înțelegerea mea despre Monads, care s-ar putea să nu fie perfectă din punct de vedere teoretic. Monadele se referă la propagarea contextului. Monad este, definiți un anumit context pentru anumite date (sau tipuri de date) și apoi definiți modul în care acest context va fi transportat cu datele pe parcursul procesului de procesare. Iar definirea propagării contextului se referă în principal la definirea modului de fuzionare a contextelor multiple (de același tip). Utilizarea Monadelor înseamnă, de asemenea, asigurarea faptului că aceste contexte nu sunt eliminate accidental din date. Pe de altă parte, alte date fără context pot fi aduse într-un context nou sau existent. Apoi, acest concept simplu poate fi utilizat pentru a asigura corectitudinea în timp de compilare a unui program.

Comentarii

  • Ei bine, în Haskell există funcții de primă clasă, iar prin utilizarea lor se obțin bucle de primă clasă (map, filter, fold, zipWith) și condiționale de primă clasă (nu sunt utile*). Ei bine, monadele sunt punct și virgulă de primă clasă, adică calcule în trepte în care valoarea calculelor depinde de valoarea pasului anterior. Monadele sunt, de asemenea, declarații de primă clasă & semantică & limbaje de declarații. –  > Por aoeu256.
Richard Berg

Dacă ați folosit vreodată Powershell, modelele descrise de Eric ar trebui să vă sune familiar. cmdlets Powershell sunt monade; compoziția funcțională este reprezentată de o conductă.

Interviul lui Jeffrey Snover cu Erik Meijer intră în mai multe detalii.

Ira

O explicație simplă despre Monads cu un studiu de caz Marvel este aici.

Monadele sunt abstracțiuni folosite pentru a secvenția funcțiile dependente care au efect. Eficace aici înseamnă că acestea returnează un tip de forma F[A], de exemplu Option[A] unde Option este F, numit constructor de tip. Să vedem acest lucru în 2 pași simpli

  1. Mai jos Compoziția funcțiilor este tranzitivă. Deci, pentru a trece de la A la C, pot compune A => B și B => C.
 A => C   =   A => B  andThen  B => C

  1. Cu toate acestea, dacă funcția returnează un tip de efect cum ar fi Option[A], adică A => F[B], compoziția nu funcționează, deoarece pentru a merge la B avem nevoie de A => B, dar avem A => F[B].

    Avem nevoie de un operator special, „bind”, care știe cum să fuzioneze aceste funcții care returnează F[A].

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

„bind” este definit pentru funcția specifică F.

Există, de asemenea, o funcție „return”, de tip A => F[A] pentru orice A, definit pentru acel specific F de asemenea. Pentru a fi o monadă, F trebuie să aibă aceste două funcții definite pentru ea.

Astfel, putem construi o funcție eficace A => F[B] din orice funcție pură A => B,

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

dar o anumită F își poate defini, de asemenea, propriile funcții speciale opace „încorporate” de astfel de tipuri pe care un utilizator nu le poate defini singur (într-o pur un limbaj pur), cum ar fi

  • „random” (Range => Random[Int])
  • „print” (String => IO[ () ])
  • „try … catch”, etc.

Jordan

Consultați răspunsul meu la întrebarea „Ce este o monadă?”.

Acesta începe cu un exemplu motivant, lucrează prin intermediul exemplului, derivă un exemplu de monadă și definește în mod formal „monada”.

Nu presupune cunoștințe de programare funcțională și folosește pseudocod cu function(argument) := expression sintaxă cu cele mai simple expresii posibile.

Acest program C++ este o implementare a pseudocodului monadei. (Pentru referință: M este constructorul de tip, feed este operația „bind” și wrap este operația „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.
";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.
";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}

novis

Din punct de vedere practic (rezumând ceea ce s-a spus în multe răspunsuri anterioare și articole conexe), mi se pare că unul dintre „scopurile” (sau utilitatea) fundamentale ale monadei este acela de a valorifica dependențele implicite în invocările recursive ale metodelor aka compoziția de funcții (de ex. atunci când f1 apelează f2 apelează f3, f3 trebuie să fie evaluată înainte de f2 înainte de f1) pentru a reprezenta compoziția secvențială într-un mod natural, în special în contextul unui model de evaluare leneș (adică, compoziția secvențială ca o secvență simplă, de exemplu „f3(); f2(); f1();” în C – trucul este deosebit de evident dacă vă gândiți la un caz în care f3, f2 și f1 nu returnează de fapt nimic [înlănțuirea lor ca f1(f2(f3)) este artificială, având ca scop pur și simplu crearea unei secvențe]).

Acest lucru este relevant mai ales atunci când sunt implicate efecte secundare, adică atunci când o anumită stare este modificată (dacă f1, f2, f3 nu ar avea efecte secundare, nu ar conta în ce ordine sunt evaluate; ceea ce reprezintă o proprietate excelentă a limbajelor pur funcționale, pentru a putea paraleliza aceste calcule, de exemplu). Cu cât mai multe funcții pure, cu atât mai bine.

Cred că, din acest punct de vedere restrâns, monadele ar putea fi văzute ca un zahăr sintactic pentru limbajele care favorizează evaluarea leneșă (care evaluează lucrurile doar atunci când este absolut necesar, urmând o ordine care nu se bazează pe prezentarea codului) și care nu au alte mijloace de a reprezenta compoziția secvențială. Rezultatul net este că secțiunile de cod care sunt „impure” (adică care au efecte secundare) pot fi prezentate în mod natural, într-o manieră imperativă, dar sunt separate în mod clar de funcțiile pure (fără efecte secundare), care pot fi evaluate leneș.

Acesta este însă doar un aspect, după cum a avertizat aici.

RedPoppy

Cea mai simplă explicație la care mă pot gândi este că monadele sunt o modalitate de a compune funcții cu rezultate înfrumusețate (aka compoziția Kleisli). O funcție „înfrumusețată” are semnătura a -> (b, smth) unde a și b sunt tipuri (gândiți-vă la Int, Bool) care pot fi diferite unul de celălalt, dar nu neapărat – și smth este „contextul” sau „înfrumusețarea”.

Acest tip de funcții poate fi scris și în felul următor a -> m b unde m este echivalentă cu „înfrumusețarea” smth. Așadar, acestea sunt funcții care returnează valori în context (gândiți-vă la funcțiile care își înregistrează acțiunile, unde smth este mesajul de logare; sau funcții care efectuează intrări/ieșiri, iar rezultatele lor depind de rezultatul acțiunii IO).

O monadă este o interfață („typeclass”) care îl face pe implementator să îi spună cum să compună astfel de funcții. Implementatorul trebuie să definească o funcție de compunere (a -> m b) -> (b -> m c) -> (a -> m c) pentru orice tip m care dorește să implementeze interfața (aceasta este compoziția Kleisli).

Așadar, dacă spunem că avem un tip de tuple (Int, String) care reprezintă rezultatele calculelor pe Ints care înregistrează, de asemenea, acțiunile lor, cu (_, String) fiind „înfrumusețarea” – jurnalul acțiunii – și două funcții increment :: Int -> (Int, String) și twoTimes :: Int -> (Int, String) dorim să obținem o funcție incrementThenDouble :: Int -> (Int, String) care este compoziția celor două funcții care ia în considerare și logurile.

Pe exemplul dat, o implementare a monadei celor două funcții se aplică la valoarea întreagă 2 incrementThenDouble 2 (care este egală cu twoTimes (increment 2)) ar returna (6, " Adding 1. Doubling 3.") pentru rezultatele intermediare increment 2 egal cu (3, " Adding 1.") și twoTimes 3 egal cu (6, " Doubling 3.")

Din această funcție de compoziție Kleisli se pot deriva funcțiile monadice obișnuite.