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.
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.
Odpowiedzi:
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 Error
monady 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
, digit
itp 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ź. async
Monada 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 async
monadę.)
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ą bind
operatora (pisane >>=
w języku Haskell). Ponieważ bind
operacja łą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
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 .
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
bind
może zrobić trochę więcej fmap
, ale nie odwrotnie. W rzeczywistości fmap
można go zdefiniować tylko w kategoriach bind
i return
. Definiując monadę, podajesz jej typ (tutaj był Wrapped a
), a następnie mówisz, jak działa return
i jak bind
dział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.
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.
Monada to typ danych, który ma dwie operacje: >>=
(aka bind
) i return
(aka unit
). return
przyjmuje 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 return
powinny 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 Just
i Nothing
są Maybe
konstruktorami. 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.
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 a
a b
) do funkcji T a -> T b
. Ta map
funkcja 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 p
i q
(notacja Haskella):
map id = id
map (p . q) = map p . map q
Na przykład konstruktor typu o nazwie List
jest funktorem, jeśli jest wyposażony w funkcję typu, (a -> b) -> List a -> List b
która jest zgodna z powyższymi prawami. Jedyne praktyczne wdrożenie jest oczywiste. Wynikowa List a -> List b
funkcja 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 T
z dwóch dodatkowych metod join
, typu T (T a) -> T a
i unit
(czasami nazywane return
, fork
lub 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 map
nad listą z funkcją, która zwraca listę. Join
pobiera wynikową listę list i łączy je. List
jest monadą, ponieważ jest to możliwe.
map
Nastę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 join
muszą być asocjacyjne. Oznacza to, że jeśli mają wartość x
typu [[[a]]]
następnie join (join x)
powinna być równa join (map join x)
. I pure
musi być tożsamość za join
takie, że join (pure x) == x
.
[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:
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 Maybe
lub an IO
.
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 Int
może być Just Int
lub Nothing
. Teraz, jeśli dodasz a Maybe Int
do Maybe Int
, operator sprawdzi, czy oba są w Just Int
środku, a jeśli tak, rozpakuje Int
s, przekaże im operator dodawania, ponownie zapakuje wynik Int
w 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 Int
są 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 Nothing
wszystko .
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 Int
s razem jest prawdopodobnie nonsensowne - nie jestem jeszcze pewien.) (Ponadto, jeśli zwróciłeś uwagę na Maybe
przykł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.
M (M a) -> M a
. To, że możesz zmienić to w jedno z typów, M a -> (a -> M b) -> M b
czyni je przydatnymi.
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:
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 lift
operator, 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.
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 y
wartość może stać się Mb
taka, jak na przykład, (is_OK, b)
jeśli y
zawiera 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 bool
wskazywania 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ć bool
część krotki.
(Ma -> Mb) >> (Mb -> Mc)
Tutaj znowu kompozycja występuje naturalnie, compose
wię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_OK
jest 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ć composition
zamiast compose
. Jako taki, zamiast po prostu przenosić wartości z wyjścia jednej funkcji na wejście innej Bind
, sprawdzałby M
część Ma
i zdecydował, czy i jak zastosować złożoną funkcję do a
. Oczywiście funkcja bind
zostałaby zdefiniowana specjalnie dla naszego konkretnego M
, aby móc sprawdzić jej strukturę i wykonać dowolny rodzaj aplikacji, jaki chcemy. Niemniej jednak, a
może to być cokolwiek, ponieważ bind
po prostu przekazuje a
niezauważ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 content
modelem, 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 bind
funkcji.
Dlatego monada to trzy rzeczy:
M
shell dla gospodarstwa monada istotnych informacji, bind
funkcje 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 a -> Mb
generują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 Mb
struktura wynikowa jest na ogół bardzo przydatna. Na przykład operator dzielenia nie zwraca liczby, gdy dzielnik jest 0
.
Dodatkowo, monad
s mogą obejmować funkcje zawijania, które zawijają wartości, a
w 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 bind
funkcji 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)
Associativity
oznacza, że bind
zachowuje kolejność oceny bez względu na bind
to, kiedy zostanie zastosowana. Oznacza to, że w definicji Associativity
powyżej, siły wczesnej oceny, czy w nawiasach binding
of f
a g
spowoduje tylko w funkcji, która oczekuje Ma
w celu zakończenia bind
. Stąd ocena Ma
musi zostać ustalona, zanim jej wartość będzie mogła zostać zastosowana, f
a wynik z kolei zastosowany do g
.
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:
(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.
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. if
Monada 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.
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.
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.
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.
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.
Monady nie są metaforami , ale praktycznie użyteczną abstrakcją wyłaniającą się ze wspólnego wzoru, jak wyjaśnia Daniel Spiewak.
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 data
a dane przepływają przez potoki.
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).
Jeśli dobrze zrozumiałem, IEnumerable pochodzi od monad. Zastanawiam się, czy to może być interesujący kąt podejścia dla tych z nas ze świata C #?
Oto, co warto, oto kilka linków do samouczków, które mi pomogły (i nie, wciąż nie rozumiem, co to są monady).
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.
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.
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 map
i 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
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, "">
f
przyjmuje 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 f
i g
oni 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 f
i g
musi 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 m
do f
”. Aby nakarmić parę <x, messages>
do funkcji f
jest 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 x
i 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 g
i 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 x
jest typu t
, to twoja para jest wartością typu „para t
i ciąg znaków”. Zadzwoń do tego typuM t
.
M
jest konstruktorem typów: M
sam nie odnosi się do typu, ale M _
odnosi się do typu po wypełnieniu go pustym typem. An M int
to 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.feed
przyjmuje (funkcja, która przyjmuje t
a zwraca an M u
) oraz an M t
i zwraca an M u
.wrap
bierze v
a zwraca an M v
.t
, u
i v
są 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 t
do funkcji jest równoznaczne z przekazaniem nieopakowanego t
do funkcji.
Formalnie: feed(f, wrap(x)) = f(x)
Karmienie M t
INTO wrap
nie robi nic do M t
.
Formalnie: feed(wrap, m) = m
Podaj M t
(wywołaj m
) do funkcji, która
t
wg
M u
(zadzwoń n
) zg
n
sięf
jest taki sam jak
m
dog
n
zg
n
dof
Formalnie: feed(h, m) = feed(f, feed(g, m))
gdzieh(x) := feed(f, g(x))
Zazwyczaj feed
nazywa się bind
(AKA >>=
in Haskell) i wrap
nazywa się return
.
Spróbuję wyjaśnić Monad
w 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 -> String
i f :: String -> Bool
.
Możemy to zrobić (f . g) x
, tak samo jak f (g x)
, gdzie x
jest Int
wartość.
Podczas tworzenia kompozycji / stosowania wyniku jednej funkcji do drugiej ważne jest dopasowanie typów. W powyższym przypadku typ zwróconego wyniku g
musi 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 Int
typ reprezentuje Int
wartość, która może nie istnieć, IO String
typ reprezentuje String
wartość występującą w wyniku wywołania pewnych efektów ubocznych.)
Powiedzmy, że teraz mamy g1 :: Int -> Maybe String
i f1 :: String -> Maybe Bool
. g1
i f1
są bardzo podobne do g
i f
odpowiednio.
Nie możemy zrobić (f1 . g1) x
lub f1 (g1 x)
, gdzie x
jest Int
wartość. Zwrócony typ wyniku g1
nie jest tym, cof1
oczekuje.
Mogliśmy komponować f
i g
z .
operatorem, ale teraz nie możemy komponować f1
i g1
z .
. 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 g1
i f1
takiego, który możemy pisać (f1 OPERATOR g1) x
? g1
zwraca 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 x
jest Maybe Int
wartością. >>=
Operator pomaga przyjąć, że Int
wartość 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 Monad
przydatny? Ponieważ Monad
jest to klasa typu, która definiuje >>=
operator, bardzo podobnie jak Eq
klasa typu, która definiuje operatory ==
i /=
.
Podsumowując, Monad
klasa 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 Monad
pozwala ona na składanie funkcji, które obejmuje wartości w kontekstach .
{-# 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
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 id
jest jego prawą i lewą tożsamością.
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 f
typó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 r
obejmują 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 a
i 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 t
do monadycznej kontekście m
. Aplikacja Extension lub Kleisli =<<
stosuje strzałkę Kleisli a -> m b
do 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 return
jest jego prawą i lewą tożsamością.
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
Typ opcji
data Maybe t = Nothing | Just t
koduje obliczenia, Maybe t
któ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 b
jest stosowany do wyniku tylko wtedy, gdy Maybe a
daje 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
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 x
strzałki Kleisli a -> [b]
do elementów [a]
pojedynczej listy wyników[b]
.
Niech właściwe dzielniki dodatniej liczby całkowitej n
bę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 fail
i <|>
tworzą monoidforall k l m.
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
i fail
jest absorbującym / anihilującym zerowym elementem monad addytywnych
_ =<< fail = fail
Jeśli w
guard (even p) >> return p
even p
jest 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.
Niesławnie monady są używane do kodowania obliczeń stanowych.
Procesor stan jest funkcją
forall st t. st -> (t, st)
który zmienia stan st
i 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 get
i put
metody abstrakcji nad stanowymi monadami:
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m -> st
deklaruje funkcjonalną zależność typu stanu st
od monady m
; że a State t
, na przykład, określi typ stanu jako t
unikalny.
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 void
w 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 :: Int
jest 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
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 IO
prymitywó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.
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 T
to odwzorowanie z kategorii C
na kategorię D
; dla każdego obiektu w C
obiekcie wD
Tobj : Obj(C) -> Obj(D)
f :: * -> *
i dla każdego morfizmu w C
morfizmie wD
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
gdzie X
, Y
są obiekty C
. HomC(X, Y)
jest klasą homomorfizmu wszystkich morfizmów X -> Y
w 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 C
jest 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 Hask
kategorii Kleisli
f* : T(X) -> T(Y)
(f =<<) :: m a -> m b
Skład w kategorii Kleisli .T
jest 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ę mu
w programowaniu zwanym join
. Monada jest definiowana mu
jako potrójny w stosunku do kategorii C
endofunkcji
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 mu
implementacja opcji monada:
instance Monad Maybe where
return = Just
join (Just m) = m
join Nothing = Nothing
concat
funkcja
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
jest join
monadą z listy.
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])
(f =<<) = concat . map f
Implementacje join
moż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 mu
na formularz rozszerzenia podano przez
f* = mu . T(f) : T(X) -> T(Y)
(f =<<) = join . map f :: m a -> m b
Philip Wadler: Monady do programowania funkcjonalnego
Simon L Peyton Jones, Philip Wadler: Imperatywne programowanie funkcjonalne
Jonathan MD Hill, Keith Clarke: Wprowadzenie do teorii kategorii, monady teorii kategorii i ich związek z programowaniem funkcjonalnym ´
Eugenio Moggi: Pojęcia obliczeń i monady
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
Świat potrzebuje kolejnego wpisu na blogu o monadach, ale myślę, że jest to przydatne do identyfikacji istniejących monad na wolności.
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.
http://code.google.com/p/monad-tutorial/ jest w toku, aby rozwiązać dokładnie to pytanie.
Niech poniższe „ {| a |m}
” reprezentują jakiś fragment danych monadycznych. Typ danych, który reklamuje a
:
(I got an a!)
/
{| a |m}
Funkcja f
wie, 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ę, f
pró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, f
znajduje sposób na wyodrębnienie a
za pomocą >>=
.
(Muaahaha. How you
like me now!?)
(Better.) \
| (Give me that a.)
(Fine, well ok.) |
\ |
{| a |m} >>= f
Mało f
wie, 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, a
co 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, a
chyba ż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. IO
Monada rzeczywistości nie mieć a
, ale ona wie faceta i będzie to a
dla ciebie. State st
Monada ma tajny zapas st
, że to minie, aby f
pod stołem, choć f
po prostu przyszedł z prośbą o a
. Reader r
Monada jest podobna do State st
, choć tylko pozwala f
przyjrzeć 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 Nothing
na przykład f
nie można już zdecydować, co zrobić w przypadku Nothing
; jest zakodowane w >>=
. To jest kompromis. Gdyby było konieczne, f
aby zdecydować, co zrobić w przypadku Nothing
, to f
powinna być funkcja od Maybe a
do 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.
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.