W języku C #, co to jest monada?

190

Obecnie dużo mówi się o monadach. Przeczytałem kilka artykułów / postów na blogu, ale nie mogę posunąć się za daleko z ich przykładami, aby w pełni zrozumieć tę koncepcję. Powodem jest to, że monady są pojęciem języka funkcjonalnego, a zatem przykłady są w językach, z którymi nie pracowałem (ponieważ nie użyłem języka funkcjonalnego dogłębnie). Nie mogę zrozumieć składni na tyle głęboko, aby w pełni śledzić artykuły ... ale mogę powiedzieć, że jest tam coś, co warto zrozumieć.

Znam jednak C # całkiem dobrze, w tym wyrażenia lambda i inne funkcje funkcjonalne. Wiem, że C # ma tylko podzbiór cech funkcjonalnych, więc może monady nie mogą być wyrażone w C #.

Czy na pewno można jednak przekazać tę koncepcję? Przynajmniej mam taką nadzieję. Być może można przedstawić przykład C # jako fundament, a następnie opisać to, co programista C # by życzyć mógł zrobić stamtąd, ale nie może, ponieważ język pozbawiony cech funkcjonalnych programowania. Byłoby to fantastyczne, ponieważ oddawałoby intencje i zalety monad. Oto moje pytanie: jakie jest najlepsze wytłumaczenie dla monad dla programisty C # 3?

Dzięki!

(EDYCJA: Nawiasem mówiąc, wiem, że są już co najmniej 3 pytania „co to jest monada” na SO. Jednak mam z nimi ten sam problem ... więc to pytanie jest potrzebne imo, ze względu na programistę C # skup się. Dzięki.)

Charlie Flowers
źródło
Należy pamiętać, że w rzeczywistości jest to programista C # 3.0. Nie pomyl go z .NET 3.5. Poza tym miłe pytanie.
Razzie
4
Warto zauważyć, że wyrażenia zapytania LINQ są przykładem zachowania monadycznego w C # 3.
Erik Forbes
1
Nadal uważam, że to duplikat pytania. Jedna z odpowiedzi w stackoverflow.com/questions/2366/can-anyone-explain-monads link do channel9vip.orcsweb.com/shows/Going+Deep/… , gdzie jeden z komentarzy ma bardzo ładny przykład w języku C #. :)
lipiec
4
To tylko jeden link z jednej odpowiedzi na jedno z pytań SO. Widzę wartość w pytaniu skierowanym do programistów C #. Jest to coś, o co zapytałbym funkcjonalnego programistę, który kiedyś robił C #, gdybym go znał, więc rozsądne wydaje się pytanie w SO. Ale szanuję również twoje prawo do twojej opinii.
Charlie Flowers
1
Czy jednak nie wystarczy jedna odpowiedź? ;) Chodzi mi o to, że jedno z pozostałych pytań (a teraz także to, więc tak) miało odpowiedź w języku C # (która wydaje się naprawdę dobrze napisana. Prawdopodobnie najlepsze wyjaśnienie, jakie widziałem)
jalf

Odpowiedzi:

147

Większość tego, co robisz w programowaniu przez cały dzień, to łączenie niektórych funkcji razem, aby tworzyć z nich większe funkcje. Zwykle masz nie tylko funkcje w zestawie narzędzi, ale także inne rzeczy, takie jak operatory, przypisania zmiennych i tym podobne, ale ogólnie twój program łączy wiele „obliczeń” z większymi obliczeniami, które zostaną połączone razem.

Monada to jakiś sposób na wykonanie tego „łączenia obliczeń”.

Zwykle twoim najbardziej podstawowym „operatorem” do łączenia dwóch obliczeń jest ; :

a; b

Kiedy to mówisz, masz na myśli „najpierw zrób a, a potem zróbb ”. Rezultatem a; bjest po prostu ponownie obliczenie, które można łączyć z wieloma innymi rzeczami. Jest to prosta monada, jest to sposób na połączenie małych obliczeń z większymi. ;Mówi „zrobić coś po lewej stronie, a następnie zrobić coś po prawej stronie”.

Inną rzeczą, którą można postrzegać jako monadę w językach obiektowych, jest .. Często znajdziesz takie rzeczy:

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

W .zasadzie oznacza „ocenia obliczeń po lewej stronie, a następnie wywołać metodę z prawej strony na skutek, że”. Jest to inny sposób łączenia funkcji / obliczeń razem, nieco bardziej skomplikowany niż; . Koncepcja łączenia łańcuchów razem .jest monadą, ponieważ jest to sposób na połączenie dwóch obliczeń razem w nowe obliczenie.

Inną dość powszechną monadą, która nie ma specjalnej składni, jest ten wzorzec:

rv = socket.bind(address, port);
if (rv == -1)
  return -1;

rv = socket.connect(...);
if (rv == -1)
  return -1;

rv = socket.send(...);
if (rv == -1)
  return -1;

Zwracana wartość -1 oznacza błąd, ale nie ma prawdziwego sposobu na usunięcie tego sprawdzania błędów, nawet jeśli masz wiele wywołań API, które musisz połączyć w ten sposób. Jest to w zasadzie kolejna monada, która łączy wywołania funkcji według reguły „jeśli funkcja po lewej zwróciła -1, sami zwróć -1, w przeciwnym razie wywołaj funkcję po prawej”. Gdybyśmy mieli operatora, >>=który to zrobił, moglibyśmy po prostu napisać:

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

Sprawiłoby, że rzeczy byłyby bardziej czytelne i pomogłyby wyodrębnić nasz specjalny sposób łączenia funkcji, abyśmy nie musieli powtarzać się od nowa.

Istnieje wiele innych sposobów łączenia funkcji / obliczeń, które są użyteczne jako ogólny wzorzec i mogą być wyodrębnione w monadzie, umożliwiając użytkownikowi monady napisanie znacznie bardziej zwięzłego i przejrzystego kodu, ponieważ cała księgowość i zarządzanie używane funkcje są wykonywane w monadzie.

Na przykład powyższe >>=można rozszerzyć na „sprawdzanie błędów, a następnie wywoływanie prawej strony gniazda, które otrzymaliśmy jako dane wejściowe”, dzięki czemu nie musimy jawnie określać socketwiele razy:

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

Definicja formalna jest nieco bardziej skomplikowana, ponieważ musisz się martwić, jak uzyskać wynik jednej funkcji jako danych wejściowych do następnej, jeśli ta funkcja wymaga tego wejścia i ponieważ chcesz się upewnić, że połączone funkcje pasują do sposób, w jaki próbujesz połączyć je w swojej monadzie. Ale podstawowa koncepcja polega na sformalizowaniu różnych sposobów łączenia funkcji.

coś
źródło
28
Świetna odpowiedź! Zamieszczę cytat Olivera Steele'a, próbując powiązać Monady z przeciążeniem operatora à la C ++ lub C #: Monady pozwalają na przeciążenie ';' operator.
Jörg W Mittag
6
@ JörgWMittag Czytałem już ten cytat, ale zabrzmiało to jak przesadnie nonsens. Teraz, gdy rozumiem monady i czytam to wyjaśnienie, jak „;” jest jeden, rozumiem Ale myślę, że to naprawdę irracjonalne stwierdzenie dla większości bezwzględnych programistów. „;” nie jest postrzegany jako operator więcej niż // dla większości.
Jimmy Hoffa
2
Czy na pewno wiesz, co to jest monada? Monady nie są „funkcją” ani obliczeniami, istnieją reguły dla monad.
Luis
W twoim ;przykładzie: jakie obiekty / typy danych są ;mapowane? (Pomyśl Listmapy Tdo List<T>) Jak ;map morfizmów / funkcje między obiektami / typów danych? Co to jest pure, join, bindza ;?
Micha Wiedenmann
44

Minął rok odkąd opublikowałem to pytanie. Po opublikowaniu, zagłębiałem się w Haskell przez kilka miesięcy. Bardzo mi się podobało, ale odłożyłem go na bok, gdy byłem gotowy zagłębić się w Monady. Wróciłem do pracy i skupiłem się na technologiach wymaganych przez mój projekt.

Ostatniej nocy przyszedłem i ponownie przeczytałem te odpowiedzi. Co najważniejsze , ponownie przeczytałem konkretny przykład C # w komentarzach tekstowych filmu Briana Beckmana , o którym ktoś wspomniał powyżej . To było tak całkowicie jasne i pouczające, że postanowiłem opublikować go bezpośrednio tutaj.

Ze względu na ten komentarz, nie tylko czuję się jak rozumiem dokładnie co Monady są ... Zdaję sobie sprawę, jakie faktycznie napisał kilka rzeczy w C #, które Monadami… lub przynajmniej bardzo blisko, i staram się rozwiązać te same problemy.

Tak więc, oto komentarz - to wszystko jest bezpośredni cytat z komentarza tutaj przez Sylvan :

To jest całkiem niezłe. Jest to jednak trochę abstrakcyjne. Mogę sobie wyobrazić ludzi, którzy nie wiedzą, jakie monady są już zdezorientowane z powodu braku prawdziwych przykładów.

Więc pozwólcie, że spróbuję się zastosować, i żeby być naprawdę jasnym, zrobię przykład w języku C #, nawet jeśli będzie on wyglądał brzydko. Na końcu dodam równoważny Haskell i pokażę ci fajny cukier syntaktyczny Haskell, w którym, IMO, monady naprawdę zaczynają się przydać.

Okej, więc jedna z najłatwiejszych Monad nazywa się w Haskell „Może monada”. W języku C # wywoływany jest typ MożeNullable<T> . Zasadniczo jest to niewielka klasa, która po prostu zawiera pojęcie wartości, która jest albo poprawna i ma wartość, albo jest „zerowa” i nie ma żadnej wartości.

Przydatną rzeczą, którą można włożyć w monadę do łączenia wartości tego typu, jest pojęcie niepowodzenia. Tzn. Chcemy być w stanie spojrzeć na wiele wartości zerowalnych i powrócić, nullgdy tylko jedna z nich będzie pusta. Może to być przydatne, jeśli na przykład wyszukujesz wiele kluczy w słowniku lub coś, a na końcu chcesz przetworzyć wszystkie wyniki i jakoś je połączyć, ale jeśli któregokolwiek z kluczy nie ma w słowniku, chcesz wrócić nullpo całość. Byłoby żmudne, aby ręcznie sprawdzać każde wyszukiwanie nulli zwracać, abyśmy mogli ukryć to sprawdzanie w operatorze powiązania (co jest swego rodzaju monadą, ukrywamy księgowanie w operatorze powiązania, co ułatwia kodowanie używać, ponieważ możemy zapomnieć o szczegółach).

Oto program, który motywuje to wszystko (zdefiniuję Bindpóźniej, aby pokazać, dlaczego jest fajny).

 class Program
    {
        static Nullable<int> f(){ return 4; }        
        static Nullable<int> g(){ return 7; }
        static Nullable<int> h(){ return 9; }


        static void Main(string[] args)
        {
            Nullable<int> z = 
                        f().Bind( fval => 
                            g().Bind( gval => 
                                h().Bind( hval =>
                                    new Nullable<int>( fval + gval + hval ))));

            Console.WriteLine(
                    "z = {0}", z.HasValue ? z.Value.ToString() : "null" );
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey();
        }
    }

Teraz zignoruj ​​przez chwilę, że jest już wsparcie dla robienia tego Nullablew C # (możesz dodać razem wartości zerowe int i otrzymasz null, jeśli którykolwiek jest null). Udawajmy, że nie ma takiej funkcji, a jest to tylko klasa zdefiniowana przez użytkownika bez specjalnej magii. Chodzi o to, że możemy użyć Bindfunkcji, aby powiązać zmienną z zawartością naszej Nullablewartości, a następnie udawać, że nie dzieje się nic dziwnego, i użyć ich jak normalnych liczb całkowitych i po prostu dodać je razem. Mamy owinąć wynik w Nullable na końcu, a pustych albo będzie zerowa (jeśli którykolwiek z f, glub hzwraca null), albo będzie to wynik sumowania f, gorazhrazem. (jest to analogiczne do tego, w jaki sposób możemy powiązać wiersz w bazie danych ze zmienną w LINQ i robić to z nim, bezpiecznie wiedząc, że Bindoperator upewni się, że zmienna będzie zawsze przekazywać tylko prawidłowe wartości wierszy).

Można bawić się z tego i zmienić dowolne f, gi hpowrócić nieważną i widać, że cała sprawa zwróci null.

Jasne jest więc, że operator powiązania musi to dla nas sprawdzić i wydzwonić zwracając wartość null, jeśli napotka wartość null, i w przeciwnym razie przekaże wartość wewnątrz Nullablestruktury do lambda.

Oto Bindoperator:

public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f ) 
    where B : struct 
    where A : struct
{
    return a.HasValue ? f(a.Value) : null;
}

Typy tutaj są takie same jak na filmie. Pobiera M a ( Nullable<A>w tym przypadku w składni C #) i funkcję od ado M b( Func<A, Nullable<B>>w składni w C #) i zwraca an M b ( Nullable<B>).

Kod po prostu sprawdza, czy nullable zawiera wartość, a jeśli tak, to ją wyodrębnia i przekazuje do funkcji, w przeciwnym razie po prostu zwraca null. Oznacza to, że Bindoperator zajmie się całą logiką sprawdzania wartości zerowej. Jeśli i tylko jeśli wartość, którą wzywamy, Bindjest różna od null, wówczas wartość ta zostanie „przekazana” do funkcji lambda, w przeciwnym razie wyskoczymy wcześniej, a całe wyrażenie jest puste. Pozwala to kod, który piszemy używając monady być całkowicie wolne od tego pustego sprawdzania zachowania, po prostu używać Bindi uzyskać zmienną związaną z wartością wewnętrzną wartością monadycznej ( fval, gvala hvalw przykładowym kodzie) i możemy z nich korzystać bezpiecznie w wiedzy, Bindktóra zajmie się sprawdzeniem ich pod kątem zerowości przed przekazaniem ich dalej.

Są inne przykłady rzeczy, które możesz zrobić z monadą. Na przykład możesz zmusić Bindoperatora do zadbania o strumień wejściowy znaków i użyć go do napisania kombinacji parserów. Każdy kombinator parserów może wtedy być całkowicie nieświadomy takich rzeczy, jak śledzenie wstecz, awarie parsera itp., I po prostu łączyć mniejsze parsery razem, tak jakby nic nigdy nie poszło źle, wiedząc, że sprytna implementacja Bindrozwiązuje całą logikę stojącą za trudne kawałki. Potem może ktoś dodaje rejestrowanie do monady, ale kod używający monady nie zmienia się, ponieważ cała magia dzieje się w definicji Bindoperatora, reszta kodu pozostaje niezmieniona.

Wreszcie, oto implementacja tego samego kodu w Haskell ( -- rozpoczyna wiersz komentarza).

-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a

-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x

-- the "unit", called "return"
return = Just

-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
     g >>= ( \gval ->  
     h >>= ( \hval -> return (fval+gval+hval ) ) ) )

-- The following is exactly the same as the three lines above
z2 = do 
   fval <- f
   gval <- g
   hval <- h
   return (fval+gval+hval)

Jak widać ładna donotacja na końcu sprawia, że ​​wygląda jak prosty kod rozkazujący. I rzeczywiście jest to zgodne z projektem. Monady mogą być używane do enkapsulacji wszystkich użytecznych rzeczy w programowaniu imperatywnym (stan mutable, IO itp.) I używane przy użyciu tej ładnej składni podobnej do imperatywnej, ale za zasłonami są to tylko monady i sprytna implementacja operatora bind! Fajne jest to, że możesz implementować własne monady, implementując >>=i return. Jeśli to zrobisz, te monady również będą mogły korzystać z donotacji, co oznacza, że ​​możesz w zasadzie pisać własne małe języki, po prostu definiując dwie funkcje!

Charlie Flowers
źródło
3
Osobiście wolę wersję F # monady, ale w obu przypadkach są niesamowite.
ChaosPandion
3
Dziękujemy za powrót tutaj i aktualizację posta. Są to następujące działania, które pomagają programistom, którzy szukają określonego obszaru, naprawdę rozumieją, w jaki sposób inni programiści ostatecznie uważają ten obszar, zamiast po prostu mieć „jak mam zrobić X w swojej technologii”. Ty stary!
kappasims
Podążyłem tą samą ścieżką, którą podjąłeś i doszedłem do tego samego miejsca, rozumiejąc monady, co mówi, że jest to najlepsze wytłumaczenie wiążącego zachowania monady, jakie kiedykolwiek widziałem dla bezwzględnego dewelopera. Chociaż myślę, że nie do końca dotykasz wszystkiego na temat monad, co jest nieco bardziej wyjaśnione powyżej przez sth.
Jimmy Hoffa
@ Jimmy Hoffa - bez wątpienia masz rację. Myślę, że naprawdę je rozumiem głębiej, najlepszym sposobem jest zacząć z nich dużo korzystać i zdobyć doświadczenie . Nie miałem jeszcze takiej okazji, ale mam nadzieję, że wkrótce.
Charlie Flowers
Wydaje się, że monada to po prostu wyższy poziom abstrakcji w zakresie programowania, lub jest to ciągła i niezróżnicowana definicja funkcji w matematyce. Tak czy inaczej, nie są nową koncepcją, szczególnie w matematyce.
liang
11

Monada jest zasadniczo odroczonym przetwarzaniem. Jeśli próbujesz napisać kod, który ma skutki uboczne (np. I / O) w języku, który im na to nie pozwala i pozwala tylko na czyste obliczenia, jednym uchyleniem jest powiedzenie: „Ok, wiem, że nie zrobisz efektów ubocznych dla mnie, ale czy możesz obliczyć, co by się stało, gdybyś zrobił? ”

To trochę oszustwo.

To wyjaśnienie pomoże ci zrozumieć ogólny obraz monad, ale diabeł tkwi w szczegółach. Jak dokładnie należy obliczyć konsekwencje? Czasami to nie jest ładne.

Najlepszym sposobem przedstawienia przeglądu tego, jak ktoś przyzwyczaił się do programowania imperatywnego, jest powiedzenie, że umieszcza cię w DSL, w którym operacje wyglądające syntaktycznie jak to, do czego przywykłeś poza monadą, jest używane zamiast do zbudowania funkcji, która by działała co chcesz, jeśli możesz (na przykład) zapisać plik wyjściowy. Prawie (ale nie tak naprawdę), jakbyś budował kod w ciągu, aby później go sprawdzić.

MarkusQ
źródło
1
Jak w książce I Robot? Gdzie naukowiec prosi komputer, aby obliczył podróże kosmiczne i prosi go o pominięcie pewnych zasad? :) :) :) :)
OscarRyz
3
Hmm, Monady można używać do odroczonego przetwarzania i do enkapsulacji funkcji wywołujących efekty uboczne. Rzeczywiście była to pierwsza prawdziwa aplikacja w Haskell, ale w rzeczywistości jest to o wiele bardziej ogólny wzorzec. Inne typowe zastosowania obejmują obsługę błędów i zarządzanie stanem. Cukier syntaktyczny (wykonaj w Haskell, wyrażenia obliczeniowe w F #, składnia Linq w C #) jest po prostu tym i fundamentalny dla Monad jako takich.
Mike Hadlow,
@MikeHadlow: Instancje monad do obsługi błędów ( Maybei Either e) oraz zarządzania stanem ( State s, ST s) wydają mi się szczególnymi przypadkami „Proszę obliczyć, co by się stało, gdybyś zrobił [efekty uboczne dla mnie]”. Innym przykładem może być niedeterminizm ( []).
pyon
to jest dokładnie to prawo; z jednym (no, dwoma) dodatkami, że jest to E DSL, tj. osadzony DSL, ponieważ każda „monadyczna” wartość jest prawidłową wartością samego „czystego” języka, oznaczającą potencjalnie nieczyste „obliczenia”. Co więcej, monadyczny konstrukt „bind” istnieje w twoim czystym języku, który pozwala ci łączyć łańcuchy czystych konstruktorów o takich wartościach, z których każda zostanie wywołana z wynikiem z poprzedniego obliczenia, gdy całe połączone obliczenie zostanie „uruchomione”. Oznacza to, że mamy możliwość rozgałęzienia się na przyszłych wynikach (lub w każdym przypadku osobnej osi czasu „uruchomienia”).
Czy Ness
ale dla programisty oznacza to, że możemy programować w EDSL, mieszając go z czystymi obliczeniami naszego czystego języka. stos kanapek wielowarstwowych to wielowarstwowa kanapka. to takie proste.
Czy Ness
4

Jestem pewien, że inni użytkownicy opublikują dogłębnie, ale uznałem ten film za pomocny, ale powiem, że nadal nie jestem zbyt biegły w koncepcji, żebym mógł (lub powinien) zacząć rozwiązywać problemy intuicyjnie z Monadami.

TheMissingLINQ
źródło
1
Jeszcze bardziej pomocny był komentarz zawierający przykład C # pod filmem.
lipiec
Nie wiem, czy jest bardziej pomocny, ale z pewnością wprowadził pomysły w życie.
TheMissingLINQ
0

Możesz myśleć o monadzie jako języku C #, interfacektóry klasy muszą zaimplementować . Jest to pragmatyczna odpowiedź, która ignoruje całą teoretyczną matematykę stojącą za tym, dlaczego chcesz mieć te deklaracje w swoim interfejsie i ignoruje wszystkie powody, dla których chcesz mieć monady w języku, który próbuje uniknąć skutków ubocznych, ale uważam, że to dobry początek, ponieważ ktoś, kto rozumie interfejsy (C #).

hao
źródło
Czy możesz rozwinąć? Co takiego jest w interfejsie, który łączy go z monadami?
Joel Coehoorn
2
Myślę, że na blogu znajduje się kilka akapitów poświęconych temu pytaniu.
hao
0

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 w C # jest implementacją monady pseudokodu. (Dla porównania: Mjest konstruktorem typu, feedjest operacją „powiązania” i operacją wrap„powrotu”).

using System.IO;
using System;

class Program
{
    public class M<A>
    {
        public A val;
        public string messages;
    }

    public static M<B> feed<A, B>(Func<A, M<B>> f, M<A> x)
    {
        M<B> m = f(x.val);
        m.messages = x.messages + m.messages;
        return m;
    }

    public static M<A> wrap<A>(A x)
    {
        M<A> m = new M<A>();
        m.val = x;
        m.messages = "";
        return m;
    }

    public class T {};
    public class U {};
    public class V {};

    public static M<U> g(V x)
    {
        M<U> m = new M<U>();
        m.messages = "called g.\n";
        return m;
    }

    public static M<T> f(U x)
    {
        M<T> m = new M<T>();
        m.messages = "called f.\n";
        return m;
    }

    static void Main()
    {
        V x = new V();
        M<T> m = feed<U, T>(f, feed(g, wrap<V>(x)));
        Console.Write(m.messages);
    }
}
Jordania
źródło