Monada zwykłym angielskim? (Dla programisty OOP bez tła FP)


743

W kategoriach zrozumiałych dla programisty OOP (bez żadnego funkcjonalnego tła programowania), czym jest monada?

Jaki problem rozwiązuje i jakie są najczęściej używane miejsca?

EDYTOWAĆ:

Aby wyjaśnić rodzaj zrozumienia, którego szukałem, załóżmy, że konwertujesz aplikację FP, która miała monady, na aplikację OOP. Co byś zrobił, aby przenieść obowiązki monad na aplikację OOP?




10
@Pavel: Odpowiedź, którą otrzymaliśmy poniżej od Erica, jest znacznie lepsza niż w innych sugerowanych pytaniach dla osób z doświadczeniem OO (w przeciwieństwie do tła FP).
Donal Fellows

5
@Donal: Jeśli jest to duplikat (o którym nie mam zdania), dobrą odpowiedź należy dodać do oryginału. To znaczy: dobra odpowiedź nie wyklucza zamknięcia jako duplikatu. Jeśli jest to wystarczająco blisko duplikat, moderator może to zrobić w drodze scalenia.
dmckee --- były moderator kociak

Odpowiedzi:


732

AKTUALIZACJA: To pytanie było przedmiotem niezwykle długiej serii blogów, którą można przeczytać w Monadach - dzięki za świetne pytanie!

W kategoriach zrozumiałych dla programisty OOP (bez żadnego funkcjonalnego tła programowania), czym jest monada?

Monada to „wzmacniacz” typów, który przestrzega pewnych zasad i ma określone operacje .

Po pierwsze, czym jest „wzmacniacz typów”? Rozumiem przez to jakiś system, który pozwala ci wziąć typ i zmienić go w bardziej specjalny typ. Na przykład w C # rozważyć Nullable<T>. To jest wzmacniacz typów. Pozwala wybrać typ, powiedzmy int, i dodać do niego nową funkcję, a mianowicie, że teraz może być zerowy, gdy wcześniej nie mógł.

Jako drugi przykład rozważ IEnumerable<T>. To wzmacniacz typów. Pozwala wziąć typ, powiedzmy, stringi dodać do niego nową funkcję, mianowicie, że możesz teraz tworzyć sekwencję ciągów z dowolnej liczby pojedynczych ciągów.

Jakie są „pewne zasady”? W skrócie, istnieje rozsądny sposób, aby funkcje typu bazowego działały na typie wzmocnionym, tak aby działały zgodnie z normalnymi zasadami składu funkcjonalnego. Na przykład, jeśli masz funkcję na liczbach całkowitych, powiedzmy

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

wtedy odpowiednia funkcja włączona Nullable<int>może sprawić, że wszyscy operatorzy i wywołania będą działać razem „w ten sam sposób”, co wcześniej.

(Jest to niezwykle niejasne i nieprecyzyjne; poprosiłeś o wyjaśnienie, które nie zakładało niczego o znajomości składu funkcjonalnego).

Jakie są „operacje”?

  1. Istnieje operacja „jednostkowa” (myląco nazywana czasami operacją „powrotu”), która pobiera wartość z typu zwykłego i tworzy równoważną wartość monadyczną. To w istocie zapewnia sposób na przyjęcie wartości typu nieamplifikowanego i przekształcenie jej w wartość typu amplifikowanego. Może być zaimplementowany jako konstruktor w języku OO.

  2. Istnieje operacja „powiązania”, która przyjmuje wartość monadyczną i funkcję, która może przekształcić tę wartość i zwraca nową wartość monadyczną. Powiązanie jest kluczową operacją, która definiuje semantykę monady. Pozwala nam przekształcać operacje na typie nieamplifikowanym na operacje na typie amplifikowanym, który przestrzega wspomnianych wcześniej zasad składu funkcjonalnego.

  3. Często istnieje sposób na odzyskanie niezamkniętego typu ze wzmacnianego typu. Ściśle mówiąc, ta operacja nie musi mieć monady. (Jest to konieczne, jeśli chcesz mieć comonad . Nie będziemy rozważać tych w dalszej części tego artykułu).

Ponownie weźmy Nullable<T>za przykład. Możesz zmienić an intw Nullable<int>konstruktora. Kompilator C # zajmuje się najbardziej zerowym „podnoszeniem” dla Ciebie, ale jeśli nie, transformacja podnoszenia jest prosta: operacja, powiedzmy,

int M(int x) { whatever }

przekształca się w

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

I obracając się Nullable<int>z powrotem do intodbywa się przy Valueobiekcie.

Kluczem jest transformacja funkcji. Zwróć uwagę, jak rzeczywista semantyka operacji zerowalnej - którą nullpropaguje operacja null- jest przechwytywana w transformacji. Możemy to uogólnić.

Załóżmy, że masz funkcję od intdo int, jak nasz oryginał M. Możesz łatwo przekształcić to w funkcję, która pobiera inti zwraca a, Nullable<int>ponieważ możesz po prostu uruchomić wynik przez konstruktor zerowalny. Załóżmy teraz, że masz tę metodę wyższego rzędu:

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

Widzisz, co możesz z tym zrobić? Każda metoda, która przyjmuje inti zwraca an int, lub przyjmuje inti zwraca a, Nullable<int>może teraz mieć przypisaną do niej semantykę zerowalną .

Ponadto: załóżmy, że masz dwie metody

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

i chcesz je skomponować:

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

To jest, Zskład Xi Y. Ale nie możesz tego zrobić, ponieważ Xbierze inti Yzwraca a Nullable<int>. Ale ponieważ masz operację „powiązania”, możesz sprawić, aby działała:

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

Operacja wiązania na monadzie sprawia, że ​​kompozycja funkcji na wzmacnianych typach działa. „Reguły”, o których wspominałem powyżej, to to, że monada zachowuje reguły normalnego składu funkcji; że komponowanie z funkcjami tożsamości skutkuje funkcją oryginalną, ta kompozycja jest skojarzona i tak dalej.

W języku C # „Bind” nazywa się „SelectMany”. Zobacz, jak to działa na monadzie sekwencji. Potrzebujemy dwóch rzeczy: przekształcić wartość w sekwencję i powiązać operacje na sekwencjach. Jako bonus mamy również „zmienić sekwencję z powrotem w wartość”. Te operacje to:

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

Regułą zerowalnej monady było: „połączyć dwie funkcje, które razem wytwarzają wartości zerowe, sprawdź, czy wewnętrzna daje null; jeśli tak się stanie, wyzeruj, jeśli nie, to wywołaj zewnętrzną z wynikiem”. To pożądana semantyka zerowalności.

Reguła monady sekwencji to „połączyć dwie funkcje, które tworzą sekwencje razem, zastosować funkcję zewnętrzną do każdego elementu wytworzonego przez funkcję wewnętrzną, a następnie połączyć wszystkie powstałe sekwencje razem”. Podstawowa semantyka monad jest ujęta w metodach Bind/ SelectMany; jest to metoda, która mówi ci, co tak naprawdę oznacza monada .

Możemy zrobić jeszcze lepiej. Załóżmy, że masz sekwencje liczb całkowitych i metodę, która przyjmuje liczby całkowite i daje w wyniku ciągi znaków. Możemy uogólnić operację wiązania, aby umożliwić zestawienie funkcji, które pobierają i zwracają różne typy wzmocnione, o ile dane wejściowe jednego pasują do wyników drugiego:

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

Teraz możemy powiedzieć: „amplifikuj tę wiązkę pojedynczych liczb całkowitych w sekwencję liczb całkowitych. Przekształć tę konkretną liczbę całkowitą w wiązkę ciągów, wzmocnioną w sekwencję ciągów. Teraz połącz obie operacje razem: wzmocnij tę wiązkę liczb całkowitych w konkatenacji wszystkie sekwencje ciągów znaków. ” Monady pozwalają komponować swoje wzmocnienia.

Jaki problem rozwiązuje i jakie są najczęściej używane miejsca?

To raczej jak pytanie „jakie problemy rozwiązuje wzór singletonu?”, Ale spróbuję.

Monady są zwykle używane do rozwiązywania problemów, takich jak:

  • Muszę stworzyć nowe możliwości dla tego typu i nadal łączyć stare funkcje tego typu, aby móc korzystać z nowych możliwości.
  • Muszę uchwycić kilka operacji na typach i przedstawić je jako obiekty do złożenia, budując coraz większe kompozycje, dopóki nie przedstawię odpowiedniej serii operacji, a następnie muszę zacząć uzyskiwać wyniki
  • Muszę czysto reprezentować działania niepożądane w języku, który nie znosi skutków ubocznych

C # używa monad w swojej konstrukcji. Jak już wspomniano, zerowalny wzór jest bardzo podobny do „może monady”. LINQ jest całkowicie zbudowany z monad; SelectManymetody jest to, co robi semantycznej pracę składzie operacji. (Erik Meijer lubi podkreślać, że każda funkcja LINQ mogłaby być faktycznie zaimplementowana SelectMany; wszystko inne to tylko wygoda).

Aby wyjaśnić rodzaj zrozumienia, którego szukałem, załóżmy, że konwertujesz aplikację FP, która miała monady, na aplikację OOP. Co byś zrobił, aby przenieść obowiązki monad w aplikacji OOP?

Większość języków OOP nie ma wystarczająco bogatego systemu typów, aby bezpośrednio reprezentować sam wzorzec monady; potrzebujesz systemu typów, który obsługuje typy wyższe niż typy ogólne. Więc nie próbowałbym tego zrobić. Zamiast tego zaimplementowałbym ogólne typy reprezentujące każdą monadę i zaimplementowałbym metody reprezentujące trzy potrzebne operacje: przekształcenie wartości w wartość amplifikowaną, (być może) przekształcenie wartości amplifikowanej w wartość i przekształcenie funkcji w wartościach nieamplifikowanych w funkcja wzmocnionych wartości.

Dobrym miejscem na początek jest to, jak wdrożyliśmy LINQ w C #. Przestudiuj SelectManymetodę; jest to klucz do zrozumienia, jak monada sekwencji działa w języku C #. Jest to bardzo prosta metoda, ale bardzo potężna!


Sugerowane dalsze czytanie:

  1. Aby uzyskać bardziej dogłębne i teoretycznie uzasadnione wyjaśnienie monad w języku C #, gorąco polecam artykuł mojego kolegi ( Erica Lipperta ) Wesa Dyera na ten temat. Ten artykuł wyjaśnił mi monady, kiedy w końcu „kliknęły” mnie.
  2. Dobra ilustracja, dlaczego możesz chcieć mieć wokół siebie monadę (w przykładach używa Haskell) .
  3. Coś w rodzaju „tłumaczenia” poprzedniego artykułu na JavaScript.


17
To świetna odpowiedź, ale moja głowa poszła na marne. Będę śledzić i wpatrywać się w to w ten weekend i zadawać ci pytania, jeśli sprawy się nie ułożą i nie będą miały sensu w mojej głowie.
Paul Nathan

5
Doskonałe wyjaśnienie jak zwykle Eric. W bardziej teoretycznej (ale wciąż bardzo interesującej) dyskusji znalazłem post na blogu Barta De Smeta na MinLINQ, który pomógł w powiązaniu niektórych funkcjonalnych konstrukcji programistycznych z powrotem do C #. community.bartdesmet.net/blogs/bart/archive/2010/01/01/…
Ron Warholic

41
Bardziej sensowne jest dla mnie stwierdzenie, że zwiększa typy, a nie je wzmacnia .
Gabe

61
@slomojo: i zmieniłem to z powrotem na to, co napisałem i zamierzałem napisać. Jeśli ty i Gabe chcesz napisać własną odpowiedź, idź naprzód.
Eric Lippert,

24
@Eric, oczywiście do ciebie, ale wzmacniacz sugeruje, że istniejące właściwości są wzmocnione, co jest mylące.
ocodo

341

Dlaczego potrzebujemy monad?

  1. Chcemy programować tylko za pomocą funkcji . („programowanie funkcjonalne” po wszystkim -FP).
  2. Mamy więc pierwszy duży problem. To jest program:

    f(x) = 2 * x

    g(x,y) = x / y

    Jak możemy powiedzieć, co należy najpierw wykonać ? Jak możemy utworzyć uporządkowaną sekwencję funkcji (tj. Program ), używając tylko funkcji?

    Rozwiązanie: komponuj funkcje . Jeśli chcesz najpierw g, a następnie f, po prostu napisz f(g(x,y)). Dobrze, ale ...

  3. Więcej problemów: niektóre funkcje mogą zawieść (tzn. g(2,0)Podzielić przez 0). W FP nie mamy żadnych „wyjątków” . Jak to rozwiązujemy?

    Rozwiązanie: Pozwólmy funkcjom zwrócić dwie rzeczy : zamiast mieć g : Real,Real -> Real(funkcja z dwóch rzeczywistych w rzeczywistą), pozwólmy g : Real,Real -> Real | Nothing(funkcja z dwóch rzeczywistych w rzeczywistą lub rzeczywistą).

  4. Ale funkcje powinny (mówiąc prościej) zwracać tylko jedną rzecz .

    Rozwiązanie: stwórzmy nowy typ danych, które mają zostać zwrócone, „ typ boksu ”, który może zawierać rzeczywisty lub po prostu nic. Dlatego możemy mieć g : Real,Real -> Maybe Real. Dobrze, ale ...

  5. Co się teraz stanie f(g(x,y))? fnie jest gotowy do spożycia Maybe Real. I nie chcemy zmieniać każdej funkcji, z którą możemy się połączyć, gaby zużyć Maybe Real.

    Rozwiązanie: skorzystajmy ze specjalnej funkcji do łączenia funkcji / łączenia / tworzenia / łączenia . W ten sposób możemy za kulisami dostosować wyjście jednej funkcji, aby zasilić następną.

    W naszym przypadku: g >>= f(connect / komponować gsię f). Chcemy >>=uzyskać gdane wyjściowe, sprawdzić je, a jeśli Nothingtak, to nie dzwoń fi nie wracaj Nothing; lub wręcz przeciwnie, wyodrębnij pudełko Reali karm fje nim. (Ten algorytm jest właśnie realizacja >>=dla Maybetypu).

  6. Powstaje wiele innych problemów, które można rozwiązać za pomocą tego samego wzorca: 1. Użyj „pola”, aby skodyfikować / zapisać różne znaczenia / wartości, i takie funkcje gzwracają te „wartości w ramkach”. 2. Mieć kompozytorów / linkerów, g >>= fktórzy pomogą połączyć gwyjście z fwejściem, więc nie musimy się wcale zmieniać f.

  7. Niezwykłe problemy, które można rozwiązać za pomocą tej techniki:

    • posiadające globalny stan, w którym każda funkcja w sekwencji funkcji („program”) może współdzielić: rozwiązanie StateMonad.

    • Nie lubimy „nieczystych funkcji”: funkcji, które dają różne dane wyjściowe dla tego samego wejścia. Dlatego zaznaczmy te funkcje, aby zwróciły wartość oznaczoną / umieszczoną w ramce: IOmonad.

Całkowite szczęście !!!!


2
@DmitriZaitsev Wyjątki mogą wystąpić tylko w „nieczystym kodzie” (monadzie IO), o ile wiem.
cibercitizen1

3
@DmitriZaitsev W roli Nic nie może grać żaden inny typ (inny niż oczekiwany Real). Nie o to chodzi. W tym przykładzie chodzi o to, jak dostosować funkcje w łańcuchu, gdy poprzedni może zwrócić nieoczekiwany typ wartości do następnego, bez łączenia tego drugiego (akceptując tylko Real jako dane wejściowe).
cibercitizen1

3
Innym zamieszaniem jest to, że słowo „monada” pojawia się tylko dwa razy w twojej odpowiedzi i tylko w połączeniu z innymi terminami - Statei IObez podania żadnego z nich oraz dokładnego znaczenia „monady”
Dmitri Zaitsev

31
Dla mnie, jako osoby pochodzącej z OOP, ta odpowiedź naprawdę dobrze wyjaśniła motywację posiadania monady, a także to, czym tak naprawdę jest monada (bardziej niż zaakceptowana odpowiedź). Uważam to za bardzo pomocne. Wielkie dzięki @ cibercitizen1 i +1
akhilless

3
Od około roku czytam o programowaniu funkcjonalnym. Ta odpowiedź, a zwłaszcza dwa pierwsze punkty, wreszcie pozwoliły mi zrozumieć, co tak naprawdę oznacza programowanie imperatywne i dlaczego programowanie funkcjonalne jest inne. Dziękuję Ci!
jrahhali

82

Powiedziałbym, że najbliższą analogią OO do monad jest „ wzorzec poleceń ”.

We wzorcu poleceń zawijasz zwykłą instrukcję lub wyrażenie w obiekcie polecenia . Obiekt polecenia ujawnia metodę wykonania , która wykonuje zawiniętą instrukcję. Tak więc instrukcje są przekształcane w obiekty pierwszej klasy, które mogą być przekazywane i wykonywane do woli. Polecenia można komponować, dzięki czemu można utworzyć obiekt programu przez połączenie i zagnieżdżenie obiektów poleceń.

Polecenia są wykonywane przez osobny obiekt, wywołujący . Zaletą używania wzorca poleceń (zamiast wykonywania szeregu zwykłych instrukcji) jest to, że różni wywołujący mogą stosować inną logikę do sposobu wykonywania poleceń.

Wzorzec poleceń można wykorzystać do dodania (lub usunięcia) funkcji językowych, które nie są obsługiwane przez język hosta. Na przykład w hipotetycznym języku OO bez wyjątków można dodać semantykę wyjątków, udostępniając komendom metody „try” i „throw”. Gdy wywołania polecenia rzucają, wywołujący przegląda listę (lub drzewo) poleceń aż do ostatniego wywołania „try”. I odwrotnie, możesz usunąć semantyczny wyjątek z języka (jeśli uważasz, że wyjątki są złe ), przechwytując wszystkie wyjątki zgłaszane przez poszczególne polecenia i przekształcając je w kody błędów, które są następnie przekazywane do następnego polecenia.

Jeszcze bardziej wymyślną semantykę wykonywania, taką jak transakcje, wykonanie niedeterministyczne lub kontynuacje, można zaimplementować w ten sposób w języku, który nie obsługuje go natywnie. Jest to dość potężny wzór, jeśli się nad tym zastanowić.

Teraz w rzeczywistości wzorce poleceń nie są używane jako ogólna funkcja języka taka jak ta. Narzut związany z przekształceniem każdej instrukcji w osobną klasę doprowadziłby do nie do zniesienia ilości kodu typu „kocioł”. Ale w zasadzie można go użyć do rozwiązania tych samych problemów, co monady do rozwiązania w fp.


15
Sądzę, że jest to pierwsze wyjaśnienie monady, które widziałem, i które nie opierało się na funkcjonalnych koncepcjach programistycznych i ujmowało je w kategoriach rzeczywistego OOP. Naprawdę dobra odpowiedź.
David K. Hess,

to jest bardzo blisko 2, jakie monady faktycznie są w FP / Haskell, z tym wyjątkiem, że same obiekty poleceń „wiedzą”, do której „logiki wywoływania” należą (i tylko te kompatybilne mogą być połączone razem); wywołujący dostarcza tylko pierwszą wartość. To nie tak, że polecenie „Drukuj” można wykonać za pomocą „niedeterministycznej logiki wykonania”. Nie, musi to być „logika I / O” (tj. Monada IO). Ale poza tym jest bardzo blisko. Można nawet powiedzieć, że monady to tylko programy (zbudowane z instrukcji kodowych, które zostaną wykonane później). We wczesnych dniach mówiono o „wiązaniu” jako o „programowalnym średniku” .
Czy Ness

1
@ DavidK.Hess Naprawdę jestem bardzo sceptycznie nastawiony do odpowiedzi, które wykorzystują FP do wyjaśnienia podstawowych pojęć dotyczących FP, a zwłaszcza odpowiedzi, które używają języka FP, takiego jak Scala. Dobra robota, JacquesB!
Przywróć Monikę

62

W kategoriach zrozumiałych dla programisty OOP (bez żadnego funkcjonalnego tła programowania), czym jest monada?

Jaki problem rozwiązuje i jakie są najczęściej używane miejsca? Jakie są najczęściej używane miejsca?

W zakresie programowania obiektowego monada jest interfejs (lub bardziej prawdopodobne wstawek), parametryzowane przez typ z dwoma metodami, returna bindktóre opisują:

  • Jak wstrzyknąć wartość, aby uzyskać monadyczną wartość tego typu wstrzykniętej wartości;
  • Jak używać funkcji, która tworzy wartość monadyczną z wartości niemonadycznej, na wartości monadycznej.

Problem, który rozwiązuje, to ten sam typ problemu, jakiego można oczekiwać od dowolnego interfejsu, a mianowicie: „Mam kilka różnych klas, które robią różne rzeczy, ale wydaje się, że robią te różne rzeczy w sposób, który ma podstawowe podobieństwo. czy mogę opisać to podobieństwo między nimi, nawet jeśli same klasy nie są tak naprawdę podtypami czegoś bliższego niż sama klasa „Object”? ”

Mówiąc dokładniej, Monad„interfejs” jest podobny IEnumeratorlub IIteratorprzyjmuje typ, który sam przyjmuje typ. Jednak głównym „punktem” Monadjest możliwość łączenia operacji opartych na typie wnętrza, nawet do takiego stopnia, że ​​ma nowy „typ wewnętrzny”, zachowując - a nawet wzmacniając - strukturę informacyjną głównej klasy.


1
returntak naprawdę nie byłby metodą na monadzie, ponieważ nie bierze ona pod uwagę instancji monady jako argumentu. (tj .: nie ma tego / ja)
Laurence Gonsalves

@LaurenceGonsalves: Ponieważ obecnie zastanawiam się nad tym w pracy licencjackiej, myślę, że najbardziej ograniczający jest brak metod statycznych w interfejsach w języku C # / Java. Możesz przejść daleko w kierunku wdrożenia całej historii monady, przynajmniej statycznie związanej zamiast opartej na typach. Co ciekawe, zadziałałoby to nawet pomimo braku wyższych typów.
Sebastian Graf

42

Masz ostatnią prezentację „ Monadologie - profesjonalna pomoc na temat lęku typu ” autorstwa Christophera League (12 lipca 2010 r.), Która jest dość interesująca na tematy kontynuacji i monady.
Wideo z prezentacją (udostępnianie slajdów) jest faktycznie dostępne na vimeo .
Część Monada rozpoczyna się około 37 minut od tego godzinnego wideo i zaczyna się od slajdu 42 z 58 prezentacji.

Jest on przedstawiany jako „wiodący wzorzec projektowy dla programowania funkcjonalnego”, ale językiem użytym w przykładach jest Scala, który jest zarówno OOP, jak i funkcjonalny.
Więcej na temat Monady w Scali można przeczytać w poście na blogu „ Monady - inny sposób na abstrakcyjne obliczenia w Scali ”, z Debasish Ghosh (27 marca 2008).

Typ konstruktor M jest monada czy obsługuje te operacje:

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

Na przykład (w Scali):

  • Option jest monadą
    def jednostka [A] (x: A): Opcja [A] = Niektóre (x)

    def flatMap [A, B] (m: Opcja [A]) (f: A => Opcja [B]): Opcja [B] =
      m dopasowanie {
       przypadek Brak => Brak
       case Niektóre (x) => f (x)
      }
  • List jest Monada
    def jednostka [A] (x: A): Lista [A] = Lista (x)

    def flatMap [A, B] (m: Lista [A]) (f: A => Lista [B]): Lista [B] =
      m dopasowanie {
        przypadek zero => zero
        przypadek x :: xs => f (x) ::: flatMap (xs) (f)
      }

Monada to wielka sprawa w Scali ze względu na wygodną składnię zbudowaną z myślą o wykorzystaniu struktur Monady:

forrozumienie w Scali :

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

jest tłumaczony przez kompilator na:

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

Kluczową abstrakcją jest ta flatMap, która wiąże obliczenia poprzez łańcuch.
Każde wywołanie flatMapzwraca ten sam typ struktury danych (ale o innej wartości), który służy jako dane wejściowe do następnego polecenia w łańcuchu.

W powyższym fragmencie flatMap przyjmuje jako dane wejściowe zamknięcie (SomeType) => List[AnotherType]i zwraca a List[AnotherType]. Należy zauważyć, że wszystkie płaskie mapy przyjmują ten sam typ zamknięcia co dane wejściowe i zwracają ten sam typ co dane wyjściowe.

Właśnie to „wiąże” wątek obliczeniowy - każdy element sekwencji w zrozumieniu musi spełniać to samo ograniczenie typu.


Jeśli wykonasz dwie operacje (które mogą się nie powieść) i przekażesz wynik trzeciej, na przykład:

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

ale bez korzystania z Monady otrzymujesz zawiły kod OOP, taki jak:

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
  }

mając na uwadze, że dzięki Monadzie możesz pracować z typami rzeczywistymi ( Venue, User), tak jak działają wszystkie operacje, i ukrywać elementy weryfikacji Opcji, wszystko z powodu płaskich map składni for:

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

Część wydajności zostanie wykonana tylko wtedy, gdy wszystkie trzy funkcje mają Some[X]; wszelkie Nonezostaną bezpośrednio zwrócone do confirm.


Więc:

Monady pozwalają na uporządkowane obliczenia w ramach programowania funkcjonalnego, co pozwala nam modelować sekwencjonowanie akcji w ładnej ustrukturyzowanej formie, trochę jak DSL.

A największa moc pochodzi z umiejętności komponowania monad, które służą różnym celom, w rozszerzalne abstrakcje w obrębie aplikacji.

To sekwencjonowanie i łączenie działań przez monadę wykonuje kompilator języka, który dokonuje transformacji za pomocą magii zamknięć.


Nawiasem mówiąc, Monada to nie tylko model obliczeniowy stosowany w FP:

Teoria kategorii proponuje wiele modeli obliczeń. Pomiędzy nimi

  • model strzałkowy obliczeń
  • model obliczeń Monady
  • Applicative modelu obliczeń

2
Uwielbiam to wyjaśnienie! Podany przykład pięknie pokazuje tę koncepcję, a także dodaje to, czego IMHO brakowało w zwiastunie Erica o SelectMany () jako monadzie. Dzięki za to!
aoven

1
IMHO to jest najbardziej elegancka odpowiedź
polimeraza

a przede wszystkim Functor.
Czy Ness

34

Aby uszanować szybkich czytelników, najpierw zaczynam od precyzyjnej definicji, kontynuuję szybkie wyjaśnienia w języku angielskim, a następnie przechodzę do przykładów.

Oto nieco zwięzła i precyzyjna definicja nieco przeredagowana:

Monada (w informatyce) jest formalnie mapa, która:

  • wysyła każdy typ Xdanego języka programowania do nowego typu T(X)(zwanego „typem Tobliczeń z wartościami w X”);

  • wyposażony w regułę tworzenia dwóch funkcji formularza f:X->T(Y)i g:Y->T(Z)funkcji g∘f:X->T(Z);

  • w sposób asocjatywny w oczywistym sensie i jedności w odniesieniu do danej wywoływanej funkcji jednostkowej pure_X:X->T(X), którą należy traktować jako przyjmowanie wartości do czystego obliczenia, które po prostu zwraca tę wartość.

Tak więc w prostych słowach monada jest regułą przekazywaną z dowolnego typu Xna inny typT(X) oraz regułą przekazywaną z dwóch funkcji f:X->T(Y)i g:Y->T(Z)(którą chciałbyś skomponować, ale nie możesz) do nowej funkcjih:X->T(Z) . Który jednak nie jest kompozycją w ścisłym sensie matematycznym. Zasadniczo zajmujemy się kompozycją funkcji „zginania” lub redefiniowaniem sposobu, w jaki funkcje są tworzone.

Dodatkowo, wymagamy reguły komponowania monady, aby spełnić „oczywiste” matematyczne aksjomaty:

  • Łączność : Tworzenie fsię g, a następnie z h(od zewnątrz), powinny być takie same jak komponować gz ha następnie f(od wewnątrz).
  • Właściwość Unital : Komponowanie fz funkcją tożsamości po obu stronach powinno dać f.

Ponownie, w prostych słowach, nie możemy po prostu zwariować, zmieniając skład naszej funkcji tak, jak lubimy:

  • Najpierw potrzebujemy asocjatywności, aby móc np. Skomponować kilka funkcji z rzędu f(g(h(k(x))), i nie martwić się o określenie par funkcji tworzących porządek. Ponieważ reguła monady określa tylko, jak skomponować parę funkcji , bez tego aksjomatu musielibyśmy wiedzieć, która para składa się jako pierwsza i tak dalej. (Należy pamiętać, że różni się od właściwości komutatywności, fz którą skomponowane gbyły takie same, jak gz f, która nie jest wymagana).
  • Po drugie, potrzebujemy jednolitej własności, czyli po prostu powiedzieć, że tożsamości składają się w sposób trywialny tak, jak się ich spodziewamy. Możemy więc bezpiecznie refaktoryzować funkcje za każdym razem, gdy można wyodrębnić te tożsamości.

Tak więc w skrócie: monada jest regułą rozszerzenia typu i tworzenia funkcji spełniających dwa aksjomaty - asocjatywność i właściwość jedności.

W praktyce chcesz, aby monada została zaimplementowana dla Ciebie przez język, kompilator lub środowisko, które zajmą się tworzeniem dla Ciebie funkcji. Możesz więc skupić się na pisaniu logiki funkcji, zamiast martwić się o to, jak zostanie wykonana ich realizacja.

To w gruncie rzeczy w skrócie.


Jako profesjonalny matematyk wolę unikać nazywania h„kompozycją” fi g. Ponieważ matematycznie tak nie jest. Nazywanie go „kompozycją” niepoprawnie zakłada, że hjest to prawdziwa kompozycja matematyczna, a tak nie jest. Nie jest to nawet jednoznacznie określone przez fi g. Zamiast tego jest to wynikiem nowej „zasady komponowania” naszych monad. Który może całkowicie różnić się od faktycznego składu matematycznego, nawet jeśli ten drugi istnieje!


Aby uczynić go mniej suchym, pozwól mi zilustrować go przykładem, który adnotuję za pomocą małych sekcji, abyś mógł przejść od razu do rzeczy.

Zgłaszanie wyjątków jako przykłady Monady

Załóżmy, że chcemy skomponować dwie funkcje:

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

Ale f(0)nie jest zdefiniowany, dlatego zgłaszany ejest wyjątek . Jak zatem zdefiniować wartość składu g(f(0))? Oczywiście ponownie rzuć wyjątek! Może to samo e. Może nowy zaktualizowany wyjątek e1.

Co dokładnie się tutaj dzieje? Po pierwsze, potrzebujemy nowych wartości wyjątku (różnych lub takich samych). Można do nich zadzwonić nothinglub nullczy cokolwiek, ale istota pozostaje ta sama - powinny one być nowe wartości, na przykład, że nie powinno być numberw naszym przykładzie tutaj. Wolę nie dzwonić do nich, nullaby uniknąć pomyłek z tym, jak nullmożna je zaimplementować w dowolnym języku. Tak nothingsamo wolę unikać, ponieważ często się z tym kojarzy null, co w zasadzie jest tym, co nullnależy zrobić, jednak zasada ta często się nagina z jakichkolwiek praktycznych powodów.

Czym dokładnie jest wyjątek?

Jest to trywialna sprawa dla każdego doświadczonego programisty, ale chciałbym dodać kilka słów, aby zgasić robaka zamieszania:

Wyjątek to obiekt kapsułkujący informacje o tym, jak wystąpił nieprawidłowy wynik wykonania.

Może to polegać na wyrzuceniu jakichkolwiek szczegółów i zwróceniu pojedynczej wartości globalnej (jak NaNlub null) lub wygenerowaniu długiej listy dziennika lub tego, co się dokładnie wydarzyło, wysłaniu jej do bazy danych i replikacji w całej rozproszonej warstwie przechowywania danych;)

Ważną różnicą między tymi dwoma skrajnymi przykładami wyjątku jest to, że w pierwszym przypadku nie występują żadne skutki uboczne . W drugim są. Co prowadzi nas do pytania (w tysiącach dolarów):

Czy dozwolone są wyjątki w czystych funkcjach?

Krótsza odpowiedź : tak, ale tylko wtedy, gdy nie prowadzą do skutków ubocznych.

Dłuższa odpowiedź. Aby być czystym, wynik funkcji musi być jednoznacznie określony przez jej dane wejściowe. Dlatego zmieniamy naszą funkcję f, wysyłając 0do nowej wartości abstrakcyjnej e, którą nazywamy wyjątkiem. Zapewniamy, że wartość enie zawiera żadnych informacji zewnętrznych, które nie są jednoznacznie określone przez nasze dane wejściowe, czyli x. Oto przykład wyjątku bez efektu ubocznego:

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

A oto jeden z efektem ubocznym:

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

W rzeczywistości ma skutki uboczne tylko wtedy, gdy ta wiadomość może się zmienić w przyszłości. Ale jeśli gwarantuje się, że nigdy się nie zmieni, wartość ta staje się wyjątkowo przewidywalna, a zatem nie występuje efekt uboczny.

Aby było jeszcze głupiej. Funkcja powracająca 42kiedykolwiek jest wyraźnie czysta. Ale jeśli ktoś szalony zdecyduje się 42na zmienną, której wartość może się zmienić, ta sama funkcja przestaje być czysta w nowych warunkach.

Zauważ, że używam notacji dosłowności obiektowej dla uproszczenia, aby zademonstrować istotę. Niestety rzeczy są pomieszane się w językach takich jak JavaScript, gdzie errornie jest to typ, który zachowuje się tak jak chcemy tutaj w odniesieniu do kompozycji funkcyjnego, natomiast rzeczywistych typów podobnych nulllub NaNnie zachowują się w ten sposób, ale raczej przejść przez jakiś sztuczny i nie zawsze intuicyjny typ konwersji.

Rozszerzenie typu

Ponieważ chcemy zmienić komunikat w naszym wyjątku, naprawdę ogłaszamy nowy typ Edla całego obiektu wyjątku, a następnie to właśnie maybe numberrobi, oprócz mylącej nazwy, która ma być albo typem, numberalbo nowym typem wyjątku E, więc jest naprawdę związek number | Ez numberi E. W szczególności zależy to od tego, jak chcemy zbudować E, co nie jest ani sugerowane, ani odzwierciedlone w nazwie maybe number.

Co to jest skład funkcjonalny?

Jest matematyczne funkcje Wykonywanie operacji f: X -> Yi g: Y -> Zi budowaniu ich skład jako funkcję h: X -> Zzaspokajania h(x) = g(f(x)). Problem z tą definicją występuje, gdy wynik f(x)nie jest dozwolony jako argument g.

W matematyce tych funkcji nie da się ułożyć bez dodatkowej pracy. Rozwiązanie ściśle matematyczne dla powyższego przykładu fi gma zostać usunięte 0ze zbioru definicji f. Dzięki temu nowemu zestawowi definicji (nowemu, bardziej restrykcyjnemu typowi x) fstaje się on kompatybilny z g.

Ograniczenie zestawu takich definicji nie jest jednak zbyt praktyczne w programowaniu f. Zamiast tego można zastosować wyjątki.

Lub innym podejściu wartości tworzone są sztuczne jak NaN, undefined, null, Infinityitd. Więc ocenia 1/0się Infinityi 1/-0do -Infinity. A następnie wymuś nową wartość z powrotem do wyrażenia zamiast zgłaszania wyjątku. Prowadząc do wyników, które możesz, ale nie musisz, przewidzieć:

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

Wróciliśmy do regularnych liczb gotowych do przejścia;)

JavaScript pozwala nam nadal wykonywać wyrażenia liczbowe za wszelką cenę bez zgłaszania błędów, jak w powyższym przykładzie. Oznacza to, że pozwala także komponować funkcje. Na tym właśnie polega monada - regułą jest komponowanie funkcji spełniających aksjomaty zdefiniowane na początku tej odpowiedzi.

Ale czy zasada komponowania funkcji wynikająca z implementacji JavaScript do obsługi błędów numerycznych to monada?

Aby odpowiedzieć na to pytanie, wystarczy sprawdzić aksjomaty (pozostawione jako ćwiczenie jako część pytania tutaj;).

Czy można zastosować wyjątek rzucania do skonstruowania monady?

Rzeczywiście, bardziej użyteczną monadą byłaby zamiast tego reguła, która mówi, że jeśli fzgłasza wyjątek dla niektórych x, to i jego skład z każdym g. Plus sprawiają, że wyjątek ten jest Eunikalny na całym świecie i ma tylko jedną możliwą wartość ( obiekt końcowy w teorii kategorii). Teraz dwa aksjomaty są natychmiast sprawdzalne i otrzymujemy bardzo przydatną monadę. Rezultatem jest tak zwana być może monada .


3
Dobry wkład. +1 Ale może chcesz usunąć „znalazłeś większość wyjaśnień za długo ...”, będąc w ogóle twoim najdłuższym. Inni ocenią, czy jest to „zwykły angielski”, zgodnie z wymaganiami, pytanie: „zwykły angielski == w prostych słowach, w prosty sposób”.
cibercitizen1

@ cibercitizen1 Thanks! W rzeczywistości jest krótki, jeśli nie policzysz przykładu. Najważniejsze jest to, że nie musisz czytać przykładu, aby zrozumieć definicję . Niestety wiele wyjaśnień zmusza mnie do przeczytania najpierw przykładów , co często jest niepotrzebne, ale oczywiście może wymagać dodatkowej pracy dla pisarza. Przy zbyt dużym poleganiu na konkretnych przykładach istnieje niebezpieczeństwo, że nieistotne szczegóły przesłaniają obraz i utrudniają jego zrozumienie. Powiedziawszy to, masz ważne punkty, zobacz aktualizację.
Dmitri Zaitsev

2
za długi i mylący
seenimurugan

1
@seenimurugan Sugestie dotyczące ulepszeń są mile widziane;)
Dmitri Zaitsev

26

Monada to typ danych, który hermetyzuje wartość i do którego zasadniczo można zastosować dwie operacje:

  • return x tworzy wartość typu monada, która jest hermetyzowana x
  • m >>= f(czytaj jako „operator powiązania”) stosuje funkcję fdo wartości w monadziem

Taka jest monada. Jest jeszcze kilka szczegółów technicznych , ale zasadniczo te dwie operacje definiują monadę. Prawdziwe pytanie brzmi: „Co robi monada ?”, A to zależy od monady - listy to monady, Maybes to monady, operacje IO to monady. Gdy mówimy, że te rzeczy są monadami, oznacza to, że mają one interfejs monad returni >>=.


„Co robi monada, a to zależy od monady”: a dokładniej, zależy to od bindfunkcji, którą należy zdefiniować dla każdego typu monady , prawda? Byłby to dobry powód, aby nie mylić wiązania z kompozycją, ponieważ istnieje jedna definicja dla kompozycji, podczas gdy nie może istnieć tylko jedna definicja dla funkcji wiązania, istnieje jedna na typ monadyczny, jeśli dobrze rozumiem.
Hibou57

14

Z wikipedii :

W programowaniu funkcjonalnym monada jest rodzajem abstrakcyjnego typu danych wykorzystywanego do reprezentowania obliczeń (zamiast danych w modelu domeny). Monady pozwalają programiście łączyć akcje w celu zbudowania potoku, w którym każda akcja jest ozdobiona dodatkowymi regułami przetwarzania zapewnianymi przez monadę. Programy napisane w stylu funkcjonalnym mogą wykorzystywać monady do strukturyzacji procedur obejmujących operacje sekwencyjne 1 [2] lub do definiowania dowolnych przepływów sterowania (takich jak obsługa współbieżności, kontynuacji lub wyjątków).

Formalnie monada jest konstruowana przez zdefiniowanie dwóch operacji (powiązania i powrotu) oraz konstruktora typu M, który musi spełniać kilka właściwości, aby umożliwić prawidłowy skład funkcji monadycznych (tj. Funkcji, które wykorzystują wartości z monady jako argumentów). Operacja return pobiera wartość z typu zwykłego i umieszcza ją w monadycznym pojemniku typu M. Operacja wiązania wykonuje proces odwrotny, wydobywając pierwotną wartość z kontenera i przekazując ją do powiązanej następnej funkcji w potoku.

Programista skomponuje funkcje monadyczne w celu zdefiniowania potoku przetwarzania danych. Monada działa jako struktura, ponieważ jest to zachowanie wielokrotnego użytku, które decyduje o kolejności wywoływania określonych funkcji monadycznych w potoku i zarządza wszystkimi tajnymi pracami wymaganymi przez obliczenia. [3] Operatory bind i return przeplecione w potoku zostaną wykonane po każdej funkcji monadycznej, która zwróci kontrolę, i zajmą się poszczególnymi aspektami obsługiwanymi przez monadę.

Wierzę, że to wyjaśnia to bardzo dobrze.


12

Spróbuję stworzyć najkrótszą definicję, którą mogę zarządzać, używając terminów OOP:

Klasa ogólna CMonadic<T>to monada, jeśli definiuje co najmniej następujące metody:

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
}

oraz jeżeli następujące prawa mają zastosowanie do wszystkich typów T i ich możliwych wartości t

lewa tożsamość:

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

właściwa tożsamość

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

skojarzenie:

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

Przykłady :

Monada listy może mieć:

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

A flatMap na liście [1,2,3] może działać tak:

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

Iterabile i Obserwowalne mogą być również monadyczne, a także obietnice i zadania.

Komentarz :

Monady nie są takie skomplikowane. flatMapFunkcja jest bardzo podobny bardziej powszechnie spotykane map. Otrzymuje argument funkcji (zwany także delegatem), który może wywoływać (natychmiast lub później, zero lub więcej razy) z wartością pochodzącą z klasy ogólnej. Oczekuje, że przekazana funkcja zawinie również wartość zwracaną w ten sam rodzaj klasy ogólnej. Aby temu zaradzić, zapewnia createkonstruktor, który może utworzyć instancję tej klasy ogólnej na podstawie wartości. Zwracany wynik flatMap jest także ogólną klasą tego samego typu, często pakującą te same wartości, które były zawarte w wynikach zwracanych przez jedną lub więcej aplikacji flatMap do wcześniej zawartych wartości. Pozwala to na łączenie flatMap w dowolny sposób:

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

Tak się składa, że ​​ten rodzaj klasy ogólnej jest użyteczny jako model podstawowy dla ogromnej liczby rzeczy. To (wraz z żargonizmem teorii kategorii) jest przyczyną, dla której Monady wydają się tak trudne do zrozumienia lub wyjaśnienia. Są bardzo abstrakcyjną rzeczą i stają się oczywiście przydatne dopiero po specjalizacji.

Na przykład można modelować wyjątki za pomocą kontenerów monadycznych. Każdy pojemnik będzie zawierał wynik operacji lub błąd, który wystąpił. Następna funkcja (delegata) w łańcuchu wywołań zwrotnych flatMap zostanie wywołana tylko wtedy, gdy poprzednia zapakowała wartość do kontenera. W przeciwnym razie, jeśli błąd zostanie spakowany, błąd będzie kontynuowany przez powiązane łańcuchy, aż do znalezienia kontenera z funkcją obsługi błędów podłączoną za pomocą metody o nazwie .orElse()(taka metoda byłaby dozwolonym rozszerzeniem)

Uwagi : Języki funkcjonalne umożliwiają pisanie funkcji, które mogą działać na dowolnym rodzaju monadycznej klasy ogólnej. Aby to zadziałało, należałoby napisać ogólny interfejs dla monad. Nie wiem, czy można napisać taki interfejs w języku C #, ale o ile wiem, nie jest to:

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

To, czy monada ma „naturalną” interpretację w OO, zależy od monady. W języku takim jak Java możesz przetłumaczyć być może monadę na język sprawdzania zerowych wskaźników, dzięki czemu obliczenia, które zawodzą (tj. Nie produkują nic w Haskell), emitują zerowe wskaźniki jako wyniki. Możesz przetłumaczyć monadę stanu na język generowany przez utworzenie zmiennej zmiennej i metody zmiany jej stanu.

Monada to monoid w kategorii endofunkorów.

Informacje, które łączy zdanie, są bardzo głębokie. I pracujesz w monadzie z dowolnym imperatywnym językiem. Monada jest językiem specyficznym dla domeny. Spełnia pewne interesujące właściwości, które razem sprawiają, że monada jest matematycznym modelem „programowania imperatywnego”. Haskell ułatwia definiowanie małych (lub dużych) języków imperatywnych, które można łączyć na różne sposoby.

Jako programista OO, używasz hierarchii klas swojego języka do organizowania rodzajów funkcji lub procedur, które można wywoływać w kontekście, który nazywasz obiektem. Monada jest również abstrakcją tego pomysłu, o ile różne monady można łączyć w dowolny sposób, skutecznie „importując” wszystkie metody sub-monady do zakresu.

Architektonicznie następnie używa się podpisów typu, aby jednoznacznie wyrazić, które konteksty mogą być użyte do obliczenia wartości.

W tym celu można zastosować transformatory monadowe, a kolekcja „standardowych” monad jest wysokiej jakości:

  • Listy (obliczenia niedeterministyczne, traktując listę jako domenę)
  • Może (obliczenia, które mogą się nie powieść, ale dla których raportowanie nie jest ważne)
  • Błąd (obliczenia, które mogą się nie powieść i wymagają obsługi wyjątków
  • Czytnik (obliczenia, które mogą być reprezentowane przez kompozycje prostych funkcji Haskella)
  • Writer (obliczenia z sekwencyjnym „renderowaniem” / „rejestrowaniem” (do ciągów znaków, html itp.)
  • Cont (kontynuacje)
  • IO (obliczenia zależne od bazowego systemu komputerowego)
  • Stan (obliczenia, których kontekst zawiera modyfikowalną wartość)

z odpowiednimi transformatorami monadowymi i klasami typów. Klasy typów umożliwiają komplementarne podejście do łączenia monad poprzez ujednolicenie ich interfejsów, dzięki czemu konkretne monady mogą implementować standardowy interfejs dla monady „rodzaju”. Na przykład moduł Control.Monad.State zawiera klasę MonadState sm, a (stan) jest instancją formularza

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

Długa historia mówi, że monada jest funktorem, który łączy „kontekst” z wartością, który ma sposób na wstrzyknięcie wartości monadzie i który ma sposób oceny wartości w odniesieniu do kontekstu z nią związanego, przynajmniej w ograniczony sposób.

Więc:

return :: a -> m a

jest funkcją, która wstrzykuje wartość typu a do monadowej „akcji” typu ma.

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

jest funkcją, która wykonuje akcję monady, ocenia jej wynik i stosuje funkcję do wyniku. Fajne w (>> =) jest to, że wynik jest w tej samej monadzie. Innymi słowy, wm >> = f, (>> =) wyciąga wynik z m i wiąże go zf, tak aby wynik był w monadzie. (Alternatywnie możemy powiedzieć, że (>> =) ściąga f do m i stosuje go do wyniku.) W konsekwencji, jeśli mamy f :: a -> mb i g :: b -> mc, możemy działania „sekwencyjne”:

m >>= f >>= g

Lub używając „notacji”

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

Typ dla (>>) może być podświetlony. To jest

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

Odpowiada operatorowi (;) w językach proceduralnych, takich jak C. Pozwala na notację:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

W logice matematycznej i filozoficznej mamy ramy i modele, które są „naturalnie” modelowane monadyzmem. Interpretacja to funkcja, która analizuje domenę modelu i oblicza wartość prawdy (lub uogólnienia) zdania (lub formuły, w uogólnieniu). W logice modalnej z konieczności możemy powiedzieć, że propozycja jest konieczna, jeśli jest prawdziwa w „każdym możliwym świecie” - jeśli jest prawdziwa w odniesieniu do każdej dopuszczalnej dziedziny. Oznacza to, że model propozycji w języku może zostać zreifikowany jako model, którego domena składa się ze zbioru odrębnych modeli (jednego odpowiadającego każdemu możliwemu światowi). Każda monada ma metodę o nazwie „łączyć”, która spłaszcza warstwy, co oznacza, że ​​każda akcja monady, której wynikiem jest akcja monady, może zostać osadzona w monadzie.

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

Co ważniejsze, oznacza to, że monada jest zamknięta w ramach operacji „układania warstw”. Oto jak działają transformatory monadowe: łączą one monady, zapewniając metody typu łączenia dla typów podobnych

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

abyśmy mogli przekształcić akcję w (MożeT m) w akcję wm, skutecznie zwijając warstwy. W tym przypadku metoda runMaybeT :: MaybeT ma -> m (Może a) jest naszą metodą łączenia. (Może T m) to monada, a Może T :: m (Może a) -> Może T ma skutecznie konstruktor nowego typu działania monady w m.

Wolna monada dla funktora to monada generowana przez układanie w stosy f, co implikuje, że każda sekwencja konstruktorów dla f jest elementem wolnej monady (lub, dokładniej, czymś o tym samym kształcie, co drzewo sekwencji konstruktorów dla fa). Darmowe monady to przydatna technika do konstruowania elastycznych monad przy minimalnej ilości płyty kotłowej. W programie Haskell mógłbym użyć wolnych monad do zdefiniowania prostych monad do „programowania systemu na wysokim poziomie”, aby pomóc w utrzymaniu bezpieczeństwa typów (używam tylko typów i ich deklaracji. Implementacje są proste przy użyciu kombinatorów):

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

Monadyzm jest architekturą leżącą u podstaw wzorca, który można by nazwać „tłumaczem” lub „dowództwem”, wyabstrahowanym do najczystszej postaci, ponieważ każde obliczenie monadyczne musi być „uruchomione”, przynajmniej trywialnie. (System wykonawczy uruchamia dla nas monadę IO i jest punktem wejścia do dowolnego programu Haskell. IO „steruje” resztą obliczeń, uruchamiając kolejno akcje IO).

Typ złączenia polega również na tym, że otrzymujemy stwierdzenie, że monada jest monoidem w kategorii endofunkcji. Łączenie jest zazwyczaj ważniejsze ze względów teoretycznych ze względu na swój typ. Ale zrozumienie tego typu oznacza zrozumienie monad. Złączenia i typy złącz transformatora monad są efektywnie kompozycjami endofunkcji w sensie kompozycji funkcji. Aby umieścić go w pseudojęzycznym języku podobnym do Haskella,

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


3

Monada to tablica funkcji

(Pst: tablica funkcji to tylko obliczenie).

W rzeczywistości zamiast prawdziwej tablicy (jedna funkcja w jednej tablicy komórkowej) masz te funkcje powiązane inną funkcją >> =. >> = pozwala dostosować wyniki z funkcji i do karmienia funkcji i + 1, wykonać obliczenia między nimi, a nawet nie wywoływać funkcji i + 1.

Zastosowane tutaj typy to „typy z kontekstem”. Jest to wartość z „znacznikiem”. Łańcuchowe funkcje muszą przyjąć „nagą wartość” i zwrócić oznaczony wynik. Jednym z obowiązków >> = jest wydobycie nagiej wartości z kontekstu. Istnieje również funkcja „return”, która przyjmuje pustą wartość i umieszcza ją w tagu.

Przykład z Może . Użyjmy go do zapisania prostej liczby całkowitej, na której dokonamy obliczeń.

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

Aby pokazać, że monady to tablica funkcji z operacjami pomocniczymi, rozważ odpowiednik powyższego przykładu, używając tylko prawdziwej tablicy funkcji

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 byłby używany w następujący sposób:

print (runMyMonad (Just 160) myArray1)

1
Super schludnie! Więc bind jest tylko sposobem na ocenę tablicy funkcji z kontekstem, w sekwencji, na danych wejściowych z kontekstem :)
Musa Al-hassy

>>=jest operatorem
10.01.2016

1
Myślę, że analogia „tablicy funkcji” niewiele wyjaśnia. Jeśli \x -> x >>= k >>= l >>= mjest tablicą funkcji, to znaczy h . g . f, że w ogóle nie dotyczy to monad.
duplode,

moglibyśmy powiedzieć, że funktory , monadyczne, aplikacyjne lub zwykłe, dotyczą „upiększonej aplikacji” . „aplikacyjny” dodaje łańcuch, a „monada” dodaje zależność (tj. tworzenie następnego kroku obliczeniowego w zależności od wyników z poprzedniego kroku obliczeniowego).
Czy Ness

3

Pod względem OO monada to płynny pojemnik.

Minimalne wymaganie to definicja, class <A> Somethingktóra obsługuje konstruktor Something(A a)i co najmniej jedną metodęSomething<B> flatMap(Function<A, Something<B>>)

Prawdopodobnie ma to również znaczenie, jeśli klasa monad ma jakieś metody z podpisem, Something<B> work()które zachowują reguły klasy - kompilator piecze w flatMapie podczas kompilacji.

Dlaczego monada jest przydatna? Ponieważ jest to kontener, który umożliwia operacje łańcuchowe, które zachowują semantykę. Na przykład, Optional<?>zachowuje semantykę isPresent dla Optional<String>, Optional<Integer>, Optional<MyClass>, itd.

Jako szorstki przykład

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

Uwaga: zaczynamy od łańcucha, a kończymy na liczbie całkowitej. Całkiem fajne.

W OO może to zająć trochę machania ręką, ale każda metoda na czymś, co zwraca inną podklasę czegoś, spełnia kryterium funkcji kontenera, która zwraca kontener oryginalnego typu.

W ten sposób zachowujesz semantykę - tzn. Znaczenie kontenera i operacje się nie zmieniają, po prostu zawijają i ulepszają obiekt wewnątrz kontenera.


2

Monady w typowym użyciu są funkcjonalnym odpowiednikiem mechanizmów obsługi wyjątków programowania proceduralnego.

W nowoczesnych językach proceduralnych program obsługi wyjątków umieszcza się wokół sekwencji instrukcji, z których każda może generować wyjątek. Jeśli którakolwiek z instrukcji zgłasza wyjątek, normalne wykonanie sekwencji instrukcji zostaje zatrzymane i przekazane do procedury obsługi wyjątku.

Funkcjonalne języki programowania jednak filozoficznie unikają funkcji obsługi wyjątków ze względu na ich charakter „goto”. Perspektywa programowania funkcjonalnego polega na tym, że funkcje nie powinny mieć „skutków ubocznych”, takich jak wyjątki, które zakłócają przebieg programu.

W rzeczywistości efektów ubocznych nie można wykluczyć w świecie rzeczywistym przede wszystkim ze względu na operacje wejścia / wyjścia. Monady w programowaniu funkcjonalnym służą do obsługi tego przez przyjęcie zestawu powiązanych funkcji (każde z nich może dać nieoczekiwany wynik) i przekształcenie dowolnego nieoczekiwanego wyniku w hermetyzowane dane, które nadal mogą bezpiecznie przepływać przez pozostałe wywołania funkcji.

Przepływ kontroli jest zachowany, ale nieoczekiwane zdarzenie jest bezpiecznie obudowane i obsługiwane.


2

Proste wyjaśnienie Monad z studium przypadku Marvela znajduje się tutaj .

Monady to abstrakcje używane do skutecznego działania funkcji zależnych od sekwencji. Skuteczne tutaj oznacza, że ​​zwracają typ w postaci F [A], na przykład Opcja [A], gdzie Opcja to F, zwany konstruktorem typu. Zobaczmy to w 2 prostych krokach

  1. Poniżej skład funkcji jest przechodni. Aby przejść od A do CI, można skomponować A => B i B => C.
 A => C   =   A => B  andThen  B => C

wprowadź opis zdjęcia tutaj

  1. Jeśli jednak funkcja zwraca typ efektu, taki jak Opcja [A], tj. A => F [B], kompozycja nie działa, aby przejść do B, potrzebujemy A => B, ale mamy A => F [B].
    wprowadź opis zdjęcia tutaj

    Potrzebujemy specjalnego operatora „bind”, który wie, jak połączyć te funkcje, które zwracają F [A].

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

„Przypisane” funkcja jest określona w odniesieniu do konkretnego F .

Istnieje również „zwrotny” , typu A => F [W], dla każdego A , zdefiniowanym w odniesieniu do konkretnego F także. Aby być Monadą, F musi mieć zdefiniowane dwie funkcje.

Zatem możemy zbudować skuteczną funkcję A => F [B] z dowolnej czystej funkcji A => B ,

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

ale dany F może również definiować własne nieprzezroczyste „wbudowane” funkcje specjalne tego typu, których użytkownik nie może sam zdefiniować (w czystym języku), jak

  • „random” ( Range => Random [Int] )
  • „print” ( String => IO [()] )
  • „spróbuj ... złapać” itp.

2

Dzielę się swoim rozumieniem Monad, które mogą nie być teoretycznie doskonałe. Monady dotyczą propagacji kontekstu . Monada polega na tym, że definiujesz kontekst dla niektórych danych (lub typów danych), a następnie definiujesz sposób, w jaki kontekst będzie przenoszony z danymi przez cały proces przetwarzania. Definiowanie propagacji kontekstu polega przede wszystkim na definiowaniu sposobu łączenia wielu kontekstów (tego samego typu). Korzystanie z Monad oznacza również, że konteksty te nie zostaną przypadkowo usunięte z danych. Z drugiej strony inne dane bezkontekstowe można przenieść do nowego lub istniejącego kontekstu. Następnie można zastosować tę prostą koncepcję, aby zapewnić poprawność czasu kompilacji programu.



1

Zobacz moją odpowiedź na „Co to jest monada?”

Zaczyna się od motywującego przykładu, przechodzi przez przykład, czerpie przykład monady i formalnie definiuje „monadę”.

Nie zakłada znajomości programowania funkcjonalnego i używa pseudokodu ze function(argument) := expressionskładnią z najprostszymi możliwymi wyrażeniami.

Ten program C ++ jest implementacją monady pseudokodu. (Dla porównania: Mjest konstruktorem typu, feedjest operacją „powiązania” i operacją wrap„powrotu”).

#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

Z praktycznego punktu widzenia (podsumowując to, co zostało powiedziane w wielu poprzednich odpowiedziach i powiązanych artykułach) wydaje mi się, że jednym z podstawowych „celów” (lub użyteczności) monady jest wykorzystanie zależności ukrytych w wywołaniach metod rekurencyjnych aka skład funkcji (tj. gdy f1 wywołuje f2 wywołuje f3, f3 należy ocenić przed f2 przed f1), aby w naturalny sposób reprezentować skład sekwencyjny, szczególnie w kontekście leniwego modelu oceny (to znaczy składu sekwencyjnego jako prostej sekwencji , np. „f3 (); f2 (); f1 ();” w C - sztuczka jest szczególnie oczywista, jeśli pomyślisz o przypadku, w którym f3, f2 i f1 faktycznie nic nie zwracają [ich łączenie jako f1 (f2 (f3)) jest sztuczny, służy wyłącznie tworzeniu sekwencji]).

Jest to szczególnie istotne, gdy występują skutki uboczne, tj. Gdy jakiś stan jest zmieniony (jeśli f1, f2, f3 nie miały skutków ubocznych, nie miałoby znaczenia, w jakiej kolejności są oceniane; co jest wielką właściwością czystego języki funkcjonalne, na przykład w celu zrównoleglenia tych obliczeń). Im bardziej czyste funkcje, tym lepiej.

Myślę, że z tego wąskiego punktu widzenia monady mogłyby być postrzegane jako cukier składniowy dla języków, które preferują leniwe ocenianie (oceniają rzeczy tylko wtedy, gdy jest to absolutnie konieczne, zgodnie z kolejnością, która nie opiera się na prezentacji kodu) i które nie mają inne sposoby reprezentowania sekwencyjnego składu. W efekcie sekcje kodu, które są „nieczyste” (tj. Mają skutki uboczne), mogą być prezentowane w sposób naturalny, w trybie rozkazującym, ale są wyraźnie oddzielone od czystych funkcji (bez skutków ubocznych), które mogą być ocenione leniwie.

Jest to jednak tylko jeden aspekt, jak tutaj ostrzegano .


0

Najprostszym wyjaśnieniem, jakie mogę wymyślić, jest to, że monady są sposobem na komponowanie funkcji z upiększonymi rezultatami (inaczej kompozycja Kleisli). Funkcja „embelished” ma podpis a -> (b, smth)gdzie ai bsą typy (myślę Int, Bool), które mogą różnić się od siebie, ale niekoniecznie - i smthjest „kontekst” lub „embelishment”.

Ten typ funkcji można również zapisać a -> m btam, gdzie mjest on równoważny „upiększeniu” smth. Są to zatem funkcje zwracające wartości w kontekście (pomyśl funkcje rejestrujące swoje działania, gdzie smthjest komunikat rejestrowania; lub funkcje wykonujące dane wejściowe / wyjściowe, a ich wyniki zależą od wyniku operacji IO).

Monada to interfejs („typeclass”), który sprawia, że ​​implementator mówi mu, jak tworzyć takie funkcje. Implementator musi zdefiniować funkcję kompozycji (a -> m b) -> (b -> m c) -> (a -> m c)dla każdego typu, mktóry chce zaimplementować interfejs (jest to kompozycja Kleisli).

Jeśli więc powiemy, że mamy typ krotki (Int, String)reprezentujący wyniki obliczeń na Ints, które również rejestrują ich działania, przy (_, String)czym jest to „upiększenie” - dziennik akcji - i dwie funkcje increment :: Int -> (Int, String)i twoTimes :: Int -> (Int, String)chcemy uzyskać funkcję, incrementThenDouble :: Int -> (Int, String)która jest kompozycją z dwóch funkcji, które również uwzględniają dzienniki.

W podanym przykładzie implementacja monadowa dwóch funkcji dotyczy wartości całkowitej 2 incrementThenDouble 2(która jest równa twoTimes (increment 2)) zwróciłaby (6, " Adding 1. Doubling 3.")wyniki pośrednie increment 2równe (3, " Adding 1.")i twoTimes 3równe(6, " Doubling 3.")

Z tej funkcji kompozycji Kleisli można wyprowadzić zwykłe funkcje monadyczne.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.