Co to jest monada?

1414

Po krótkim spojrzeniu na Haskella, jakie byłoby krótkie, zwięzłe i praktyczne wyjaśnienie, czym właściwie jest monada?

Odkryłem, że większość wyjaśnień jest dość niedostępna i pozbawiona praktycznych szczegółów.

Peter Mortensen
źródło
12
Eric Lippert napisał odpowiedź na te pytania ( stackoverflow.com/questions/2704652/... ), która z powodu pewnych problemów żyje na osobnej stronie.
P Shved
70
Oto nowe wprowadzenie z użyciem javascript - uważam, że jest bardzo czytelne.
Benjol
7
Zobacz także Różne sposoby wyświetlania monady .
Petr Pudlák
20
Zobacz także Monady na zdjęciach
cibercitizen1
2
Monada to tablica funkcji z operacjami pomocniczymi. Zobacz tę odpowiedź
cibercitizen1

Odpowiedzi:

1059

Po pierwsze: termin monada jest trochę niepotrzebny, jeśli nie jesteś matematykiem. Alternatywnym terminem jest konstruktor obliczeń, który jest nieco bardziej opisowy w stosunku do tego, do czego są faktycznie przydatni.

Pytasz o praktyczne przykłady:

Przykład 1: Zrozumienie listy :

[x*2 | x<-[1..10], odd x]

To wyrażenie zwraca podwojenie wszystkich liczb nieparzystych w zakresie od 1 do 10. Bardzo przydatne!

Okazuje się, że tak naprawdę jest to po prostu cukier składniowy dla niektórych operacji w monadzie List. To samo rozumienie listy można zapisać jako:

do
   x <- [1..10]
   guard (odd x)
   return (x * 2)

Lub nawet:

[1..10] >>= (\x -> guard (odd x) >> return (x*2))

Przykład 2: Wejście / Wyjście :

do
   putStrLn "What is your name?"
   name <- getLine
   putStrLn ("Welcome, " ++ name ++ "!")

Oba przykłady używają monad, konstruktorów obliczeń AKA. Częstym tematem jest to, że monada łączy operacje w określony, użyteczny sposób. W rozumieniu listy operacje są powiązane w taki sposób, że jeśli operacja zwraca listę, wówczas dla każdej pozycji na liście wykonywane są następujące operacje . Z drugiej strony monada IO wykonuje operacje sekwencyjnie, ale przekazuje „ukrytą zmienną”, która reprezentuje „stan świata”, co pozwala nam pisać kod I / O w sposób czysto funkcjonalny.

Okazuje się, że schemat operacji łańcuchowych jest dość przydatny i jest używany do wielu różnych rzeczy w Haskell.

Innym przykładem są wyjątki: przy użyciu Errormonady operacje są powiązane w taki sposób, że są wykonywane sekwencyjnie, z wyjątkiem sytuacji, gdy zostanie zgłoszony błąd, w którym to przypadku reszta łańcucha zostanie porzucona.

Zarówno składnia ze zrozumieniem listy, jak i notacja są składniowym cukrem do operacji łączenia za pomocą >>=operatora. Monada jest w zasadzie tylko typem obsługującym >>=operatora.

Przykład 3: Analizator składni

Jest to bardzo prosty parser, który analizuje albo cytowany ciąg, albo liczbę:

parseExpr = parseString <|> parseNumber

parseString = do
        char '"'
        x <- many (noneOf "\"")
        char '"'
        return (StringValue x)

parseNumber = do
    num <- many1 digit
    return (NumberValue (read num))

Operacje char, digititp są dość proste. One pasują albo nie pasują. Magią jest monada, która zarządza przepływem sterowania: operacje są wykonywane sekwencyjnie, dopóki dopasowanie się nie powiedzie, w którym to przypadku monada cofa się do najnowszej <|>i próbuje następnej opcji. Ponownie, sposób łączenia operacji z pewną dodatkową, przydatną semantyką.

Przykład 4: Programowanie asynchroniczne

Powyższe przykłady są w Haskell, ale okazuje się, że F # obsługuje również monady. Ten przykład został skradziony z Don Syme :

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

Ta metoda pobiera stronę internetową. Używana jest linia dziurkowania GetResponseAsync- faktycznie czeka ona na odpowiedź w oddzielnym wątku, podczas gdy główny wątek wraca z funkcji. Ostatnie trzy wiersze są wykonywane w odrodzonym wątku po otrzymaniu odpowiedzi.

W większości innych języków musisz jawnie utworzyć osobną funkcję dla linii obsługujących odpowiedź. asyncMonada jest w stanie „Split” bloku na własną rękę i odroczyć wykonanie drugiej połowie. ( async {}Składnia wskazuje, że przepływ kontrolny w bloku jest definiowany przez asyncmonadę.)

Jak oni pracują

Jak więc monada może robić te wszystkie wymyślne rzeczy z kontrolą przepływu? To, co faktycznie dzieje się w bloku do (lub wyrażeniu obliczeniowym, jak są one wywoływane w F #), polega na tym, że każda operacja (w zasadzie każda linia) jest zawinięta w osobną anonimową funkcję. Funkcje te są następnie łączone za pomocą bindoperatora (pisane >>=w języku Haskell). Ponieważ bindoperacja łączy funkcje, może wykonywać je według własnego uznania: sekwencyjnie, wielokrotnie, odwrotnie, odrzucać niektóre, wykonywać niektóre w osobnym wątku, gdy ma na to ochotę i tak dalej.

Na przykład jest to rozszerzona wersja kodu IO z przykładu 2:

putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))

To brzydsze, ale bardziej oczywiste, co się właściwie dzieje. >>=Operator jest magiczny składnik: Potrzeba wartość (po lewej stronie) i łączy go z funkcji (po prawej stronie), aby wytworzyć nową wartość. Ta nowa wartość jest następnie przejmowana przez następnego >>=operatora i ponownie łączona z funkcją tworzenia nowej wartości. >>=może być postrzegany jako mini-ewaluator.

Zauważ, że >>=jest przeciążony dla różnych typów, więc każda monada ma własną implementację >>=. (Jednak wszystkie operacje w łańcuchu muszą być tego samego rodzaju monady, w przeciwnym razie >>=operator nie będzie działał).

Najprostsza możliwa implementacja po >>=prostu bierze wartość po lewej stronie i stosuje ją do funkcji po prawej stronie i zwraca wynik, ale jak powiedziano wcześniej, to, co sprawia, że ​​cały wzór jest użyteczny, to gdy dzieje się coś dodatkowego w implementacji monady >>=.

Istnieje pewna sprytność w przekazywaniu wartości z jednej operacji do drugiej, ale wymaga to głębszego wyjaśnienia systemu typu Haskell.

Podsumowując

W terminologii Haskell monada to sparametryzowany typ, który jest instancją klasy typu Monad, która definiuje >>=wraz z kilkoma innymi operatorami. Mówiąc ogólnie, monada jest po prostu typem, dla którego >>=zdefiniowano operację.

Sam w sobie >>=jest po prostu nieporęcznym sposobem łączenia łańcuchów, ale przy obecności notacji do, która ukrywa „hydraulikę”, operacje monadyczne okazują się bardzo ładną i przydatną abstrakcją, przydatną w wielu miejscach w języku i użyteczną do tworzenia własnych mini-języków w języku.

Dlaczego monady są trudne?

Dla wielu uczniów Haskell monady są przeszkodą, którą uderzają jak ceglany mur. Nie chodzi o to, że same monady są złożone, ale że implementacja opiera się na wielu innych zaawansowanych funkcjach Haskell, takich jak typy sparametryzowane, klasy typów i tak dalej. Problem polega na tym, że we / wy Haskell oparte są na monadach, a we / wy jest prawdopodobnie jedną z pierwszych rzeczy, które chcesz zrozumieć, ucząc się nowego języka - w końcu tworzenie programów, które nie produkują żadnych programów wynik. Nie mam bezpośredniego rozwiązania tego problemu z kurczakiem i jajkami, z wyjątkiem traktowania we / wy „magii dzieje się tutaj”, dopóki nie będziesz mieć wystarczającego doświadczenia z innymi częściami języka. Przepraszam.

Doskonały blog na monadach: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

JacquesB
źródło
65
Jako ktoś, kto miał wiele problemów ze zrozumieniem monad, mogę powiedzieć, że ta odpowiedź pomogła… trochę. Jednak wciąż są pewne rzeczy, których nie rozumiem. W jaki sposób rozumienie listy jest monadą? Czy istnieje rozszerzona forma tego przykładu? Kolejną rzeczą, która naprawdę przeszkadza mi w większości objaśnień dotyczących monad, w tym także w tym - Czy ciągle mieszają „co to jest monada”? z „do czego służy monada?” oraz „Jak wdrażana jest monada?”. przeskoczyłeś tego rekina, kiedy napisałeś: „Monada jest w zasadzie tylko typem obsługującym operator >> =”. Który właśnie mnie miał ...
Breton
82
Nie zgadzam się również z twoim wnioskiem na temat tego, dlaczego monady są trudne. Jeśli same monady nie są skomplikowane, powinieneś być w stanie wyjaśnić, czym są bez dużej ilości bagażu. Nie chcę wiedzieć o implementacji, kiedy zadaję pytanie „Co to jest monada”, chcę wiedzieć, co to znaczy świąd. Jak dotąd wydaje się, że odpowiedź brzmi: „Ponieważ autorzy Haskell są sadomasochistami i postanowili, że powinieneś zrobić coś głupio złożonego, aby osiągnąć proste rzeczy, więc musisz nauczyć się monad, jak używać haskell, nie dlatego, że są w jakikolwiek sposób przydatne w same "...
Breton
69
Ale ... to nie może być prawda, prawda? Myślę, że monady są trudne, ponieważ nikt nie może wymyślić, jak je wyjaśnić, nie wpadając w mylące szczegóły implementacji. Mam na myśli .. co to jest autobus szkolny? To metalowa platforma z urządzeniem z przodu, które zużywa rafinowany produkt naftowy do napędzania w cyklu metalowych tłoków, które z kolei obracają wał korbowy przymocowany do niektórych kół zębatych napędzających niektóre koła. Koła mają nadmuchane gumowe torby wokół nich, które stykają się z powierzchnią asfaltu, aby spowodować przesunięcie się siedzeń do przodu. Siedzenia przesuwają się do przodu, ponieważ ...
Breton,
130
Przeczytałem to wszystko i nadal nie wiem, co to jest monada, poza tym, że jest to coś, czego programiści Haskell nie rozumieją wystarczająco dobrze, aby to wyjaśnić. Przykłady niewiele pomagają, biorąc pod uwagę, że to wszystko, co można zrobić bez monad, a ta odpowiedź nie wyjaśnia, w jaki sposób monady sprawiają, że są łatwiejsze, tylko bardziej mylące. Jedną z części tej odpowiedzi, która była prawie użyteczna, było usunięcie cukru syntaktycznego z przykładu # 2. Mówię, że zbliżyło się, ponieważ oprócz pierwszego wiersza rozszerzenie nie ma żadnego rzeczywistego podobieństwa do oryginału.
Laurence Gonsalves,
81
Innym problemem, który wydaje się być endemiczny dla wyjaśnień monad, jest to, że jest napisany w języku Haskell. Nie twierdzę, że Haskell to zły język - mówię, że to zły język do wyjaśniania monad. Gdybym wiedział, że Haskell już rozumiem monady, więc jeśli chcesz wyjaśnić monady, zacznij od języka, który ludzie, którzy nie znają monad, są w stanie zrozumieć. Jeśli musisz użyć Haskell, w ogóle nie używaj cukru syntaktycznego - użyj najmniejszego, najprostszego podzbioru języka, jaki możesz, i nie zakładaj zrozumienia Haskell IO.
Laurence Gonsalves,
712

Wyjaśnienie „czym jest monada” jest trochę jak powiedzenie „czym jest liczba?” Cały czas używamy liczb. Ale wyobraź sobie, że spotkałeś kogoś, kto nic nie wiedział o liczbach. Jak, do diabła , wyjaśniłbyś, jakie są liczby? Jak w ogóle zacząłbyś opisywać, dlaczego to może być przydatne?

Co to jest monada? Krótka odpowiedź: to specyficzny sposób łączenia operacji w łańcuch.

Zasadniczo piszesz kroki wykonywania i łączysz je razem z „funkcją wiązania”. (W Haskell ma nazwę >>=.) Możesz napisać wywołania do operatora powiązania lub użyć cukru składniowego, który zmusi kompilator do wstawienia tych wywołań funkcji. Ale tak czy inaczej, każdy krok jest oddzielony wywołaniem tej funkcji wiązania.

Zatem funkcja wiązania jest jak średnik; oddziela kroki w procesie. Zadaniem funkcji wiązania jest pobranie danych wyjściowych z poprzedniego kroku i przekazanie ich do następnego kroku.

To nie brzmi zbyt mocno, prawda? Ale istnieje więcej niż jeden rodzaj monady. Dlaczego? W jaki sposób?

Cóż, funkcja bind może po prostu pobrać wynik z jednego kroku i podać go do następnego. Ale jeśli to „wszystko”, co robi monada… to nie jest zbyt przydatne. I to ważne, aby zrozumieć: każda przydatna monada oprócz bycia monadą robi coś jeszcze . Każda przydatna monada ma „specjalną moc”, co czyni ją wyjątkową.

(Monada, która nie robi nic specjalnego, nazywa się „monadą tożsamości”. Raczej jak funkcja tożsamości, brzmi to jak zupełnie bezcelowa rzecz, ale okazuje się, że nie jest… Ale to inna historia ™.)

Zasadniczo każda monada ma własną implementację funkcji wiązania. I możesz napisać funkcję powiązania, która będzie wykonywać kopie rzeczy między krokami wykonania. Na przykład:

  • Jeśli każdy krok zwraca wskaźnik powodzenia / niepowodzenia, można zlecić wykonanie następnego kroku tylko wtedy, gdy poprzedni się powiódł. W ten sposób nieudany krok przerywa całą sekwencję „automatycznie”, bez żadnego testowania warunkowego. ( Monada awarii ).

  • Rozszerzając ten pomysł, możesz wdrożyć „wyjątki”. ( Monada błędu lub monada wyjątku .) Ponieważ definiujesz je samodzielnie, a nie jako funkcję języka, możesz zdefiniować sposób ich działania. (Np. Być może chcesz zignorować dwa pierwsze wyjątki i przerwać tylko po zgłoszeniu trzeciego wyjątku).

  • Możesz sprawić, że każdy krok zwróci wiele wyników , i będziesz mieć nad nimi pętlę funkcji wiązania, podając każdy z nich do następnego kroku. W ten sposób nie musisz ciągle pisać pętli wszędzie, gdy masz do czynienia z wieloma wynikami. Funkcja wiązania „automatycznie” robi to wszystko za Ciebie. (The List Monad .)

  • Oprócz przekazywania „wyniku” z jednego kroku do drugiego, funkcja powiązania może również przekazywać dodatkowe dane . Te dane nie są teraz wyświetlane w kodzie źródłowym, ale nadal można uzyskać do nich dostęp z dowolnego miejsca, bez konieczności ręcznego przekazywania ich do każdej funkcji. (The Reader Monad .)

  • Możesz to zrobić, aby „dodatkowe dane” mogły zostać zastąpione. Umożliwia to symulowanie destrukcyjnych aktualizacji bez faktycznego wykonywania destrukcyjnych aktualizacji. ( State Monad i jego kuzyn Writer Monad .)

  • Ponieważ symulujesz tylko destrukcyjne aktualizacje, możesz w trywialny sposób robić rzeczy, które byłyby niemożliwe przy prawdziwych aktualizacjach destrukcyjnych. Na przykład możesz cofnąć ostatnią aktualizację lub przywrócić starszą wersję .

  • Możesz stworzyć monadę, w której obliczenia mogą zostać wstrzymane , aby można było wstrzymać program, wejść i majstrować przy wewnętrznych danych stanu, a następnie wznowić.

  • Możesz zaimplementować „kontynuacje” jako monadę. To pozwala złamać ludzkie umysły!

Wszystko to i więcej jest możliwe w przypadku monad. Oczywiście wszystko to jest również całkowicie możliwe bez monad. Korzystanie z monad jest po prostu znacznie łatwiejsze .

MathematicalOrchid
źródło
13
Doceniam twoją odpowiedź - zwłaszcza ostatnią koncesję, że wszystko to jest oczywiście możliwe również bez monad. Należy podkreślić, że w przypadku monad jest to w większości łatwiejsze, ale często nie jest tak wydajne, jak bez nich. Gdy trzeba zaangażować transformatory, dodatkowe warstwowanie wywołań funkcji (i utworzonych obiektów funkcyjnych) wiąże się z trudnym do zauważenia i kontrolowania kosztem, który jest niewidoczny dzięki sprytnej składni.
seh
1
Przynajmniej w Haskell większość optymizmu monad zostaje zniesiona przez optymalizator. Zatem jedynym prawdziwym „kosztem” jest wymagana moc mózgu. (Nie jest to bez znaczenia, jeśli zależy ci na „ łatwości konserwacji”). Zwykle jednak monady ułatwiają , a nie utrudniają. (W przeciwnym razie, dlaczego miałbyś zawracać sobie głowę?)
MathematicalOrchid
Nie jestem pewien, czy Haskell to obsługuje, ale matematycznie możesz zdefiniować monadę w kategoriach >> = i zwrócić lub dołączyć i ap. >> = i powrót są tym, co sprawia, że ​​monady są praktycznie przydatne, ale łączenie i ap dają bardziej intuicyjne zrozumienie, czym jest monada.
Jeremy List
15
Ta pochodząca z nie matematyki, niefunkcjonalnego środowiska programistycznego, ta odpowiedź była dla mnie najbardziej sensowna.
jrahhali
10
To jest pierwsza odpowiedź, która faktycznie dała mi pojęcie o tym, czym do diabła jest monada. Dziękujemy za znalezienie sposobu, aby to wyjaśnić!
robotmay
186

W rzeczywistości, w przeciwieństwie do powszechnego zrozumienia Monad, nie mają one nic wspólnego ze stanem. Monady są po prostu sposobem na zawijanie rzeczy i zapewniają metody wykonywania operacji na opakowanych rzeczach bez ich rozpakowywania.

Na przykład możesz utworzyć typ, aby zawinąć inny, w Haskell:

data Wrapped a = Wrap a

Aby zawinąć rzeczy, które definiujemy

return :: a -> Wrapped a
return x = Wrap x

Aby wykonać operacje bez rozpakowywania, powiedzmy, że masz funkcję f :: a -> b, możesz to zrobić, aby podnieść tę funkcję i działać na opakowanych wartościach:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

To wszystko, co trzeba zrozumieć. Okazuje się jednak, że istnieje bardziej ogólna funkcja wykonywania tego podnoszenia , a mianowicie bind:

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bindmoże zrobić trochę więcej fmap, ale nie odwrotnie. W rzeczywistości fmapmożna go zdefiniować tylko w kategoriach bindi return. Definiując monadę, podajesz jej typ (tutaj był Wrapped a), a następnie mówisz, jak działa returni jak binddziała.

Fajne jest to, że okazuje się, że jest to tak ogólny wzorzec, że wyskakuje w każdym miejscu, enkapsulacja stanu w czysty sposób jest tylko jednym z nich.

Dobry artykuł na temat tego, jak można wykorzystać monady do wprowadzenia zależności funkcjonalnych, a tym samym kontrolować kolejność oceny, tak jak w przypadku monady IO Haskella, zajrzyj do IO Inside .

Jeśli chodzi o rozumienie monad, nie przejmuj się tym zbytnio. Przeczytaj o nich to, co uważasz za interesujące i nie martw się, jeśli nie zrozumiesz od razu. W takim razie wystarczy nurkować w języku takim jak Haskell. Monady to jedna z tych rzeczy, w których praktyka przenika do twojego mózgu. Pewnego dnia nagle zdajesz sobie sprawę, że je rozumiesz.

Arnar
źródło
-> jest aplikacją funkcji dublującej po prawej stronie, która ma lewą asocjację, więc pominięcie nawiasów nie ma tutaj znaczenia.
Matthias Benkard,
1
Nie wydaje mi się, żeby to było bardzo dobre wytłumaczenie. Monady są po prostu sposobem? dobrze, w którą stronę? Dlaczego nie miałbym enkapsulować za pomocą klasy zamiast monady?
Breton
4
@ mb21: Jeśli tylko wskazujesz, że jest zbyt wiele nawiasów, zwróć uwagę, że a-> b-> c w rzeczywistości jest tylko skrótem od a -> (b-> c). Zapisanie tego szczególnego przykładu jako (a -> b) -> (Ta -> Tb) jest ściśle mówiąc po prostu dodaniem niepotrzebnych znaków, ale moralnie „jest to właściwe”, ponieważ podkreśla, że ​​fmap odwzorowuje funkcję typu a -> b do funkcji typu Ta -> Tb. I pierwotnie to właśnie funktory robią w teorii kategorii i stąd pochodzą monady.
Nikolaj-K
1
Ta odpowiedź jest myląca. Niektóre monady w ogóle nie mają „opakowania”, takie funkcje mają stałą wartość.
1
@ DanMandel Monady to wzorce projektowe, które dostarczają własne opakowanie typu danych. Monady są zaprojektowane w sposób abstrakcyjny kod bojlera. Więc kiedy wywołujesz Monadę w swoim kodzie, robi to za kulisami rzeczy, o które nie chcesz się martwić. Pomyśl o Nullable <T> lub IEnumerable <T>, co robią za kulisami? Thad Monad.
sksallaj
168

Ale mógłbyś wymyślić Monady!

sigfpe mówi:

Ale wszystkie te wprowadzają monady jako coś ezoterycznego, wymagającego wyjaśnienia. Ale chcę argumentować, że wcale nie są ezoteryczne. W rzeczywistości, mając do czynienia z różnymi problemami w programowaniu funkcjonalnym, nieuchronnie doprowadzono by Cię do pewnych rozwiązań, z których wszystkie są przykładami monad. Mam nadzieję, że uda ci się je teraz wymyślić, jeśli jeszcze tego nie zrobiłeś. Jest to zatem mały krok, aby zauważyć, że wszystkie te rozwiązania są w rzeczywistości tym samym rozwiązaniem w przebraniu. Po przeczytaniu tego możesz być w stanie lepiej zrozumieć inne dokumenty dotyczące monad, ponieważ rozpoznasz wszystko, co widzisz, jako coś, co już wymyśliłeś.

Wiele problemów, które monady próbują rozwiązać, wiąże się z kwestią skutków ubocznych. Zaczniemy od nich. (Zauważ, że monady pozwalają robić więcej niż radzenie sobie ze skutkami ubocznymi, w szczególności wiele typów obiektów kontenerowych może być postrzeganych jako monady. Niektóre wprowadzenie do monad sprawiają, że trudno jest pogodzić te dwa różne zastosowania monad i skoncentrować się na jednym lub inny.)

W imperatywnym języku programowania, takim jak C ++, funkcje nie zachowują się tak, jak funkcje matematyki. Załóżmy na przykład, że mamy funkcję C ++, która pobiera pojedynczy argument zmiennoprzecinkowy i zwraca wynik zmiennoprzecinkowy. Pozornie może to wyglądać trochę jak funkcja matematyczna odwzorowująca reale na reale, ale funkcja C ++ może zrobić coś więcej niż tylko zwrócić liczbę zależną od jej argumentów. Może odczytywać i zapisywać wartości zmiennych globalnych, a także zapisywać dane wyjściowe na ekranie i odbierać dane wejściowe od użytkownika. Jednak w czysto funkcjonalnym języku funkcja może odczytywać tylko to, co jest do niej dostarczone w swoich argumentach, a jedynym sposobem, w jaki może wpływać na świat, są wartości, które zwraca.

nlucaroni
źródło
9
… Najlepszy sposób nie tylko w Internecie, ale wszędzie. (Oryginalny artykuł Wadlera Monady do programowania funkcjonalnego, o którym wspomniałem w mojej odpowiedzi poniżej, jest również dobry). Żaden z zylionów samouczków analogicznie się nie zbliża.
ShreevatsaR
13
To tłumaczenie JavaScript posta Sigfpe to nowy najlepszy sposób na naukę monad dla osób, które jeszcze nie znają zaawansowanego Haskella!
Sam Watkins,
1
W ten sposób dowiedziałem się, czym jest monada. Przeprowadzenie czytelnika przez proces wynalezienia koncepcji jest często najlepszym sposobem na jej nauczenie.
Jordan
Jednak funkcja akceptująca obiekt ekranowy jako argument i zwracająca jego kopię ze zmodyfikowanym tekstem byłaby czysta.
Dmitri Zaitsev,
87

Monada to typ danych, który ma dwie operacje: >>=(aka bind) i return(aka unit). returnprzyjmuje dowolną wartość i tworzy z nią instancję monady. >>=pobiera instancję monady i odwzorowuje na niej funkcję. (Widać już, że monada jest dziwnym rodzajem typu danych, ponieważ w większości języków programowania nie można napisać funkcji, która przyjmuje dowolną wartość i tworzy z niej typ. Monady wykorzystują rodzaj polimorfizmu parametrycznego .)

W notacji Haskell zapisany jest interfejs monady

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

Operacje te mają być zgodne z pewnymi „prawami”, ale nie jest to niezwykle ważne: „prawa” tylko kodyfikują sposób, w jaki powinny zachowywać się rozsądne implementacje operacji (w zasadzie to >>=i returnpowinny zgadzać się co do tego, w jaki sposób wartości przekształcają się w instancje monad i to >>=jest skojarzenie).

Monady to nie tylko stan i operacje we / wy: wyodrębniają one wspólny wzorzec obliczeń obejmujący pracę ze stanem, operacje we / wy, wyjątki i niedeterminizm. Prawdopodobnie najprostszymi monadami do zrozumienia są listy i typy opcji:

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

gdzie []i :są konstruktorami listy, ++jest operatorem konkatenacji Justi NothingMaybekonstruktorami. Obie te monady zawierają typowe i użyteczne wzorce obliczeń dla swoich odpowiednich typów danych (zauważ, że żadne z nich nie ma nic wspólnego z efektami ubocznymi ani operacjami wejścia / wyjścia).

Naprawdę musisz się bawić, pisząc jakiś nietrywialny kod Haskella, aby docenić, o czym są monady i dlaczego są one przydatne.

Chris Conway
źródło
Co dokładnie rozumiesz przez „mapowanie funkcji nad nim”?
Casebash
Casebash, jestem celowo nieformalny we wstępie. Zobacz przykłady na końcu, aby dowiedzieć się, co pociąga za sobą „mapowanie funkcji”.
Chris Conway
3
Monada nie jest typem danych. Jest to zasada komponowania funkcji: stackoverflow.com/a/37345315/1614973
Dmitri Zaitsev
@DmitriZaitsev ma rację, Monady faktycznie zapewniają własny typ danych, Monady nie są typami danych
sksallaj
78

Najpierw powinieneś zrozumieć, czym jest funktor. Przedtem poznaj funkcje wyższego rzędu.

Funkcja wyższego rzędu jest po prostu funkcją, która przyjmuje funkcję jako argument.

Funktor jakikolwiek typ budowy T, dla których istnieje funkcję wyższego rzędu nazwać map, który przekształca funkcją typu a -> b(biorąc pod uwagę dowolne dwa typy aa b) do funkcji T a -> T b. Ta mapfunkcja musi także być zgodna z prawami tożsamości i składu, tak aby następujące wyrażenia zwróciły prawdę dla wszystkich pi q(notacja Haskella):

map id = id
map (p . q) = map p . map q

Na przykład konstruktor typu o nazwie Listjest funktorem, jeśli jest wyposażony w funkcję typu, (a -> b) -> List a -> List bktóra jest zgodna z powyższymi prawami. Jedyne praktyczne wdrożenie jest oczywiste. Wynikowa List a -> List bfunkcja iteruje po podanej liście, wywołując (a -> b)funkcję dla każdego elementu i zwraca listę wyników.

Monada jest zasadniczo tylko funktor Tz dwóch dodatkowych metod join, typu T (T a) -> T ai unit(czasami nazywane return, forklub pure) typu a -> T a. W przypadku list w Haskell:

join :: [[a]] -> [a]
pure :: a -> [a]

Dlaczego to jest przydatne? Ponieważ możesz na przykład mapnad listą z funkcją, która zwraca listę. Joinpobiera wynikową listę list i łączy je. Listjest monadą, ponieważ jest to możliwe.

mapNastępnie możesz napisać funkcję, która działa join. Ta funkcja jest nazywana bind, lub flatMap, lub (>>=), lub (=<<). Tak zwykle podaje się instancję monady w Haskell.

Monada musi spełniać określone prawa, a mianowicie joinmuszą być asocjacyjne. Oznacza to, że jeśli mają wartość xtypu [[[a]]]następnie join (join x)powinna być równa join (map join x). I puremusi być tożsamość za jointakie, że join (pure x) == x.

Apocalisp
źródło
3
niewielki dodatek do definicji „funkcji wyższego rzędu”: mogą przyjmować funkcje LUB POWRÓT. Właśnie dlatego są „wyższe”, ponieważ robią rzeczy ze sobą.
Kevin Won
9
Według tej definicji dodawanie jest funkcją wyższego rzędu. Pobiera liczbę i zwraca funkcję, która dodaje tę liczbę do drugiej. Więc nie, funkcje wyższego rzędu są funkcjami ściśle, których domena składa się z funkcji.
Apocalisp
Wideo „ Brian Beckman: Nie bój się Monady ” ma tę samą logikę.
icc97 10.10.16
48

[Uwaga: Nadal próbuję w pełni wykorzystać monady. Oto, co do tej pory zrozumiałem. Jeśli to źle, mam nadzieję, że ktoś kompetentny zadzwoni do mnie na dywan.]

Arnar napisał:

Monady są po prostu sposobem na zawijanie rzeczy i zapewniają metody wykonywania operacji na opakowanych rzeczach bez ich rozpakowywania.

Właśnie o to chodzi. Pomysł wygląda następująco:

  1. Bierzesz jakąś wartość i zawijasz ją dodatkowymi informacjami. Podobnie jak wartość jest pewnego rodzaju (np. Liczba całkowita lub ciąg znaków), więc dodatkowe informacje są pewnego rodzaju.

    Na przykład, ta dodatkowa informacja może być a Maybelub an IO.

  2. Następnie masz operatorów, którzy pozwalają ci operować na opakowanych danych, przenosząc te dodatkowe informacje. Operatorzy ci wykorzystują dodatkowe informacje, aby zdecydować, jak zmienić zachowanie operacji na opakowanej wartości.

    Np. A Maybe Intmoże być Just Intlub Nothing. Teraz, jeśli dodasz a Maybe Intdo Maybe Int, operator sprawdzi, czy oba są w Just Intśrodku, a jeśli tak, rozpakuje Ints, przekaże im operator dodawania, ponownie zapakuje wynik Intw nowy Just Int(który jest prawidłowy Maybe Int), a zatem zwróci a Maybe Int. Ale jeśli jeden z nich był w Nothingśrodku, operator natychmiast po prostu wróci Nothing, co znowu jest ważne Maybe Int. W ten sposób możesz udawać, że twoje Maybe Intsą zwykłymi liczbami i wykonywać na nich regularną matematykę. Jeśli miałbyś dostać a Nothing, twoje równania nadal będą dawały właściwy wynik - bez konieczności zaśmiecania czeków na Nothingwszystko .

Ale przykładem jest właśnie to, co się dzieje Maybe. Jeśli dodatkowa informacja była IO, to ten operator specjalny został zdefiniowany dlaIO s byłby wywoływany i mógłby zrobić coś zupełnie innego przed wykonaniem dodawania. (OK, dodanie dwóch IO Ints razem jest prawdopodobnie nonsensowne - nie jestem jeszcze pewien.) (Ponadto, jeśli zwróciłeś uwagę na Maybeprzykład, zauważyłeś, że „zawijanie wartości dodatkowymi rzeczami” nie zawsze jest poprawne. Ale to trudne by być dokładnym, poprawnym i precyzyjnym bez bycia nieprzeniknionym).

Zasadniczo „monada” z grubsza oznacza „wzór” . Ale zamiast książki pełnej nieformalnie wyjaśnionych i specjalnie nazwanych Wzorów, masz teraz konstrukcję językową - składnię i wszystko - która pozwala zadeklarować nowe wzorce jako rzeczy w twoim programie . (Niedokładność polega na tym, że wszystkie wzory muszą przybierać określoną formę, więc monada nie jest tak ogólna jak wzór. Myślę jednak, że jest to najbliższy termin, który większość ludzi zna i rozumie.)

I dlatego ludzie uważają monady za mylące: ponieważ są tak ogólną koncepcją. Pytanie o to, co czyni coś monadą, jest równie niejasne, co pytanie o to, co czyni coś wzorem.

Ale pomyśl o konsekwencjach posiadania wsparcia syntaktycznego w języku dla idei wzorca: zamiast czytać książkę Gang of Four i zapamiętywać budowę konkretnego wzorca, po prostu piszesz kod, który implementuje ten wzorzec w agnostyce, ogólny sposób raz, a potem gotowe! Następnie możesz ponownie użyć tego wzorca, takiego jak Odwiedzający, Strategia, Fasada lub cokolwiek innego, po prostu dekorując nim operacje w kodzie, bez konieczności jego ponownego wdrażania!

Dlatego ludzie, którzy rozumieją monady, uważają je za tak przydatne : to nie jest jakaś koncepcja wieży z kości słoniowej, z której snobowie intelektualni są dumni ze zrozumienia (OK, to też oczywiście teehee), ale w rzeczywistości upraszcza kod.

Arystoteles Pagaltzis
źródło
12
Czasami wyjaśnienie „ucznia” (takiego jak ty) jest bardziej odpowiednie dla innego ucznia niż wyjaśnienie eksperta. Uczniowie myślą podobnie :)
Adrian
To, co czyni coś monadą, to istnienie funkcji z typem M (M a) -> M a. To, że możesz zmienić to w jedno z typów, M a -> (a -> M b) -> M bczyni je przydatnymi.
Jeremy List,
„monada” z grubsza oznacza „wzór” ... nie.
Dziękuję
44

Po wielu wysiłkach myślę, że w końcu rozumiem monadę. Po ponownym przeczytaniu mojej długiej krytyki przytłaczającej większości głosowanych odpowiedzi przedstawię to wyjaśnienie.

Aby zrozumieć monady, należy odpowiedzieć na trzy pytania:

  1. Dlaczego potrzebujesz monady?
  2. Co to jest monada?
  3. Jak wdrażana jest monada?

Jak zauważyłem w moich oryginalnych komentarzach, zbyt wiele objaśnień monad zostało uwikłanych w pytanie nr 3, bez, a wcześniej naprawdę odpowiednio obejmujące pytanie 2 lub pytanie 1.

Dlaczego potrzebujesz monady?

Czyste języki funkcjonalne, takie jak Haskell, różnią się od języków imperatywnych, takich jak C lub Java, ponieważ czysty program funkcjonalny niekoniecznie jest wykonywany w określonej kolejności, krok po kroku. Program Haskell jest bardziej podobny do funkcji matematycznej, w której możesz rozwiązać „równanie” w dowolnej liczbie potencjalnych rzędów. Daje to szereg korzyści, między innymi eliminuje możliwość wystąpienia pewnych rodzajów błędów, szczególnie związanych z takimi rzeczami jak „stan”.

Istnieją jednak pewne problemy, które nie są tak łatwe do rozwiązania w przypadku tego stylu programowania. Niektóre rzeczy, takie jak programowanie konsoli i we / wy plików, wymagają, aby coś się wydarzyło w określonej kolejności lub muszą utrzymywać stan. Jednym ze sposobów rozwiązania tego problemu jest utworzenie rodzaju obiektu reprezentującego stan obliczeń oraz szereg funkcji, które przyjmują obiekt stanu jako dane wejściowe i zwracają nowy zmodyfikowany obiekt stanu.

Stwórzmy więc hipotetyczną wartość „stanu”, która reprezentuje stan ekranu konsoli. dokładne skonstruowanie tej wartości nie jest ważne, ale powiedzmy, że jest to tablica znaków ascii o długości bajtów, która reprezentuje to, co jest obecnie widoczne na ekranie, oraz tablica, która reprezentuje ostatni wiersz danych wejściowych wprowadzonych przez użytkownika, w pseudokodzie. Zdefiniowaliśmy niektóre funkcje, które przyjmują stan konsoli, modyfikują go i zwracają nowy stan konsoli.

consolestate MyConsole = new consolestate;

Aby więc programować konsolę, ale w czysto funkcjonalny sposób, trzeba zagnieżdżać w sobie wiele wywołań funkcji.

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

Programowanie w ten sposób zachowuje „czysty” styl funkcjonalny, a jednocześnie wymusza zmiany w konsoli w określonej kolejności. Ale prawdopodobnie będziemy chcieli wykonać więcej niż kilka operacji jednocześnie, jak w powyższym przykładzie. Zagnieżdżanie funkcji w ten sposób zacznie się niezręcznie. To, czego chcemy, to kod, który robi zasadniczo to samo, co powyżej, ale jest napisany nieco bardziej w następujący sposób:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

Byłby to rzeczywiście wygodniejszy sposób na napisanie tego. Jak to robimy?

Co to jest monada?

Gdy masz typ (taki jak consolestate), który definiujesz wraz z szeregiem funkcji zaprojektowanych specjalnie do działania na tym typie, możesz przekształcić cały pakiet tych rzeczy w „monadę”, definiując operator taki jak :(bind), który automatycznie podaje wartości zwracane po lewej stronie, do parametrów funkcji po prawej stronie, a liftoperator, który zmienia normalne funkcje, w funkcje, które działają z tym specyficznym rodzajem operatora powiązania.

Jak wdrażana jest monada?

Zobacz inne odpowiedzi, które wydają się dość swobodne, aby przejść do szczegółów tego.

bretoński
źródło
Sekwencjonowanie nie jest jedynym powodem do zdefiniowania monady. Monada to każdy funktor, który ma powiązanie i powrót. Bind i return zapewniają sekwencjonowanie. Ale dają też inne rzeczy. Zauważ też, że twoim ulubionym językiem imperatywnym jest fantazyjna monada we / wy z klasami OO. Ułatwienie definiowania monad oznacza, że ​​łatwo jest użyć wzorca interpretera - zdefiniuj dsl jako monadę i zinterpretuj go!
nomen
38

Po udzieleniu odpowiedzi na to pytanie kilka lat temu, uważam, że mogę poprawić i uprościć tę odpowiedź za pomocą ...

Monada to technika kompozycji funkcji, która uzewnętrznia leczenie niektórych scenariuszy wejściowych za pomocą funkcji komponowania bind, aby wstępnie przetworzyć dane wejściowe podczas kompozycji.

W normalnym składzie funkcja compose (>>)służy do zastosowania kolejno funkcji złożonej do wyniku jej poprzednika. Co ważne, tworzona funkcja jest wymagana do obsługi wszystkich scenariuszy jej wprowadzania.

(x -> y) >> (y -> z)

Ten projekt można ulepszyć poprzez restrukturyzację danych wejściowych, aby łatwiej było przesłuchać odpowiednie stany. Tak więc zamiast po prostu ywartość może stać się Mbtaka, jak na przykład, (is_OK, b)jeśli yzawiera pojęcie ważności.

Na przykład, gdy wejście jest możliwe tylko liczbą, zamiast wrócić ciąg, który może zawierać obowiązkowo zawierać liczbę lub nie, można zrestrukturyzować wpisać do boolwskazywania obecności ważnego numeru oraz liczbę w krotce takie jak, bool * float. Skomponowane funkcje nie będą już musiały analizować ciągu wejściowego, aby ustalić, czy istnieje liczba, ale mogłyby jedynie sprawdzić boolczęść krotki.

(Ma -> Mb) >> (Mb -> Mc)

Tutaj znowu kompozycja występuje naturalnie, composewięc każda funkcja musi obsługiwać wszystkie scenariusze danych wejściowych indywidualnie, chociaż teraz jest to o wiele łatwiejsze.

Co jednak, jeśli moglibyśmy zlecić przesłuchanie w tych czasach, w których obsługa scenariusza jest rutyną. Na przykład, co jeśli nasz program nic nie robi, gdy dane wejściowe są nieprawidłowe, tak jak w przypadku, gdy is_OKjest false. Gdyby tak było, wówczas złożone funkcje nie musiałyby same radzić sobie z tym scenariuszem, radykalnie upraszczając swój kod i powodując kolejny poziom ponownego użycia.

Aby osiągnąć tę eksternalizację, moglibyśmy użyć funkcji bind (>>=), aby wykonać compositionzamiast compose. Jako taki, zamiast po prostu przenosić wartości z wyjścia jednej funkcji na wejście innej Bind, sprawdzałby Mczęść Mai zdecydował, czy i jak zastosować złożoną funkcję do a. Oczywiście funkcja bindzostałaby zdefiniowana specjalnie dla naszego konkretnego M, aby móc sprawdzić jej strukturę i wykonać dowolny rodzaj aplikacji, jaki chcemy. Niemniej jednak, amoże to być cokolwiek, ponieważ bindpo prostu przekazuje aniezauważoną do złożonej funkcji, gdy określa ona konieczność zastosowania. Ponadto same złożone funkcje nie muszą już sobie z tym radzićM część struktury wejściowej, upraszczając je. W związku z tym...

(a -> Mb) >>= (b -> Mc) lub bardziej zwięźle Mb >>= (b -> Mc)

W skrócie, monada uzewnętrznia się, a tym samym zapewnia standardowe zachowanie wokół niektórych scenariuszy wejściowych, gdy dane wejściowe zostaną zaprojektowane w taki sposób, aby wystarczająco je ujawnić. Ten projekt jest shell and contentmodelem, w którym powłoka zawiera dane istotne dla zastosowania złożonej funkcji i jest odpytywana przez i pozostaje dostępna tylko dla tej bindfunkcji.

Dlatego monada to trzy rzeczy:

  1. Mshell dla gospodarstwa monada istotnych informacji,
  2. bindfunkcje realizowane w celu wykorzystania informacji powłoki w stosowaniu złożonych funkcji do wartości zawartości (S) nie znajdzie się w powłoce i
  3. funkcje kompozycyjne formularza, a -> Mbgenerujące wyniki zawierające monadyczne dane zarządzania.

Ogólnie rzecz biorąc, dane wejściowe do funkcji są znacznie bardziej restrykcyjne niż dane wyjściowe, które mogą obejmować takie rzeczy, jak warunki błędu; stąd Mbstruktura wynikowa jest na ogół bardzo przydatna. Na przykład operator dzielenia nie zwraca liczby, gdy dzielnik jest 0.

Dodatkowo, monads mogą obejmować funkcje zawijania, które zawijają wartości, aw typ monadyczny Ma, i funkcje ogólne a -> b, w funkcje monadyczne a -> Mb, poprzez zawijanie ich wyników po zastosowaniu. Oczywiście bind, takie funkcje zawijania są specyficzne dla M. Przykład:

let return a = [a]
let lift f a = return (f a)

Projekt bindfunkcji zakłada niezmienne struktury danych i czyste funkcje, inne rzeczy stają się złożone i nie można zagwarantować. Jako takie istnieją prawa monadyczne:

Dany...

M_ 
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)

Następnie...

Left Identity  : (return a) >>= f === f a
Right Identity : Ma >>= return    === Ma
Associative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativityoznacza, że bindzachowuje kolejność oceny bez względu na bindto, kiedy zostanie zastosowana. Oznacza to, że w definicji Associativitypowyżej, siły wczesnej oceny, czy w nawiasach bindingof fa gspowoduje tylko w funkcji, która oczekuje Maw celu zakończenia bind. Stąd ocena Mamusi zostać ustalona, ​​zanim jej wartość będzie mogła zostać zastosowana, fa wynik z kolei zastosowany do g.

George
źródło
„... ale mam nadzieję, że inni uznają to za przydatne” , rzeczywiście było to dla mnie przydatne, pomimo wszystkich podkreślonych zdań: D
To najbardziej zwięzłe i jasne wyjaśnienie monad, jakie kiedykolwiek czytałem / oglądałem / słyszałem. Dziękuję Ci!
James
Istnieje istotna różnica między Monadą a Monoidem. Monada to reguła „komponowania” funkcji między różnymi typami, więc nie tworzą one operacji binarnej wymaganej dla Monoidów, zobacz tutaj więcej szczegółów: stackoverflow.com/questions/2704652/...
Dmitri Zaitsev
Tak. Masz rację. Twój artykuł był nad moją głową :). Jednak uznałem to leczenie za bardzo pomocne (i dodałem je do mojego jako wskazówkę dla innych). Dzięki za heads-up: stackoverflow.com/a/7829607/1612190
George
2
Być może pomyliłeś teorię grup algebraicznych z teorią kategorii, skąd pochodzi Monada. Pierwsza to teoria grup algebraicznych, która nie jest ze sobą powiązana.
Dmitri Zaitsev
37

Monada jest w rzeczywistości formą „operatora typu”. Zrobi trzy rzeczy. Najpierw „zawinie” (lub w inny sposób przekształci) wartość jednego typu w inny typ (zwykle nazywany „typem monadycznym”). Po drugie, wszystkie operacje (lub funkcje) będą dostępne dla typu bazowego dostępnego dla typu monadycznego. Wreszcie zapewni wsparcie w łączeniu siebie z inną monadą w celu uzyskania monady złożonej.

„Może monada” jest zasadniczo odpowiednikiem „typów zerowalnych” w Visual Basic / C #. Pobiera niedopuszczalny typ „T” i przekształca go w „Nullable <T>”, a następnie określa, co oznaczają wszystkie operatory binarne w Nullable <T>.

Efekty uboczne są przedstawione podobnie. Tworzona jest struktura, która zawiera opisy efektów ubocznych obok wartości zwracanej przez funkcję. Operacje „podniesione” następnie kopiują efekty uboczne, gdy wartości są przekazywane między funkcjami.

Nazywa się je raczej „monadami” niż łatwiejszą do uchwycenia nazwą „operatorów typów” z kilku powodów:

  1. Monady mają ograniczenia co do tego, co mogą zrobić (szczegóły w definicji).
  2. Ograniczenia te, wraz z faktem, że w grę wchodzą trzy operacje, są zgodne ze strukturą czegoś zwanego monadą w teorii kategorii, która jest niejasną gałęzią matematyki.
  3. Zostały zaprojektowane przez zwolenników „czystych” języków funkcjonalnych
  4. Zwolennicy czysto funkcjonalnych języków, takich jak niejasne gałęzie matematyki
  5. Ponieważ matematyka jest niejasna, a monady kojarzone są z określonymi stylami programowania, ludzie używają słowa monad jako pewnego rodzaju tajnego uścisku dłoni. Z tego powodu nikt nie zadał sobie trudu, aby zainwestować w lepszą nazwę.
Scott Wisniewski
źródło
1
Monady nie zostały „zaprojektowane”, były stosowane z jednej dziedziny (teorii kategorii) do drugiej (we / wy w czysto funkcjonalnych językach programowania). Czy Newton „zaprojektował” rachunek różniczkowy?
Jared Updike,
1
Punkty 1 i 2 powyżej są poprawne i przydatne. Punkty 4 i 5 są swego rodzaju ad hominem, nawet jeśli mniej więcej są prawdziwe. Nie pomagają wyjaśnić monad.
Jared Updike,
13
Re: 4, 5: „Sekretnym uściskiem dłoni” jest czerwony śledź. Programowanie jest pełne żargonu. Haskell po prostu nazywa rzeczy takimi, jakimi są, bez udawania, że ​​coś odkrywa. Jeśli już istnieje w matematyce, po co wymyślać dla niego nową nazwę? Nazwa nie jest tak naprawdę powodem, dla którego ludzie nie dostają monad; są subtelną koncepcją. Przeciętny człowiek prawdopodobnie rozumie dodawanie i mnożenie, dlaczego nie rozumie koncepcji Grupy Abelowej? Ponieważ jest to bardziej abstrakcyjne i ogólne, a ta osoba nie wykonała pracy, aby owinąć głowę wokół koncepcji. Zmiana nazwy nie pomogłaby.
Jared Updike,
16
Westchnienie ... Nie atakuję Haskella ... Żartowałem. Więc tak naprawdę nie rozumiem bycia „ad hominem”. Tak, rachunek został „zaprojektowany”. Dlatego na przykład uczniowie rachunku różniczkowego i całkowego uczy się notacji Leibniza, a nie trudnych rzeczy, których używał Netwton. Lepszy projekt. Dobre imiona bardzo pomagają zrozumieć. Gdybym nazwał Grupy Abelowe „rozdętymi strąkami zmarszczek”, możesz mieć problem ze zrozumieniem mnie. Być może mówisz „ale to imię to nonsens”, nikt nigdy nie nazwałby ich tak. Dla ludzi, którzy nigdy nie słyszeli o teorii kategorii, „monada” brzmi jak nonsens.
Scott Wiśniewski
4
@Scott: Przepraszam, jeśli moje obszerne komentarze sprawiły, że wydawało mi się, że bronię się od Haskella. Podoba mi się twój humor na temat tajnego uścisku dłoni i zauważysz, że powiedziałem, że jest to mniej więcej prawda. :-) Gdybyście nazwali Grupy Abelowe „rozdętymi strąkami zmarszczek”, popełnilibyście ten sam błąd, próbując nadać monadom „lepszą nazwę” (por. F # „wyrażenia obliczeniowe”): termin istnieje i ludzie, którym zależy, wiedzą, jakie monady są, ale nie czym są „ciepłe, rozmyte rzeczy” (lub „wyrażenia obliczeniowe”). Jeśli dobrze rozumiem twoje użycie terminu „operator typu”, istnieje wiele innych operatorów typu niż monady.
Jared Updike,
35

(Zobacz także odpowiedzi w Co to jest monada? )

Dobrą motywacją do Monads jest You Could Had Invented Monads sigfpe (Dan Piponi) ! (I może już masz) . Istnieje wiele innych samouczków dotyczących monad , z których wiele mylnie próbuje wyjaśnić monady w „prostych terminach” przy użyciu różnych analogii: jest to błędny samouczek monady ; Unikaj ich.

Jak mówi DR MacIver w Powiedz nam, dlaczego twój język jest do kitu :

Rzeczy, których nienawidzę w Haskell:

Zacznijmy od oczywistości. Samouczki Monad. Nie, nie monady. W szczególności samouczki. Są nieskończone, przesadzone i drogi Boże, czy są nużące. Co więcej, nigdy nie widziałem żadnych przekonujących dowodów, że faktycznie pomagają. Przeczytaj definicję klasy, napisz kod, poznaj przerażającą nazwę.

Mówisz, że rozumiesz być może monadę? Dobrze, jesteś na dobrej drodze. Po prostu zacznij używać innych monad, a wcześniej czy później zrozumiesz, czym są monady.

[Jeśli jesteś zorientowany matematycznie, możesz zignorować dziesiątki samouczków i nauczyć się definicji lub postępować zgodnie z wykładami z teorii kategorii :) Główną częścią definicji jest to, że Monada M obejmuje „konstruktor typów”, który określa dla każdego istniejący typ „T” nowy typ „MT” oraz kilka sposobów przechodzenia między typami „zwykłymi” i typami „M”.]

Co zaskakujące, jednym z najlepszych wstępów do monad jest właściwie jedna z pierwszych prac naukowych przedstawiających monady, Monady Philipa Wadlera do programowania funkcjonalnego . W rzeczywistości ma praktyczne, nietrywialne motywujące przykłady, w przeciwieństwie do wielu sztucznych samouczków.

ShreevatsaR
źródło
2
Jedyny problem związany z pracą Wadlera polega na tym, że notacja jest inna, ale zgadzam się, że praca jest dość przekonująca i stanowi wyraźną, zwięzłą motywację do zastosowania monad.
Jared Updike,
+1 za „błąd samouczka monady”. Samouczki na temat monad są podobne do posiadania kilku samouczków próbujących wyjaśnić pojęcie liczb całkowitych. Jeden z samouczków powiedziałby: „1 jest podobne do jabłka”; inny samouczek mówi: „2 jest jak gruszka”; trzeci mówi: „3 to w zasadzie pomarańcza”. Ale nigdy nie otrzymujesz całego obrazu z jednego samouczka. Wyciągnąłem z tego to, że monady są abstrakcyjną koncepcją, która może być używana do wielu całkiem różnych celów.
stakx - nie przyczynia się już
@stakx: Tak, prawda. Ale nie miałem na myśli, że monady są abstrakcją, której nie można się nauczyć lub której nie należy się uczyć; Tyle tylko, że najlepiej się tego nauczyć po obejrzeniu wystarczającej liczby konkretnych przykładów, by dostrzec jedną abstrakcyjną podstawę. Zobacz moją drugą odpowiedź tutaj .
ShreevatsaR
5
Czasami wydaje mi się, że jest tak wiele samouczków, które próbują przekonać czytelnika, że ​​monady są użyteczne przy użyciu kodu, który robi skomplikowane lub przydatne rzeczy. To utrudniało mi zrozumienie przez wiele miesięcy. Nie uczę się w ten sposób. Wolę widzieć bardzo prosty kod, robiąc coś głupiego, przez co mogę przejść mentalnie i nie mogłem znaleźć tego rodzaju przykładu. Nie mogę się dowiedzieć, czy pierwszym przykładem jest monada analizująca skomplikowaną gramatykę. Mogę się dowiedzieć, czy to monada sumująca liczby całkowite.
Rafael S. Calsaverini,
Wymienienie tylko konstruktora typu jest niekompletny: stackoverflow.com/a/37345315/1614973
Dmitri Zaitsev
23

Monady mają kontrolować przepływ, jakie abstrakcyjne typy danych mają dla danych.

Innymi słowy, wielu programistów nie ma pojęcia o zestawach, listach, słownikach (lub skrótach lub mapach) i drzewach. W obrębie tych typów danych istnieje wiele specjalnych przypadków (na przykład InsertionOrderPreservingIdentityHashMap).

Jednak w konfrontacji z „przepływem” programu wielu programistów nie było narażonych na o wiele więcej konstrukcji niż wtedy, gdy przełącznik / case, robią, podczas gdy, goto (grr) i (być może) zamknięcia.

Zatem monada jest po prostu konstrukcją przepływu kontrolnego. Lepszym wyrażeniem zastępującym monadę byłby „typ kontroli”.

W związku z tym monada ma gniazda na logikę sterowania, instrukcje lub funkcje - odpowiednikiem w strukturach danych byłoby stwierdzenie, że niektóre struktury danych pozwalają dodawać i usuwać dane.

Na przykład monada „if”:

if( clause ) then block

w najprostszym przypadku ma dwa miejsca - klauzulę i blok. ifMonada jest zwykle wbudowany ocenić wynik klauzuli, a jeśli nie fałszywy, oceniać bloku. Wielu programistów nie jest zaznajomionych z monadami, kiedy uczą się „jeśli”, i po prostu nie trzeba rozumieć monad, aby pisać skuteczną logikę.

Monady mogą stać się bardziej skomplikowane, podobnie jak struktury danych mogą stać się bardziej skomplikowane, ale istnieje wiele szerokich kategorii monad, które mogą mieć podobną semantykę, ale różne implementacje i składnię.

Oczywiście w ten sam sposób, w jaki struktury danych mogą być iterowane lub przemierzane, monady mogą być oceniane.

Kompilatory mogą, ale nie muszą, obsługiwać monad zdefiniowanych przez użytkownika. Haskell na pewno tak. Ioke ma pewne podobne możliwości, chociaż termin monada nie jest używany w tym języku.

Nick Drew
źródło
14

Mój ulubiony samouczek Monady:

http://www.haskell.org/haskellwiki/All_About_Monads

(spośród 170 000 trafień w wyszukiwarce Google hasła „samouczek monady”!)

@Stu: Celem monad jest umożliwienie dodawania (zwykle) semantyki sekwencyjnej do czystego kodu; możesz nawet komponować monady (używając Monad Transformers) i uzyskać bardziej interesującą i skomplikowaną połączoną semantykę, na przykład parsowanie z obsługą błędów, stanem współdzielonym i logowaniem. Wszystko to jest możliwe w czystym kodzie, monady pozwalają po prostu wyodrębnić go i ponownie użyć w bibliotekach modułowych (zawsze dobre w programowaniu), a także zapewnić wygodną składnię, aby wyglądało to bezwzględnie.

Haskell ma już przeciążenie operatora [1]: używa klas typów w taki sam sposób, w jaki można używać interfejsów w Javie lub C #, ale Haskell akurat akceptuje również tokeny niealfanumeryczne takie jak + && i> jako identyfikatory infix. To przeciążenie operatora tylko na twój sposób patrzenia na niego, jeśli masz na myśli „przeciążenie średnika” [2]. Brzmi jak czarna magia i prosi o kłopoty z „przeciążeniem średnika” (przedsiębiorcy hakerzy Perla znają ten pomysł), ale chodzi o to, że bez monad nie ma średnika, ponieważ czysto funkcjonalny kod nie wymaga ani nie pozwala na wyraźne sekwencjonowanie.

Wszystko to brzmi o wiele bardziej skomplikowane niż trzeba. Artykuł sigfpe jest całkiem fajny, ale używa Haskella, aby to wyjaśnić, co w pewnym sensie nie łamie problemu z kurczakiem i jajami, rozumienia Haskella do grokowania Monad i zrozumienia Monad do grokowania Haskell.

[1] Jest to problem odrębny od monad, ale monady używają funkcji przeciążania operatora Haskell.

[2] Jest to również nadmierne uproszczenie, ponieważ operatorem łączenia działań monadycznych jest >> = (wymawiane „bind”), ale istnieje cukier składniowy („do”), który pozwala używać nawiasów klamrowych i średników i / lub wcięć i znaków nowej linii.

Jared Updike
źródło
9

Ostatnio myślałem o Monadach w inny sposób. Myślałem o nich jako o matematycznym wyodrębnianiu kolejności egzekucji , co umożliwia nowe rodzaje polimorfizmu.

Jeśli używasz języka rozkazującego i piszesz niektóre wyrażenia w kolejności, kod ZAWSZE działa dokładnie w tej kolejności.

A w prostym przypadku, gdy używasz monady, wydaje się to samo - definiujesz listę wyrażeń, które występują w kolejności. Z wyjątkiem tego, że w zależności od używanej monady kod może być uruchamiany w kolejności (jak w monadzie IO), równolegle w kilku elementach jednocześnie (jak w monadzie z listy), może się zatrzymać w połowie (jak w monadzie może) , może wstrzymać się częściowo, aby wznowić później (jak w monadzie Wznowienia), może przewinąć do tyłu i zacząć od początku (jak w monadzie Transakcji), lub może przewinąć do tyłu, aby wypróbować inne opcje (jak w monadzie Logiki) .

A ponieważ monady są polimorficzne, możliwe jest uruchomienie tego samego kodu w różnych monadach, w zależności od potrzeb.

Ponadto w niektórych przypadkach można łączyć monady razem (z transformatorami monad), aby uzyskać wiele funkcji jednocześnie.

jes5199
źródło
9

Nadal jestem nowy w monadach, ale pomyślałem, że podzielę się linkiem, który uznałem za naprawdę dobry do czytania (Z OBRAZAMI !!): http://www.matusiak.eu/numerodix/blog/2012/3/11/ monads-for-the-layman / (bez przynależności)

Zasadniczo, ciepłą i rozmytą koncepcją, którą otrzymałem z tego artykułu, była koncepcja, że ​​monady są w zasadzie adapterami, które pozwalają różnym funkcjom działać w sposób składalny, tj. Być w stanie zestawiać wiele funkcji i łączyć je ze sobą, nie martwiąc się o niespójny zwrot. rodzaje i takie. Tak więc funkcja BIND odpowiada za utrzymywanie jabłek z jabłkami i pomarańczy z pomarańczami, gdy próbujemy zrobić te adaptery. A funkcja LIFT odpowiada za przejmowanie funkcji „niższego poziomu” i „uaktualnianie” ich do pracy z funkcjami BIND i bycia również kompozycyjnymi.

Mam nadzieję, że dobrze to zrozumiałem, a co ważniejsze, mam nadzieję, że artykuł ma prawidłowy pogląd na monady. Jeśli nic więcej, ten artykuł pomógł zaostrzyć mój apetyt na więcej informacji na temat monad.

magicpanda
źródło
Przykłady python ułatwiły zrozumienie! Dzięki za udostępnienie.
Ryan Efendy
8

Oprócz doskonałych odpowiedzi powyżej, pozwól mi zaoferować link do następującego artykułu (autorstwa Patricka Thomsona), który wyjaśnia monady, odnosząc tę ​​koncepcję do biblioteki jQuery biblioteki JavaScript (i jej sposobu użycia „łączenia łańcuchów metod” do manipulowania DOM) : jQuery to monada

Sama dokumentacja jQuery nie odnosi się do terminu „monada”, ale mówi o „wzorcu konstruktora”, który prawdopodobnie jest bardziej znany. Nie zmienia to faktu, że masz tam odpowiednią monadę, nawet nie zdając sobie z tego sprawy.

Siggboy
źródło
Jeśli używasz jQuery, to wyjaśnienie może być bardzo pomocne, szczególnie jeśli twój Haskell nie jest silny
byteclub
10
JQuery zdecydowanie nie jest monadą. Połączony artykuł jest nieprawidłowy.
Tony Morris,
1
Bycie „stanowczym” nie jest zbyt przekonujące. Aby uzyskać przydatne dyskusje na ten temat, zobacz Czy jQuery jest
monadą
1
Zobacz także Douglasa Crackforda Google Talk Monady i Gonady oraz jego kod JavaScript do robienia modadów, rozwijający podobne zachowanie bibliotek AJAX i obietnic: douglascrockford / monad · GitHub
nealmcb
7

Monada to sposób łączenia obliczeń, które mają wspólny kontekst. To jest jak budowanie sieci rur. Podczas budowy sieci przepływają przez nią dane. Ale kiedy skończyłem układać wszystkie bity razem z „bind” i „return”, wywołuję coś podobnego, runMyMonad monad dataa dane przepływają przez potoki.

Alex
źródło
1
To bardziej przypomina Applicative niż Monad. W Monadach musisz pobrać dane z potoków, zanim będziesz mógł wybrać następny potok do połączenia.
Peaker
tak, opisujesz aplikację, a nie monadę. Monada polega na budowaniu następnego segmentu rury na miejscu, w zależności od danych, które osiągnęły ten punkt, wewnątrz rury.
Czy Ness
6

W praktyce monada jest niestandardową implementacją operatora kompozycji funkcji, która dba o skutki uboczne i niekompatybilne wartości wejściowe i zwracane (do łączenia).

Mateusz Charytoniuk
źródło
5

Dwie rzeczy, które pomogły mi najlepiej, gdy się o nich dowiedziałem:

Rozdział 8, „Parsery funkcyjne” z książki Grahama Huttona Programowanie w Haskell . Właściwie to wcale nie wspomina o monadach, ale jeśli możesz przejść przez rozdział i naprawdę zrozumieć wszystko w nim, a zwłaszcza to, jak ocenia się sekwencję operacji wiązania, zrozumiesz wewnętrzne elementy monad. Spodziewaj się, że zajmie to kilka prób.

Samouczek Wszystko o monadach . Daje to kilka dobrych przykładów ich użycia i muszę powiedzieć, że analogia w Appendexie działała dla mnie.

cjs
źródło
5

Monoid wydaje się być czymś, co zapewnia, że ​​wszystkie operacje zdefiniowane na Monoid i obsługiwanym typie zawsze zwracają obsługiwany typ w Monoid. Np. Dowolna liczba + Dowolna liczba = Liczba, bez błędów.

Podczas gdy podział akceptuje dwa ułamki zwykłe i zwraca ułamek, który definiuje dzielenie przez zero jako Nieskończoność w haskell (który jest czasem ułamkiem) ...

W każdym razie wydaje się, że Monady są po prostu sposobem na zapewnienie, że łańcuch operacji zachowuje się w przewidywalny sposób, a funkcja, która twierdzi, że jest Num -> Num, złożona z innej funkcji Num-> Num wywoływanej za pomocą x powiedzmy, odpal pociski.

Z drugiej strony, jeśli mamy funkcję, która wystrzeliwuje pociski, możemy połączyć ją z innymi funkcjami, które również wystrzeliwują pociski, ponieważ nasza intencja jest jasna - chcemy wystrzelić pociski - ale nie spróbuje drukowanie „Hello World” z jakiegoś dziwnego powodu.

W Haskell main jest typu IO () lub IO [()], rozróżnienie jest dziwne i nie będę o tym dyskutować, ale oto, co myślę, że się dzieje:

Jeśli mam główny, chcę, aby wykonał łańcuch działań, powodem, dla którego uruchamiam program, jest uzyskanie efektu - zwykle przez IO. W ten sposób mogę łączyć operacje IO razem, aby - zrobić IO, nic więcej.

Jeśli spróbuję zrobić coś, co nie „zwróci IO”, program narzeka, że ​​łańcuch nie płynie, lub w zasadzie „Jak to się ma do tego, co próbujemy zrobić - działanie IO”, wydaje się, że wymusza programiści, aby zachować ciąg myśli, nie zbaczając z drogi i nie myśląc o odpaleniu pocisków, jednocześnie tworząc algorytmy sortowania - które nie płyną.

Zasadniczo Monady wydają się wskazówką dla kompilatora, że ​​„hej, znasz tę funkcję, która zwraca tutaj liczbę, tak naprawdę nie zawsze działa, czasami może wygenerować liczbę, a czasami nic, po prostu trzymaj ją umysł". Wiedząc o tym, jeśli spróbujesz zastosować akcję monadyczną, akcja monadyczna może działać jako wyjątek czasu kompilacji, mówiąc: „hej, to nie jest właściwie liczba, MOŻE to być liczba, ale nie możesz tego założyć, zrób coś w celu zapewnienia, że ​​przepływ jest akceptowalny. ” co zapobiega nieprzewidzianemu zachowaniu programu - w dość dużym stopniu.

Wygląda na to, że w monadach nie chodzi o czystość ani kontrolę, ale o utrzymanie tożsamości kategorii, w której wszystkie zachowania są przewidywalne i zdefiniowane lub nie kompilują się. Nie możesz nic zrobić, gdy oczekuje się, że coś zrobisz, i nie możesz nic zrobić, jeśli oczekuje się, że nic nie zrobisz (widoczne).

Największym powodem, dla którego mogłem wymyślić Monady, jest - spójrz na kod Procedural / OOP, a zauważysz, że nie wiesz, gdzie program się kończy, a co kończy, wszystko, co widzisz, to dużo skoków i matematyki , magia i pociski. Nie będziesz w stanie go utrzymać, a jeśli możesz, poświęcisz sporo czasu na skupienie się na całym programie, zanim zrozumiesz jego część, ponieważ modułowość w tym kontekście opiera się na współzależnych „sekcjach” kodu, w którym kod jest zoptymalizowany tak, aby był jak najbardziej powiązany w celu zapewnienia wydajności / wzajemnych relacji. Monady są bardzo konkretne i dobrze zdefiniowane z definicji, i zapewniają, że przepływ programu jest możliwy do analizy i wyodrębnienia części trudnych do analizy - ponieważ one same są monadami. Monada wydaje się być „ lub zniszczyć wszechświat, a nawet zniekształcić czas - nie mamy pojęcia ani żadnych gwarancji, że TO TO, CO TO JEST. Monada GWARANTUJE, ŻE TO, CO TO JEST. co jest bardzo potężne. lub zniszczyć wszechświat, a nawet zniekształcić czas - nie mamy pojęcia ani żadnych gwarancji, że TO TO, CO TO JEST. Monada GWARANTUJE, ŻE TO, CO TO JEST. co jest bardzo potężne.

Wszystkie rzeczy w „prawdziwym świecie” wydają się być monadami, w tym sensie, że wiążą je określone obserwowalne prawa zapobiegające zamieszaniu. Nie oznacza to, że musimy naśladować wszystkie operacje tego obiektu, aby utworzyć klasy, zamiast tego możemy po prostu powiedzieć „kwadrat jest kwadratem”, nic oprócz kwadratu, nawet prostokąta ani koła, i „kwadrat ma powierzchnię o długości jednego z istniejących wymiarów pomnożonych przez siebie. Bez względu na kwadrat, który masz, jeśli jest to kwadrat w przestrzeni 2D, jego powierzchnia absolutnie nie może być niczym innym jak kwadrat podniesiony do kwadratu, jest to prawie banalne do udowodnienia. Jest to bardzo potężne, ponieważ nie musimy przedstawiać twierdzeń, aby upewnić się, że nasz świat jest taki, jaki jest, po prostu wykorzystujemy implikacje rzeczywistości, aby zapobiec upadkowi naszych programów.

Jestem prawie pewny, że się mylę, ale myślę, że to może komuś pomóc, więc mam nadzieję, że to komuś pomoże.

Dmitry
źródło
5

W kontekście Scali znajdziesz najprostszą definicję. Zasadniczo flatMap (lub bind) jest „asocjacyjny” i istnieje tożsamość.

trait M[+A] {
  def flatMap[B](f: A => M[B]): M[B] // AKA bind

  // Pseudo Meta Code
  def isValidMonad: Boolean = {
    // for every parameter the following holds
    def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
      x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))

    // for every parameter X and x, there exists an id
    // such that the following holds
    def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
      x.flatMap(id) == x
  }
}

Na przykład

// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)

// Observe these are identical. Since Option is a Monad 
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)

scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)


// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)

// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)

scala> Some(7)
res214: Some[Int] = Some(7)

UWAGA Ściśle mówiąc, definicja Monady w programowaniu funkcjonalnym nie jest tym samym, co definicja Monady w Teorii Kategorii , która jest zdefiniowana na przemian z mapi flatten. Chociaż są one swego rodzaju odpowiednikiem w niektórych mapowaniach. Prezentacje są bardzo dobre: http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category

samthebest
źródło
5

Ta odpowiedź zaczyna się od motywującego przykładu, przechodzi przez przykład, wyprowadza przykład monady i formalnie definiuje „monada”.

Rozważ te trzy funkcje w pseudokodzie:

f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x)          := <x, "">

fprzyjmuje uporządkowaną parę formularza <x, messages>i zwraca uporządkowaną parę. Pozostawia nietknięty pierwszy element i dołącza "called f. "do drugiego. To samo z g.

Możesz skomponować te funkcje i uzyskać oryginalną wartość wraz z łańcuchem, który pokazuje, w jakiej kolejności zostały wywołane funkcje:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">

Nie podoba ci się fakt, że fi goni są odpowiedzialni za dołączanie własnych wiadomości dziennika do poprzednich informacji logowania. (Wyobraźcie sobie tylko dla argumentu, że zamiast dodawać ciągi fi gmusi wykonywać skomplikowaną logikę na drugim elemencie pary. Powtórzenie tej skomplikowanej logiki w dwóch lub więcej różnych funkcjach byłoby utrudnieniem.)

Wolisz pisać prostsze funkcje:

f(x)    := <x, "called f. ">
g(x)    := <x, "called g. ">
wrap(x) := <x, "">

Ale spójrz na to, co się stanie, gdy je skomponujesz:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">

Problem polega na tym, że przekazanie pary do funkcji nie daje tego, czego chcesz. Ale co, jeśli możesz karmić parę do funkcji:

  feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">

Czytać feed(f, m)jako „pasza mdo f”. Aby nakarmić parę <x, messages>do funkcji fjest przekazać x do f, get <y, message>out of f, a powrót<y, messages message> .

feed(f, <x, messages>) := let <y, message> = f(x)
                          in  <y, messages message>

Zwróć uwagę, co się dzieje, gdy wykonujesz trzy czynności za pomocą swoich funkcji:

Po pierwsze: jeśli owinąć wartość i następnie paszy uzyskanej pary do funkcji:

  feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
  in  <y, "" message>
= let <y, message> = <x, "called f. ">
  in  <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)

To jest tak samo jak przemijanie wartości do funkcji.

Po drugie: jeśli wpiszesz parę w wrap:

  feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
  in  <y, messages message>
= let <y, message> = <x, "">
  in  <y, messages message>
= <x, messages "">
= <x, messages>

To nie zmienia pary.

Po trzecie: jeśli zdefiniujesz funkcję, która pobiera xi przekazuje informacje g(x)do f:

h(x) := feed(f, g(x))

i nakarm parę:

  feed(h, <x, messages>)
= let <y, message> = h(x)
  in  <y, messages message>
= let <y, message> = feed(f, g(x))
  in  <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
  in  <y, messages message>
= let <y, message> = let <z, msg> = f(x)
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
  in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))

To jest to samo, co karmienie pary gi karmienie otrzymanej pary f.

Masz większość monady. Teraz musisz tylko wiedzieć o typach danych w swoim programie.

Jakiego rodzaju jest wartość <x, "called f. ">? Zależy to od rodzaju wartości x. Jeśli xjest typu t, to twoja para jest wartością typu „para ti ciąg znaków”. Zadzwoń do tego typuM t .

Mjest konstruktorem typów: Msam nie odnosi się do typu, ale M _odnosi się do typu po wypełnieniu go pustym typem. An M intto para liczb całkowitych i ciąg znaków. NaM string to para łańcucha i łańcucha. Itp.

Gratulacje, stworzyłeś monadę!

Formalnie twoja monada jest krotką <M, feed, wrap> .

Monada to krotka, w <M, feed, wrap>której:

  • M jest konstruktorem typów.
  • feedprzyjmuje (funkcja, która przyjmuje ta zwraca an M u) oraz an M ti zwraca an M u.
  • wrapbierze va zwraca an M v.

t, ui vsą dowolnymi trzema typami, które mogą, ale nie muszą być takie same. Monada spełnia trzy właściwości udowodnione dla konkretnej monady:

  • Podawanie owiniętego tdo funkcji jest równoznaczne z przekazaniem nieopakowanego tdo funkcji.

    Formalnie: feed(f, wrap(x)) = f(x)

  • Karmienie M tINTO wrapnie robi nic do M t.

    Formalnie: feed(wrap, m) = m

  • Podaj M t(wywołaj m) do funkcji, która

    • przechodzi twg
    • dostaje M u(zadzwoń n) zg
    • karmi nsięf

    jest taki sam jak

    • karmienie mdog
    • uzyskiwać nzg
    • karmienie ndof

    Formalnie: feed(h, m) = feed(f, feed(g, m))gdzieh(x) := feed(f, g(x))

Zazwyczaj feednazywa się bind(AKA >>=in Haskell) i wrapnazywa się return.

Jordania
źródło
5

Spróbuję wyjaśnić Monadw kontekście Haskella.

W programowaniu funkcjonalnym ważny jest skład funkcji. Pozwala naszemu programowi składać się z małych, łatwych do odczytania funkcji.

Powiedzmy, że mamy dwie funkcje: g :: Int -> Stringi f :: String -> Bool.

Możemy to zrobić (f . g) x, tak samo jak f (g x), gdzie xjest Intwartość.

Podczas tworzenia kompozycji / stosowania wyniku jednej funkcji do drugiej ważne jest dopasowanie typów. W powyższym przypadku typ zwróconego wyniku gmusi być taki sam, jak typ zaakceptowany przezf .

Ale czasami wartości są kontekstowe, co sprawia, że ​​nieco łatwiej jest ustawiać typy w szeregu. (Posiadanie wartości w kontekstach jest bardzo przydatne. Na przykład Maybe Inttyp reprezentuje Intwartość, która może nie istnieć, IO Stringtyp reprezentuje Stringwartość występującą w wyniku wywołania pewnych efektów ubocznych.)

Powiedzmy, że teraz mamy g1 :: Int -> Maybe Stringi f1 :: String -> Maybe Bool. g1i f1są bardzo podobne do gi fodpowiednio.

Nie możemy zrobić (f1 . g1) xlub f1 (g1 x), gdzie xjest Intwartość. Zwrócony typ wyniku g1nie jest tym, cof1 oczekuje.

Mogliśmy komponować fi gz .operatorem, ale teraz nie możemy komponować f1i g1z .. Problem polega na tym, że nie możemy bezpośrednio przekazać wartości w kontekście do funkcji, która oczekuje wartości, która nie jest w kontekście.

Czy nie byłoby miło, gdybyśmy wprowadzili operatora do komponowania g1i f1takiego, który możemy pisać (f1 OPERATOR g1) x? g1zwraca wartość w kontekście. Wartość zostanie wyjęta z kontekstu i zastosowana do f1. I tak, mamy takiego operatora. Jest <=<.

Mamy również >>= operatora, który robi dla nas dokładnie to samo, choć w nieco innej składni.

Piszemy: g1 x >>= f1. g1 xjest Maybe Intwartością. >>=Operator pomaga przyjąć, że Intwartość z „chyba-nie-tam” kontekście, i zastosować go do f1. Wynik f1, który jest Maybe Bool, będzie wynikiem całej >>=operacji.

I na koniec, dlaczego jest Monadprzydatny? Ponieważ Monadjest to klasa typu, która definiuje >>=operator, bardzo podobnie jak Eqklasa typu, która definiuje operatory ==i /=.

Podsumowując, Monadklasa typu definiuje>>= operator, który pozwala nam przekazywać wartości w kontekście (nazywamy te wartości monadyczne) do funkcji, które nie oczekują wartości w kontekście. Zadbamy o kontekst.

Jeśli jest tutaj jedna rzecz do zapamiętania, to Monadpozwala ona na składanie funkcji, które obejmuje wartości w kontekstach .

Jonas
źródło
IOW, Monad jest uogólnionym protokołem wywoływania funkcji.
Will Ness
Moim zdaniem odpowiedź jest najbardziej pomocna. Chociaż muszę powiedzieć, że uważam, że należy położyć nacisk na fakt, że funkcje, do których się odwołujesz, nie tylko uwzględniają wartości w kontekstach, ale aktywnie umieszczają wartości w kontekstach. Na przykład funkcja f :: ma -> mb bardzo łatwo komponuje się z inną funkcją, g :: mb -> m c. Ale monady (konkretnie wiążące) pozwalają nam na ciągłe komponowanie funkcji, które umieszczają swoje dane wejściowe w tym samym kontekście, bez konieczności uprzedniego wyciągania wartości z tego kontekstu (co skutecznie usuwa informacje z wartości)
James
@James Myślę, że to powinien być nacisk na funktory?
Jonas
@Jonas Chyba nie wyjaśniłem właściwie. Kiedy mówię, że funkcje umieszczają wartości w kontekstach, mam na myśli, że mają typ (a -> mb). Są to bardzo przydatne, ponieważ umieszczenie wartości w kontekście dodaje do niej nowe informacje, ale zwykle trudno byłoby połączyć razem łańcuch (a -> mb) i (b -> mc), ponieważ nie możemy po prostu wyciągnąć wartości kontekstu. Musielibyśmy więc zastosować skomplikowany proces, aby połączyć te funkcje w rozsądny sposób, w zależności od konkretnego kontekstu, a monady pozwalają nam to robić w spójny sposób, niezależnie od kontekstu.
James
5

tl; dr

{-# LANGUAGE InstanceSigs #-}

newtype Id t = Id t

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

Prolog

Operator aplikacji $funkcji

forall a b. a -> b

jest zdefiniowany kanonicznie

($) :: (a -> b) -> a -> b
f $ x = f x

infixr 0 $

pod względem zastosowania funkcji prymitywnej Haskell f x( infixl 10).

Skład .określa się $jako

(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x

infixr 9 .

i spełnia równoważności forall f g h.

     f . id  =  f            :: c -> d   Right identity
     id . g  =  g            :: b -> c   Left identity
(f . g) . h  =  f . (g . h)  :: a -> d   Associativity

.ma asocjację i idjest jego prawą i lewą tożsamością.

Potrójne kleisli

W programowaniu monada jest konstruktorem typu funktora z instancją klasy typu monada. Istnieje kilka równoważnych wariantów definicji i implementacji, z których każda zawiera nieco inne intuicje na temat abstrakcji monady.

Funktor to rodzaj konstruktora ftypów * -> *z instancją klasy typu funktora.

{-# LANGUAGE KindSignatures #-}

class Functor (f :: * -> *) where
   map :: (a -> b) -> (f a -> f b)

Oprócz przestrzegania statycznie wymuszonego protokołu typu, instancje klasy typu funktora muszą być zgodne z algebraicznymi prawami funktora forall f g.

       map id  =  id           :: f t -> f t   Identity
map f . map g  =  map (f . g)  :: f a -> f c   Composition / short cut fusion

Obliczenia Functor mają typ

forall f t. Functor f => f t

Obliczenia c robejmują wyniki r w kontekście c .

Unary monadyczne funkcje lub strzały Kleisli mają ten typ

forall m a b. Functor m => a -> m b

Strzałki Kleisi są funkcjami, które pobierają jeden argument ai zwracają monadyczne obliczeniam b .

Monady są kanonicznie zdefiniowane w kategoriach potrójnej Kleisli forall m. Functor m =>

(m, return, (=<<))

zaimplementowany jako klasa typu

class Functor m => Monad m where
   return :: t -> m t
   (=<<)  :: (a -> m b) -> m a -> m b

infixr 1 =<<

Tożsamość Kleisli return jest Kleisli strzałka, która promuje wartości tdo monadycznej kontekście m. Aplikacja Extension lub Kleisli =<< stosuje strzałkę Kleisli a -> m bdo wyników obliczeńm a .

Skład Kleisli <=< definiuje się w kategoriach rozszerzenia jako

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x

infixr 1 <=<

<=< tworzy dwie strzałki Kleisli, stosując lewą strzałkę do wyników zastosowania prawej strzałki.

Instancje klasy typu monada muszą być zgodne z prawami monady , najbardziej elegancko określonymi pod względem składu Kleisli:forall f g h.

   f <=< return  =  f                :: c -> m d   Right identity
   return <=< g  =  g                :: b -> m c   Left identity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d   Associativity

<=<ma asocjację i returnjest jego prawą i lewą tożsamością.

Tożsamość

Typ tożsamości

type Id t = t

jest funkcją tożsamości dla typów

Id :: * -> *

Interpretowany jako funktor,

   return :: t -> Id t
=      id :: t ->    t

    (=<<) :: (a -> Id b) -> Id a -> Id b
=     ($) :: (a ->    b) ->    a ->    b

    (<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
=     (.) :: (b ->    c) -> (a ->    b) -> (a ->    c)

W kanonicznym Haskell monada tożsamości jest zdefiniowana

newtype Id t = Id t

instance Functor Id where
   map :: (a -> b) -> Id a -> Id b
   map f (Id x) = Id (f x)

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

Opcja

Typ opcji

data Maybe t = Nothing | Just t

koduje obliczenia, Maybe tktóre niekoniecznie dają wynik t, obliczenia, które mogą „zawieść”. Opcja monada jest zdefiniowana

instance Functor Maybe where
   map :: (a -> b) -> (Maybe a -> Maybe b)
   map f (Just x) = Just (f x)
   map _ Nothing  = Nothing

instance Monad Maybe where
   return :: t -> Maybe t
   return = Just

   (=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
   f =<< (Just x) = f x
   _ =<< Nothing  = Nothing

a -> Maybe bjest stosowany do wyniku tylko wtedy, gdy Maybe adaje wynik.

newtype Nat = Nat Int

Liczby naturalne mogą być kodowane jako liczby całkowite większe lub równe zero.

toNat :: Int -> Maybe Nat
toNat i | i >= 0    = Just (Nat i)
        | otherwise = Nothing

Liczby naturalne nie są zamykane przy odejmowaniu.

(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)

infixl 6 -?

Opcja monada obejmuje podstawową formę obsługi wyjątków.

(-? 20) <=< toNat :: Int -> Maybe Nat

Lista

Monada listy nad typem listy

data [] t = [] | t : [t]

infixr 5 :

i jego addytywna operacja monoidu „append”

(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[]       ++ ys = ys

infixr 5 ++

koduje obliczenia nieliniowe,[t] dając naturalną liczbę 0, 1, ...wyników t.

instance Functor [] where
   map :: (a -> b) -> ([a] -> [b])
   map f (x : xs) = f x : map f xs
   map _ []       = []

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> [a] -> [b]
   f =<< (x : xs) = f x ++ (f =<< xs)
   _ =<< []       = []

Rozszerzenie =<<łączy ++wszystkie listy [b]wynikające z zastosowania f xstrzałki Kleisli a -> [b]do elementów [a]pojedynczej listy wyników[b] .

Niech właściwe dzielniki dodatniej liczby całkowitej nbędą

divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]

divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)

następnie

forall n.  let { f = f <=< divisors } in f n   =   []

Podczas definiowania klasy typu monada zamiast rozszerzenia =<<standard Haskell używa operatora odwracania, czyli bind>>= .

class Applicative m => Monad m where
   (>>=) :: forall a b. m a -> (a -> m b) -> m b

   (>>) :: forall a b. m a -> m b -> m b
   m >> k = m >>= \ _ -> k
   {-# INLINE (>>) #-}

   return :: a -> m a
   return = pure

Dla uproszczenia wyjaśnienie to wykorzystuje hierarchię klas typów

class              Functor f
class Functor m => Monad m

W Haskell obecna standardowa hierarchia to

class                  Functor f
class Functor p     => Applicative p
class Applicative m => Monad m

ponieważ nie tylko każda monada jest funktorem, ale każda aplikacja jest funktorem, a każda monada jest również aplikacją.

Używając monady listy, pseudokodu imperatywnego

for a in (1, ..., 10)
   for b in (1, ..., 10)
      p <- a * b
      if even(p)
         yield p

z grubsza przekłada się na blok do ,

do a <- [1 .. 10]
   b <- [1 .. 10]
   let p = a * b
   guard (even p)
   return p

równoważne rozumienie monady ,

[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]

i wyrażenie

[1 .. 10] >>= (\ a ->
   [1 .. 10] >>= (\ b ->
      let p = a * b in
         guard (even p) >>       -- [ () | even p ] >>
            return p
      )
   )

Czy notacje i rozumienia monad są cukrem syntaktycznym dla wyrażeń wiązania zagnieżdżonego. Operator wiązania służy do wiązania nazw lokalnych wyników monadycznych.

let x = v in e    =   (\ x -> e)  $  v   =   v  &  (\ x -> e)
do { r <- m; c }  =   (\ r -> c) =<< m   =   m >>= (\ r -> c)

gdzie

(&) :: a -> (a -> b) -> b
(&) = flip ($)

infixl 0 &

Funkcja wartownika jest zdefiniowana

guard :: Additive m => Bool -> m ()
guard True  = return ()
guard False = fail

gdzie typ jednostki lub „pusta krotka”

data () = ()

Addytywne monady, które obsługują wybór i niepowodzenie, można wyodrębnić za pomocą klasy typu

class Monad m => Additive m where
   fail  :: m t
   (<|>) :: m t -> m t -> m t

infixl 3 <|>

instance Additive Maybe where
   fail = Nothing

   Nothing <|> m = m
   m       <|> _ = m

instance Additive [] where
   fail = []
   (<|>) = (++)

gdzie faili <|>tworzą monoidforall k l m.

     k <|> fail  =  k
     fail <|> l  =  l
(k <|> l) <|> m  =  k <|> (l <|> m)

i failjest absorbującym / anihilującym zerowym elementem monad addytywnych

_ =<< fail  =  fail

Jeśli w

guard (even p) >> return p

even pjest prawdą, wówczas wartownik tworzy [()]i, z definicji >>, lokalną funkcję stałą

\ _ -> return p

jest stosowany do wyniku (). Jeśli false, to wartownik produkuje listę monad's fail( []), co nie daje żadnego rezultatu dla zastosowania strzałki Kleisli >>, więc top jest to pomijane.

Stan

Niesławnie monady są używane do kodowania obliczeń stanowych.

Procesor stan jest funkcją

forall st t. st -> (t, st)

który zmienia stan sti daje wynik t. stan st może być cokolwiek. Nic, flaga, liczba, tablica, uchwyt, maszyna, świat.

Typ procesorów stanu jest zwykle nazywany

type State st t = st -> (t, st)

Monada procesora stanu jest uprzywilejowanym * -> *funktorem State st. Strzałki Kleisli monady procesora stanu są funkcjami

forall st a b. a -> (State st) b

W kanonicznym Haskell jest zdefiniowana leniwa wersja monady procesora stanu

newtype State st t = State { stateProc :: st -> (t, st) }

instance Functor (State st) where
   map :: (a -> b) -> ((State st) a -> (State st) b)
   map f (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  (f x, s1)

instance Monad (State st) where
   return :: t -> (State st) t
   return x = State $ \ s -> (x, s)

   (=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
   f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  stateProc (f x) s1

Procesor stanu jest uruchamiany przez podanie stanu początkowego:

run :: State st t -> st -> (t, st)
run = stateProc

eval :: State st t -> st -> t
eval = fst . run

exec :: State st t -> st -> st
exec = snd . run

Dostęp do stanu zapewniają prymitywy geti putmetody abstrakcji nad stanowymi monadami:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}

class Monad m => Stateful m st m -> st where
   get :: m st
   put :: st -> m ()

m -> stdeklaruje funkcjonalną zależność typu stanu stod monady m; że a State t, na przykład, określi typ stanu jako tunikalny.

instance Stateful (State st) st where
   get :: State st st
   get = State $ \ s -> (s, s)

   put :: st -> State st ()
   put s = State $ \ _ -> ((), s)

z typem jednostki stosowanym analogicznie jak voidw C.

modify :: Stateful m st => (st -> st) -> m ()
modify f = do
   s <- get
   put (f s)

gets :: Stateful m st => (st -> t) -> m t
gets f = do
   s <- get
   return (f s)

gets jest często używany z akcesoriami pola rekordu.

Stan monadowy odpowiednik zmiennej wątków

let s0 = 34
    s1 = (+ 1) s0
    n = (* 12) s1
    s2 = (+ 7) s1
in  (show n, s2)

gdzie s0 :: Intjest równie referencyjnie przejrzysty, ale nieskończenie bardziej elegancki i praktyczny

(flip run) 34
   (do
      modify (+ 1)
      n <- gets (* 12)
      modify (+ 7)
      return (show n)
   )

modify (+ 1)jest obliczeniem typu State Int (), z wyjątkiem jego efektu równoważnego z return ().

(flip run) 34
   (modify (+ 1) >>
      gets (* 12) >>= (\ n ->
         modify (+ 7) >>
            return (show n)
      )
   )

Monadowe prawo asocjacyjne można zapisać w kategoriach >>= forall m f g.

(m >>= f) >>= g  =  m >>= (\ x -> f x >>= g)

lub

do {                 do {                   do {
   r1 <- do {           x <- m;                r0 <- m;
      r0 <- m;   =      do {            =      r1 <- f r0;
      f r0                 r1 <- f x;          g r1
   };                      g r1             }
   g r1                 }
}                    }

Podobnie jak w programowaniu zorientowanym na wyrażenia (np. Rust), ostatnia instrukcja bloku reprezentuje jego wydajność. Operator wiązania jest czasem nazywany „programowalnym średnikiem”.

Operacje podstawowe struktury kontroli iteracji ze strukturalnego programowania imperatywnego są emulowane monadycznie

for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())

while :: Monad m => m Bool -> m t -> m ()
while c m = do
   b <- c
   if b then m >> while c m
        else return ()

forever :: Monad m => m t
forever m = m >> forever m

Wejście wyjście

data World

Monada procesora światowego we / wy jest połączeniem czystego Haskella i świata rzeczywistego, funkcjonalnej denotatywnej i bezwzględnej semantyki operacyjnej. Bliski odpowiednik faktycznego ścisłego wdrożenia:

type IO t = World -> (t, World)

Interakcje ułatwiają nieczyste prymitywy

getChar         :: IO Char
putChar         :: Char -> IO ()
readFile        :: FilePath -> IO String
writeFile       :: FilePath -> String -> IO ()
hSetBuffering   :: Handle -> BufferMode -> IO ()
hTell           :: Handle -> IO Integer
. . .              . . .

Zanieczyszczenie kodu używającego IOprymitywów jest na stałe protokołowane przez system typów. Ponieważ czystość jest niesamowita, to, co się dzieje IO, pozostaje IO.

unsafePerformIO :: IO t -> t

A przynajmniej powinien.

Podpis typu programu Haskell

main :: IO ()
main = putStrLn "Hello, World!"

rozwija się do

World -> ((), World)

Funkcja, która przekształca świat.

Epilog

Kategoria, w której obiektami są typy Haskella, a morfizmy, w których morfizmy są funkcjami między typami Haskella, jest „szybka i luźna”, kategoria Hask .

Funktor Tto odwzorowanie z kategorii Cna kategorię D; dla każdego obiektu w Cobiekcie wD

Tobj :  Obj(C) -> Obj(D)
   f :: *      -> *

i dla każdego morfizmu w Cmorfizmie wD

Tmor :  HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
 map :: (a -> b)   -> (f a -> f b)

gdzie X, Ysą obiekty C. HomC(X, Y)jest klasą homomorfizmu wszystkich morfizmów X -> Yw C. Funktor musi zachować tożsamość i kompozycję morfizmu, „strukturę” C, w D.

                    Tmor    Tobj

      T(id)  =  id        : T(X) -> T(X)   Identity
T(f) . T(g)  =  T(f . g)  : T(X) -> T(Z)   Composition

Kategoria Kleisli z kategorii Cjest podana przez Kleisli Triple

<T, eta, _*>

endofunkcji

T : C -> C

( f), morfizm tożsamości eta( return) i operator rozszerzenia *( =<<).

Każdy morfizm Kleisli w Hask

      f :  X -> T(Y)
      f :: a -> m b

przez operatora wewnętrznego

   (_)* :  Hom(X, T(Y)) -> Hom(T(X), T(Y))
  (=<<) :: (a -> m b)   -> (m a -> m b)

otrzymuje morfizm w Haskkategorii Kleisli

     f* :  T(X) -> T(Y)
(f =<<) :: m a  -> m b

Skład w kategorii Kleisli .Tjest podany w odniesieniu do rozszerzenia

 f .T g  =  f* . g       :  X -> T(Z)
f <=< g  =  (f =<<) . g  :: a -> m c

i spełnia aksjomaty kategorii

       eta .T g  =  g                :  Y -> T(Z)   Left identity
   return <=< g  =  g                :: b -> m c

       f .T eta  =  f                :  Z -> T(U)   Right identity
   f <=< return  =  f                :: c -> m d

  (f .T g) .T h  =  f .T (g .T h)    :  X -> T(U)   Associativity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d

który, stosując transformacje równoważności

     eta .T g  =  g
     eta* . g  =  g               By definition of .T
     eta* . g  =  id . g          forall f.  id . f  =  f
         eta*  =  id              forall f g h.  f . h  =  g . h  ==>  f  =  g

(f .T g) .T h  =  f .T (g .T h)
(f* . g)* . h  =  f* . (g* . h)   By definition of .T
(f* . g)* . h  =  f* . g* . h     . is associative
    (f* . g)*  =  f* . g*         forall f g h.  f . h  =  g . h  ==>  f  =  g

pod względem przedłużenia są podane kanonicznie

               eta*  =  id                 :  T(X) -> T(X)   Left identity
       (return =<<)  =  id                 :: m t -> m t

           f* . eta  =  f                  :  Z -> T(U)      Right identity
   (f =<<) . return  =  f                  :: c -> m d

          (f* . g)*  =  f* . g*            :  T(X) -> T(Z)   Associativity
(((f =<<) . g) =<<)  =  (f =<<) . (g =<<)  :: m a -> m c

Monady można również zdefiniować w kategoriach nie rozszerzenia Kleisliana, ale naturalną transformację muw programowaniu zwanym join. Monada jest definiowana mujako potrójny w stosunku do kategorii Cendofunkcji

     T :  C -> C
     f :: * -> *

i dwie naturalne transformacje

   eta :  Id -> T
return :: t  -> f t

    mu :  T . T   -> T
  join :: f (f t) -> f t

spełnianie równoważności

       mu . T(mu)  =  mu . mu               :  T . T . T -> T . T   Associativity
  join . map join  =  join . join           :: f (f (f t)) -> f t

      mu . T(eta)  =  mu . eta       =  id  :  T -> T               Identity
join . map return  =  join . return  =  id  :: f t -> f t

Klasa typu monada jest następnie definiowana

class Functor m => Monad m where
   return :: t -> m t
   join   :: m (m t) -> m t

Kanoniczna muimplementacja opcji monada:

instance Monad Maybe where
   return = Just

   join (Just m) = m
   join Nothing  = Nothing

concatfunkcja

concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat []       = []

jest joinmonadą z listy.

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> ([a] -> [b])
   (f =<<) = concat . map f

Implementacje joinmożna tłumaczyć z formularza rozszerzenia przy użyciu równoważności

     mu  =  id*           :  T . T -> T
   join  =  (id =<<)      :: m (m t) -> m t

Odwrotne tłumaczenie z muna formularz rozszerzenia podano przez

     f*  =  mu . T(f)     :  T(X) -> T(Y)
(f =<<)  =  join . map f  :: m a -> m b

Dlaczego jednak tak abstrakcyjna teoria ma być przydatna w programowaniu?

Odpowiedź jest prosta: jako informatycy cenimy sobie abstrakcję ! Kiedy projektujemy interfejs do komponentu oprogramowania, chcemy , aby ujawniał jak najmniej informacji o implementacji. Chcemy być w stanie zastąpić wdrożenie wieloma alternatywami, wieloma innymi „instancjami” tej samej „koncepcji”. Kiedy projektujemy ogólny interfejs dla wielu bibliotek programów, jeszcze ważniejsze jest, aby wybrany interfejs miał wiele implementacji. Jest to ogólna koncepcja monady, którą tak wysoko cenimy, ponieważ teoria kategorii jest tak abstrakcyjna, że ​​jej pojęcia są tak przydatne do programowania.

Nic więc dziwnego, że uogólnienie monad, które prezentujemy poniżej, ma również ścisły związek z teorią kategorii. Podkreślamy jednak, że nasz cel jest bardzo praktyczny: nie polega na „wdrożeniu teorii kategorii”, lecz na znalezieniu bardziej ogólnego sposobu strukturyzacji bibliotek kombinatora. To po prostu nasze szczęście, że matematycy wykonali już dla nas większość pracy!

od generalizacji monad po strzały Johna Hughesa

6428287
źródło
4

Świat potrzebuje kolejnego wpisu na blogu o monadach, ale myślę, że jest to przydatne do identyfikacji istniejących monad na wolności.

Trójkąt Sierpińskiego

Powyżej jest fraktal zwany trójkątem Sierpińskiego, jedynym fraktalem, jaki pamiętam narysować. Fraktale są samopodobną strukturą jak powyższy trójkąt, w której części są podobne do całości (w tym przypadku dokładnie połowa skali jako trójkąt macierzysty).

Monady to fraktale. Biorąc pod uwagę monadyczną strukturę danych, jej wartości można skomponować w celu utworzenia innej wartości struktury danych. Dlatego jest przydatny do programowania i dlatego występuje w wielu sytuacjach.

Eugene Yokota
źródło
3
Masz na myśli „czego świat nie potrzebuje ...”? Niezła analogia!
groverboy
@ icc97 masz rację - znaczenie jest wystarczająco jasne. Niezamierzony sarkazm, przeprosiny dla autora.
groverboy
Świat potrzebuje jeszcze jednego komentarza potwierdzającego sarkazm, ale jeśli uważnie go przeczytam, napisałem, ale to powinno wyjaśnić.
Eugene Yokota
4

Niech poniższe „ {| a |m}” reprezentują jakiś fragment danych monadycznych. Typ danych, który reklamuje a:

        (I got an a!)
          /        
    {| a |m}

Funkcja fwie, jak stworzyć monadę, gdyby tylko miała a:

       (Hi f! What should I be?)
                      /
(You?. Oh, you'll be /
 that data there.)  /
 /                 /  (I got a b.)
|    --------------      |
|  /                     |
f a                      |
  |--later->       {| b |m}

Tutaj widzimy funkcję, fpróbuje ocenić monadę, ale zostaje zganiona.

(Hmm, how do I get that a?)
 o       (Get lost buddy.
o         Wrong type.)
o       /
f {| a |m}

Funtion, fznajduje sposób na wyodrębnienie aza pomocą >>=.

        (Muaahaha. How you 
         like me now!?)       
    (Better.)      \
        |     (Give me that a.)
(Fine, well ok.)    |
         \          |
   {| a |m}   >>=   f

Mało fwie, monada i >>=są w zmowie.

            (Yah got an a for me?)       
(Yeah, but hey    | 
 listen. I got    |
 something to     |
 tell you first   |
 ...)   \        /
         |      /
   {| a |m}   >>=   f

Ale o czym tak naprawdę mówią? To zależy od monady. Rozmawianie wyłącznie w sposób abstrakcyjny ma ograniczone zastosowanie; musicie mieć trochę doświadczenia z poszczególnymi monadami, aby rozwinąć zrozumienie.

Na przykład typ danych Może

 data Maybe a = Nothing | Just a

ma instancję monady, która będzie działać następująco ...

W którym przypadku jest Just a

            (Yah what is it?)       
(... hm? Oh,      |
forget about it.  |
Hey a, yr up.)    | 
            \     |
(Evaluation  \    |
time already? \   |
Hows my hair?) |  |
      |       /   |
      |  (It's    |
      |  fine.)  /
      |   /     /    
   {| a |m}   >>=   f

Ale w przypadku Nothing

        (Yah what is it?)       
(... There      |
is no a. )      |
  |        (No a?)
(No a.)         |
  |        (Ok, I'll deal
  |         with this.)
   \            |
    \      (Hey f, get lost.) 
     \          |   ( Where's my a? 
      \         |     I evaluate a)
       \    (Not any more  |
        \    you don't.    |
         |   We're returning
         |   Nothing.)   /
         |      |       /
         |      |      /
         |      |     /
   {| a |m}   >>=   f      (I got a b.)
                    |  (This is   \
                    |   such a     \
                    |   sham.) o o  \
                    |               o|
                    |--later-> {| b |m}

Więc monada Może pozwala na kontynuowanie obliczeń, jeśli faktycznie zawiera to, aco reklamuje, ale przerywa obliczenia, jeśli tego nie robi. Rezultatem jest jednak nadal kawałek danych monadycznych, choć nie wynik wyjściowy f. Z tego powodu monada Może reprezentuje kontekst niepowodzenia.

Różne monady zachowują się inaczej. Listy to inne typy danych z instancjami monadycznymi. Zachowują się następująco:

(Ok, here's your a. Well, its
 a bunch of them, actually.)
  |
  |    (Thanks, no problem. Ok
  |     f, here you go, an a.)
  |       |
  |       |        (Thank's. See
  |       |         you later.)
  |  (Whoa. Hold up f,      |
  |   I got another         |
  |   a for you.)           |
  |       |      (What? No, sorry.
  |       |       Can't do it. I 
  |       |       have my hands full
  |       |       with all these "b" 
  |       |       I just made.) 
  |  (I'll hold those,      |
  |   you take this, and   /
  |   come back for more  /
  |   when you're done   / 
  |   and we'll do it   / 
  |   again.)          /
   \      |  ( Uhhh. All right.)
    \     |       /    
     \    \      /
{| a |m}   >>=  f  

W tym przypadku funkcja wiedziała, jak utworzyć listę na podstawie danych wejściowych, ale nie wiedziała, co zrobić z dodatkowymi danymi wejściowymi i dodatkowymi listami. Wiązanie >>=pomogło f, łącząc wiele wyników. Podaję ten przykład, aby pokazać, że chociaż >>=jest on odpowiedzialny za rozpakowywanie a, ma również dostęp do ostatecznego związanego wyniku f. Rzeczywiście, nigdy go nie wyodrębni, achyba że będzie wiedział, że ostateczny wynik ma ten sam typ kontekstu.

Istnieją inne monady, które służą do reprezentowania różnych kontekstów. Oto kilka charakterystycznych cech kilku innych. IOMonada rzeczywistości nie mieć a, ale ona wie faceta i będzie to adla ciebie. State stMonada ma tajny zapas st, że to minie, aby fpod stołem, choć fpo prostu przyszedł z prośbą o a. Reader rMonada jest podobna do State st, choć tylko pozwala fprzyjrzeć r.

Chodzi o to, że każdy typ danych, które są zadeklarowane jako Monada, deklaruje pewien kontekst wokół wydobywania wartości z monady. Duży zysk z tego wszystkiego? Cóż, łatwo jest obliczyć obliczenia w pewnym kontekście. Może być jednak bałagan podczas łączenia wielu obliczeń obciążonych kontekstem. Operacje monady zajmują się rozwiązywaniem interakcji kontekstu, aby programista nie musiał.

Zauważ, że użycie tego rozwiązania >>=ułatwia bałagan, odbierając trochę autonomii f. Oznacza to, że w powyższym przypadku Nothingna przykład fnie można już zdecydować, co zrobić w przypadku Nothing; jest zakodowane w >>=. To jest kompromis. Gdyby było konieczne, faby zdecydować, co zrobić w przypadku Nothing, to fpowinna być funkcja od Maybe ado Maybe b. W tym przypadku,Maybe bycie monadą nie ma znaczenia.

Zauważ jednak, że czasami typ danych nie eksportuje swoich konstruktorów (patrząc na ciebie IO), a jeśli chcemy pracować z reklamowaną wartością, nie mamy innego wyboru, jak pracować z jej interfejsem monadycznym.

trevor cook
źródło
3

Monada to rzecz używana do enkapsulacji obiektów o zmiennym stanie. Najczęściej spotyka się go w językach, które inaczej nie pozwalają na modyfikowalny stan (np. Haskell).

Przykładem może być we / wy pliku.

Mógłbyś użyć monady dla pliku I / O, aby wyizolować zmienną naturę stanu tylko kodowi, który użył Monady. Kod wewnątrz Monady może skutecznie ignorować zmieniający się stan świata poza Monadą - znacznie ułatwia to rozumowanie ogólnego efektu twojego programu.

1800 INFORMACJE
źródło
3
Jak rozumiem, monady to coś więcej. Osadzanie stanu zmiennego w „czystych” językach funkcjonalnych to tylko jedna aplikacja monad.
thSoft