W kategoriach zrozumiałych dla programisty OOP (bez żadnego funkcjonalnego tła programowania), czym jest monada?
Jaki problem rozwiązuje i jakie są najczęściej używane miejsca?
EDYTOWAĆ:
Aby wyjaśnić rodzaj zrozumienia, którego szukałem, załóżmy, że konwertujesz aplikację FP, która miała monady, na aplikację OOP. Co byś zrobił, aby przenieść obowiązki monad na aplikację OOP?
Odpowiedzi:
AKTUALIZACJA: To pytanie było przedmiotem niezwykle długiej serii blogów, którą można przeczytać w Monadach - dzięki za świetne pytanie!
Monada to „wzmacniacz” typów, który przestrzega pewnych zasad i ma określone operacje .
Po pierwsze, czym jest „wzmacniacz typów”? Rozumiem przez to jakiś system, który pozwala ci wziąć typ i zmienić go w bardziej specjalny typ. Na przykład w C # rozważyć
Nullable<T>
. To jest wzmacniacz typów. Pozwala wybrać typ, powiedzmyint
, i dodać do niego nową funkcję, a mianowicie, że teraz może być zerowy, gdy wcześniej nie mógł.Jako drugi przykład rozważ
IEnumerable<T>
. To wzmacniacz typów. Pozwala wziąć typ, powiedzmy,string
i dodać do niego nową funkcję, mianowicie, że możesz teraz tworzyć sekwencję ciągów z dowolnej liczby pojedynczych ciągów.Jakie są „pewne zasady”? W skrócie, istnieje rozsądny sposób, aby funkcje typu bazowego działały na typie wzmocnionym, tak aby działały zgodnie z normalnymi zasadami składu funkcjonalnego. Na przykład, jeśli masz funkcję na liczbach całkowitych, powiedzmy
wtedy odpowiednia funkcja włączona
Nullable<int>
może sprawić, że wszyscy operatorzy i wywołania będą działać razem „w ten sam sposób”, co wcześniej.(Jest to niezwykle niejasne i nieprecyzyjne; poprosiłeś o wyjaśnienie, które nie zakładało niczego o znajomości składu funkcjonalnego).
Jakie są „operacje”?
Istnieje operacja „jednostkowa” (myląco nazywana czasami operacją „powrotu”), która pobiera wartość z typu zwykłego i tworzy równoważną wartość monadyczną. To w istocie zapewnia sposób na przyjęcie wartości typu nieamplifikowanego i przekształcenie jej w wartość typu amplifikowanego. Może być zaimplementowany jako konstruktor w języku OO.
Istnieje operacja „powiązania”, która przyjmuje wartość monadyczną i funkcję, która może przekształcić tę wartość i zwraca nową wartość monadyczną. Powiązanie jest kluczową operacją, która definiuje semantykę monady. Pozwala nam przekształcać operacje na typie nieamplifikowanym na operacje na typie amplifikowanym, który przestrzega wspomnianych wcześniej zasad składu funkcjonalnego.
Często istnieje sposób na odzyskanie niezamkniętego typu ze wzmacnianego typu. Ściśle mówiąc, ta operacja nie musi mieć monady. (Jest to konieczne, jeśli chcesz mieć comonad . Nie będziemy rozważać tych w dalszej części tego artykułu).
Ponownie weźmy
Nullable<T>
za przykład. Możesz zmienić anint
wNullable<int>
konstruktora. Kompilator C # zajmuje się najbardziej zerowym „podnoszeniem” dla Ciebie, ale jeśli nie, transformacja podnoszenia jest prosta: operacja, powiedzmy,przekształca się w
I obracając się
Nullable<int>
z powrotem doint
odbywa się przyValue
obiekcie.Kluczem jest transformacja funkcji. Zwróć uwagę, jak rzeczywista semantyka operacji zerowalnej - którą
null
propaguje operacjanull
- jest przechwytywana w transformacji. Możemy to uogólnić.Załóżmy, że masz funkcję od
int
doint
, jak nasz oryginałM
. Możesz łatwo przekształcić to w funkcję, która pobieraint
i zwraca a,Nullable<int>
ponieważ możesz po prostu uruchomić wynik przez konstruktor zerowalny. Załóżmy teraz, że masz tę metodę wyższego rzędu:Widzisz, co możesz z tym zrobić? Każda metoda, która przyjmuje
int
i zwraca anint
, lub przyjmujeint
i zwraca a,Nullable<int>
może teraz mieć przypisaną do niej semantykę zerowalną .Ponadto: załóżmy, że masz dwie metody
i chcesz je skomponować:
To jest,
Z
składX
iY
. Ale nie możesz tego zrobić, ponieważX
bierzeint
iY
zwraca aNullable<int>
. Ale ponieważ masz operację „powiązania”, możesz sprawić, aby działała:Operacja wiązania na monadzie sprawia, że kompozycja funkcji na wzmacnianych typach działa. „Reguły”, o których wspominałem powyżej, to to, że monada zachowuje reguły normalnego składu funkcji; że komponowanie z funkcjami tożsamości skutkuje funkcją oryginalną, ta kompozycja jest skojarzona i tak dalej.
W języku C # „Bind” nazywa się „SelectMany”. Zobacz, jak to działa na monadzie sekwencji. Potrzebujemy dwóch rzeczy: przekształcić wartość w sekwencję i powiązać operacje na sekwencjach. Jako bonus mamy również „zmienić sekwencję z powrotem w wartość”. Te operacje to:
Regułą zerowalnej monady było: „połączyć dwie funkcje, które razem wytwarzają wartości zerowe, sprawdź, czy wewnętrzna daje null; jeśli tak się stanie, wyzeruj, jeśli nie, to wywołaj zewnętrzną z wynikiem”. To pożądana semantyka zerowalności.
Reguła monady sekwencji to „połączyć dwie funkcje, które tworzą sekwencje razem, zastosować funkcję zewnętrzną do każdego elementu wytworzonego przez funkcję wewnętrzną, a następnie połączyć wszystkie powstałe sekwencje razem”. Podstawowa semantyka monad jest ujęta w metodach
Bind
/SelectMany
; jest to metoda, która mówi ci, co tak naprawdę oznacza monada .Możemy zrobić jeszcze lepiej. Załóżmy, że masz sekwencje liczb całkowitych i metodę, która przyjmuje liczby całkowite i daje w wyniku ciągi znaków. Możemy uogólnić operację wiązania, aby umożliwić zestawienie funkcji, które pobierają i zwracają różne typy wzmocnione, o ile dane wejściowe jednego pasują do wyników drugiego:
Teraz możemy powiedzieć: „amplifikuj tę wiązkę pojedynczych liczb całkowitych w sekwencję liczb całkowitych. Przekształć tę konkretną liczbę całkowitą w wiązkę ciągów, wzmocnioną w sekwencję ciągów. Teraz połącz obie operacje razem: wzmocnij tę wiązkę liczb całkowitych w konkatenacji wszystkie sekwencje ciągów znaków. ” Monady pozwalają komponować swoje wzmocnienia.
To raczej jak pytanie „jakie problemy rozwiązuje wzór singletonu?”, Ale spróbuję.
Monady są zwykle używane do rozwiązywania problemów, takich jak:
C # używa monad w swojej konstrukcji. Jak już wspomniano, zerowalny wzór jest bardzo podobny do „może monady”. LINQ jest całkowicie zbudowany z monad;
SelectMany
metody jest to, co robi semantycznej pracę składzie operacji. (Erik Meijer lubi podkreślać, że każda funkcja LINQ mogłaby być faktycznie zaimplementowanaSelectMany
; wszystko inne to tylko wygoda).Większość języków OOP nie ma wystarczająco bogatego systemu typów, aby bezpośrednio reprezentować sam wzorzec monady; potrzebujesz systemu typów, który obsługuje typy wyższe niż typy ogólne. Więc nie próbowałbym tego zrobić. Zamiast tego zaimplementowałbym ogólne typy reprezentujące każdą monadę i zaimplementowałbym metody reprezentujące trzy potrzebne operacje: przekształcenie wartości w wartość amplifikowaną, (być może) przekształcenie wartości amplifikowanej w wartość i przekształcenie funkcji w wartościach nieamplifikowanych w funkcja wzmocnionych wartości.
Dobrym miejscem na początek jest to, jak wdrożyliśmy LINQ w C #. Przestudiuj
SelectMany
metodę; jest to klucz do zrozumienia, jak monada sekwencji działa w języku C #. Jest to bardzo prosta metoda, ale bardzo potężna!Sugerowane dalsze czytanie:
źródło
Dlaczego potrzebujemy monad?
Mamy więc pierwszy duży problem. To jest program:
f(x) = 2 * x
g(x,y) = x / y
Jak możemy powiedzieć, co należy najpierw wykonać ? Jak możemy utworzyć uporządkowaną sekwencję funkcji (tj. Program ), używając tylko funkcji?
Rozwiązanie: komponuj funkcje . Jeśli chcesz najpierw
g
, a następnief
, po prostu napiszf(g(x,y))
. Dobrze, ale ...Więcej problemów: niektóre funkcje mogą zawieść (tzn.
g(2,0)
Podzielić przez 0). W FP nie mamy żadnych „wyjątków” . Jak to rozwiązujemy?Rozwiązanie: Pozwólmy funkcjom zwrócić dwie rzeczy : zamiast mieć
g : Real,Real -> Real
(funkcja z dwóch rzeczywistych w rzeczywistą), pozwólmyg : Real,Real -> Real | Nothing
(funkcja z dwóch rzeczywistych w rzeczywistą lub rzeczywistą).Ale funkcje powinny (mówiąc prościej) zwracać tylko jedną rzecz .
Rozwiązanie: stwórzmy nowy typ danych, które mają zostać zwrócone, „ typ boksu ”, który może zawierać rzeczywisty lub po prostu nic. Dlatego możemy mieć
g : Real,Real -> Maybe Real
. Dobrze, ale ...Co się teraz stanie
f(g(x,y))
?f
nie jest gotowy do spożyciaMaybe Real
. I nie chcemy zmieniać każdej funkcji, z którą możemy się połączyć,g
aby zużyćMaybe Real
.Rozwiązanie: skorzystajmy ze specjalnej funkcji do łączenia funkcji / łączenia / tworzenia / łączenia . W ten sposób możemy za kulisami dostosować wyjście jednej funkcji, aby zasilić następną.
W naszym przypadku:
g >>= f
(connect / komponowaćg
sięf
). Chcemy>>=
uzyskaćg
dane wyjściowe, sprawdzić je, a jeśliNothing
tak, to nie dzwońf
i nie wracajNothing
; lub wręcz przeciwnie, wyodrębnij pudełkoReal
i karmf
je nim. (Ten algorytm jest właśnie realizacja>>=
dlaMaybe
typu).Powstaje wiele innych problemów, które można rozwiązać za pomocą tego samego wzorca: 1. Użyj „pola”, aby skodyfikować / zapisać różne znaczenia / wartości, i takie funkcje
g
zwracają te „wartości w ramkach”. 2. Mieć kompozytorów / linkerów,g >>= f
którzy pomogą połączyćg
wyjście zf
wejściem, więc nie musimy się wcale zmieniaćf
.Niezwykłe problemy, które można rozwiązać za pomocą tej techniki:
posiadające globalny stan, w którym każda funkcja w sekwencji funkcji („program”) może współdzielić: rozwiązanie
StateMonad
.Nie lubimy „nieczystych funkcji”: funkcji, które dają różne dane wyjściowe dla tego samego wejścia. Dlatego zaznaczmy te funkcje, aby zwróciły wartość oznaczoną / umieszczoną w ramce:
IO
monad.Całkowite szczęście !!!!
źródło
State
iIO
bez podania żadnego z nich oraz dokładnego znaczenia „monady”Powiedziałbym, że najbliższą analogią OO do monad jest „ wzorzec poleceń ”.
We wzorcu poleceń zawijasz zwykłą instrukcję lub wyrażenie w obiekcie polecenia . Obiekt polecenia ujawnia metodę wykonania , która wykonuje zawiniętą instrukcję. Tak więc instrukcje są przekształcane w obiekty pierwszej klasy, które mogą być przekazywane i wykonywane do woli. Polecenia można komponować, dzięki czemu można utworzyć obiekt programu przez połączenie i zagnieżdżenie obiektów poleceń.
Polecenia są wykonywane przez osobny obiekt, wywołujący . Zaletą używania wzorca poleceń (zamiast wykonywania szeregu zwykłych instrukcji) jest to, że różni wywołujący mogą stosować inną logikę do sposobu wykonywania poleceń.
Wzorzec poleceń można wykorzystać do dodania (lub usunięcia) funkcji językowych, które nie są obsługiwane przez język hosta. Na przykład w hipotetycznym języku OO bez wyjątków można dodać semantykę wyjątków, udostępniając komendom metody „try” i „throw”. Gdy wywołania polecenia rzucają, wywołujący przegląda listę (lub drzewo) poleceń aż do ostatniego wywołania „try”. I odwrotnie, możesz usunąć semantyczny wyjątek z języka (jeśli uważasz, że wyjątki są złe ), przechwytując wszystkie wyjątki zgłaszane przez poszczególne polecenia i przekształcając je w kody błędów, które są następnie przekazywane do następnego polecenia.
Jeszcze bardziej wymyślną semantykę wykonywania, taką jak transakcje, wykonanie niedeterministyczne lub kontynuacje, można zaimplementować w ten sposób w języku, który nie obsługuje go natywnie. Jest to dość potężny wzór, jeśli się nad tym zastanowić.
Teraz w rzeczywistości wzorce poleceń nie są używane jako ogólna funkcja języka taka jak ta. Narzut związany z przekształceniem każdej instrukcji w osobną klasę doprowadziłby do nie do zniesienia ilości kodu typu „kocioł”. Ale w zasadzie można go użyć do rozwiązania tych samych problemów, co monady do rozwiązania w fp.
źródło
W zakresie programowania obiektowego monada jest interfejs (lub bardziej prawdopodobne wstawek), parametryzowane przez typ z dwoma metodami,
return
abind
które opisują:Problem, który rozwiązuje, to ten sam typ problemu, jakiego można oczekiwać od dowolnego interfejsu, a mianowicie: „Mam kilka różnych klas, które robią różne rzeczy, ale wydaje się, że robią te różne rzeczy w sposób, który ma podstawowe podobieństwo. czy mogę opisać to podobieństwo między nimi, nawet jeśli same klasy nie są tak naprawdę podtypami czegoś bliższego niż sama klasa „Object”? ”
Mówiąc dokładniej,
Monad
„interfejs” jest podobnyIEnumerator
lubIIterator
przyjmuje typ, który sam przyjmuje typ. Jednak głównym „punktem”Monad
jest możliwość łączenia operacji opartych na typie wnętrza, nawet do takiego stopnia, że ma nowy „typ wewnętrzny”, zachowując - a nawet wzmacniając - strukturę informacyjną głównej klasy.źródło
return
tak naprawdę nie byłby metodą na monadzie, ponieważ nie bierze ona pod uwagę instancji monady jako argumentu. (tj .: nie ma tego / ja)Masz ostatnią prezentację „ Monadologie - profesjonalna pomoc na temat lęku typu ” autorstwa Christophera League (12 lipca 2010 r.), Która jest dość interesująca na tematy kontynuacji i monady.
Wideo z prezentacją (udostępnianie slajdów) jest faktycznie dostępne na vimeo .
Część Monada rozpoczyna się około 37 minut od tego godzinnego wideo i zaczyna się od slajdu 42 z 58 prezentacji.
Jest on przedstawiany jako „wiodący wzorzec projektowy dla programowania funkcjonalnego”, ale językiem użytym w przykładach jest Scala, który jest zarówno OOP, jak i funkcjonalny.
Więcej na temat Monady w Scali można przeczytać w poście na blogu „ Monady - inny sposób na abstrakcyjne obliczenia w Scali ”, z Debasish Ghosh (27 marca 2008).
Na przykład (w Scali):
Option
jest monadąList
jest MonadaMonada to wielka sprawa w Scali ze względu na wygodną składnię zbudowaną z myślą o wykorzystaniu struktur Monady:
for
rozumienie w Scali :jest tłumaczony przez kompilator na:
Kluczową abstrakcją jest ta
flatMap
, która wiąże obliczenia poprzez łańcuch.Każde wywołanie
flatMap
zwraca ten sam typ struktury danych (ale o innej wartości), który służy jako dane wejściowe do następnego polecenia w łańcuchu.W powyższym fragmencie flatMap przyjmuje jako dane wejściowe zamknięcie
(SomeType) => List[AnotherType]
i zwraca aList[AnotherType]
. Należy zauważyć, że wszystkie płaskie mapy przyjmują ten sam typ zamknięcia co dane wejściowe i zwracają ten sam typ co dane wyjściowe.Właśnie to „wiąże” wątek obliczeniowy - każdy element sekwencji w zrozumieniu musi spełniać to samo ograniczenie typu.
Jeśli wykonasz dwie operacje (które mogą się nie powieść) i przekażesz wynik trzeciej, na przykład:
ale bez korzystania z Monady otrzymujesz zawiły kod OOP, taki jak:
mając na uwadze, że dzięki Monadzie możesz pracować z typami rzeczywistymi (
Venue
,User
), tak jak działają wszystkie operacje, i ukrywać elementy weryfikacji Opcji, wszystko z powodu płaskich map składni for:Część wydajności zostanie wykonana tylko wtedy, gdy wszystkie trzy funkcje mają
Some[X]
; wszelkieNone
zostaną bezpośrednio zwrócone doconfirm
.Więc:
Nawiasem mówiąc, Monada to nie tylko model obliczeniowy stosowany w FP:
źródło
Aby uszanować szybkich czytelników, najpierw zaczynam od precyzyjnej definicji, kontynuuję szybkie wyjaśnienia w języku angielskim, a następnie przechodzę do przykładów.
Oto nieco zwięzła i precyzyjna definicja nieco przeredagowana:
Tak więc w prostych słowach monada jest regułą przekazywaną z dowolnego typu
X
na inny typT(X)
oraz regułą przekazywaną z dwóch funkcjif:X->T(Y)
ig:Y->T(Z)
(którą chciałbyś skomponować, ale nie możesz) do nowej funkcjih:X->T(Z)
. Który jednak nie jest kompozycją w ścisłym sensie matematycznym. Zasadniczo zajmujemy się kompozycją funkcji „zginania” lub redefiniowaniem sposobu, w jaki funkcje są tworzone.Dodatkowo, wymagamy reguły komponowania monady, aby spełnić „oczywiste” matematyczne aksjomaty:
f
sięg
, a następnie zh
(od zewnątrz), powinny być takie same jak komponowaćg
zh
a następnief
(od wewnątrz).f
z funkcją tożsamości po obu stronach powinno daćf
.Ponownie, w prostych słowach, nie możemy po prostu zwariować, zmieniając skład naszej funkcji tak, jak lubimy:
f(g(h(k(x)))
, i nie martwić się o określenie par funkcji tworzących porządek. Ponieważ reguła monady określa tylko, jak skomponować parę funkcji , bez tego aksjomatu musielibyśmy wiedzieć, która para składa się jako pierwsza i tak dalej. (Należy pamiętać, że różni się od właściwości komutatywności,f
z którą skomponowaneg
były takie same, jakg
zf
, która nie jest wymagana).Tak więc w skrócie: monada jest regułą rozszerzenia typu i tworzenia funkcji spełniających dwa aksjomaty - asocjatywność i właściwość jedności.
W praktyce chcesz, aby monada została zaimplementowana dla Ciebie przez język, kompilator lub środowisko, które zajmą się tworzeniem dla Ciebie funkcji. Możesz więc skupić się na pisaniu logiki funkcji, zamiast martwić się o to, jak zostanie wykonana ich realizacja.
To w gruncie rzeczy w skrócie.
Jako profesjonalny matematyk wolę unikać nazywania
h
„kompozycją”f
ig
. Ponieważ matematycznie tak nie jest. Nazywanie go „kompozycją” niepoprawnie zakłada, żeh
jest to prawdziwa kompozycja matematyczna, a tak nie jest. Nie jest to nawet jednoznacznie określone przezf
ig
. Zamiast tego jest to wynikiem nowej „zasady komponowania” naszych monad. Który może całkowicie różnić się od faktycznego składu matematycznego, nawet jeśli ten drugi istnieje!Aby uczynić go mniej suchym, pozwól mi zilustrować go przykładem, który adnotuję za pomocą małych sekcji, abyś mógł przejść od razu do rzeczy.
Zgłaszanie wyjątków jako przykłady Monady
Załóżmy, że chcemy skomponować dwie funkcje:
Ale
f(0)
nie jest zdefiniowany, dlatego zgłaszanye
jest wyjątek . Jak zatem zdefiniować wartość składug(f(0))
? Oczywiście ponownie rzuć wyjątek! Może to samoe
. Może nowy zaktualizowany wyjąteke1
.Co dokładnie się tutaj dzieje? Po pierwsze, potrzebujemy nowych wartości wyjątku (różnych lub takich samych). Można do nich zadzwonić
nothing
lubnull
czy cokolwiek, ale istota pozostaje ta sama - powinny one być nowe wartości, na przykład, że nie powinno byćnumber
w naszym przykładzie tutaj. Wolę nie dzwonić do nich,null
aby uniknąć pomyłek z tym, jaknull
można je zaimplementować w dowolnym języku. Taknothing
samo wolę unikać, ponieważ często się z tym kojarzynull
, co w zasadzie jest tym, conull
należy zrobić, jednak zasada ta często się nagina z jakichkolwiek praktycznych powodów.Czym dokładnie jest wyjątek?
Jest to trywialna sprawa dla każdego doświadczonego programisty, ale chciałbym dodać kilka słów, aby zgasić robaka zamieszania:
Wyjątek to obiekt kapsułkujący informacje o tym, jak wystąpił nieprawidłowy wynik wykonania.
Może to polegać na wyrzuceniu jakichkolwiek szczegółów i zwróceniu pojedynczej wartości globalnej (jak
NaN
lubnull
) lub wygenerowaniu długiej listy dziennika lub tego, co się dokładnie wydarzyło, wysłaniu jej do bazy danych i replikacji w całej rozproszonej warstwie przechowywania danych;)Ważną różnicą między tymi dwoma skrajnymi przykładami wyjątku jest to, że w pierwszym przypadku nie występują żadne skutki uboczne . W drugim są. Co prowadzi nas do pytania (w tysiącach dolarów):
Czy dozwolone są wyjątki w czystych funkcjach?
Krótsza odpowiedź : tak, ale tylko wtedy, gdy nie prowadzą do skutków ubocznych.
Dłuższa odpowiedź. Aby być czystym, wynik funkcji musi być jednoznacznie określony przez jej dane wejściowe. Dlatego zmieniamy naszą funkcję
f
, wysyłając0
do nowej wartości abstrakcyjneje
, którą nazywamy wyjątkiem. Zapewniamy, że wartośće
nie zawiera żadnych informacji zewnętrznych, które nie są jednoznacznie określone przez nasze dane wejściowe, czylix
. Oto przykład wyjątku bez efektu ubocznego:A oto jeden z efektem ubocznym:
W rzeczywistości ma skutki uboczne tylko wtedy, gdy ta wiadomość może się zmienić w przyszłości. Ale jeśli gwarantuje się, że nigdy się nie zmieni, wartość ta staje się wyjątkowo przewidywalna, a zatem nie występuje efekt uboczny.
Aby było jeszcze głupiej. Funkcja powracająca
42
kiedykolwiek jest wyraźnie czysta. Ale jeśli ktoś szalony zdecyduje się42
na zmienną, której wartość może się zmienić, ta sama funkcja przestaje być czysta w nowych warunkach.Zauważ, że używam notacji dosłowności obiektowej dla uproszczenia, aby zademonstrować istotę. Niestety rzeczy są pomieszane się w językach takich jak JavaScript, gdzie
error
nie jest to typ, który zachowuje się tak jak chcemy tutaj w odniesieniu do kompozycji funkcyjnego, natomiast rzeczywistych typów podobnychnull
lubNaN
nie zachowują się w ten sposób, ale raczej przejść przez jakiś sztuczny i nie zawsze intuicyjny typ konwersji.Rozszerzenie typu
Ponieważ chcemy zmienić komunikat w naszym wyjątku, naprawdę ogłaszamy nowy typ
E
dla całego obiektu wyjątku, a następnie to właśniemaybe number
robi, oprócz mylącej nazwy, która ma być albo typem,number
albo nowym typem wyjątkuE
, więc jest naprawdę związeknumber | E
znumber
iE
. W szczególności zależy to od tego, jak chcemy zbudowaćE
, co nie jest ani sugerowane, ani odzwierciedlone w nazwiemaybe number
.Co to jest skład funkcjonalny?
Jest matematyczne funkcje Wykonywanie operacji
f: X -> Y
ig: Y -> Z
i budowaniu ich skład jako funkcjęh: X -> Z
zaspokajaniah(x) = g(f(x))
. Problem z tą definicją występuje, gdy wynikf(x)
nie jest dozwolony jako argumentg
.W matematyce tych funkcji nie da się ułożyć bez dodatkowej pracy. Rozwiązanie ściśle matematyczne dla powyższego przykładu
f
ig
ma zostać usunięte0
ze zbioru definicjif
. Dzięki temu nowemu zestawowi definicji (nowemu, bardziej restrykcyjnemu typowix
)f
staje się on kompatybilny zg
.Ograniczenie zestawu takich definicji nie jest jednak zbyt praktyczne w programowaniu
f
. Zamiast tego można zastosować wyjątki.Lub innym podejściu wartości tworzone są sztuczne jak
NaN
,undefined
,null
,Infinity
itd. Więc ocenia1/0
sięInfinity
i1/-0
do-Infinity
. A następnie wymuś nową wartość z powrotem do wyrażenia zamiast zgłaszania wyjątku. Prowadząc do wyników, które możesz, ale nie musisz, przewidzieć:Wróciliśmy do regularnych liczb gotowych do przejścia;)
JavaScript pozwala nam nadal wykonywać wyrażenia liczbowe za wszelką cenę bez zgłaszania błędów, jak w powyższym przykładzie. Oznacza to, że pozwala także komponować funkcje. Na tym właśnie polega monada - regułą jest komponowanie funkcji spełniających aksjomaty zdefiniowane na początku tej odpowiedzi.
Ale czy zasada komponowania funkcji wynikająca z implementacji JavaScript do obsługi błędów numerycznych to monada?
Aby odpowiedzieć na to pytanie, wystarczy sprawdzić aksjomaty (pozostawione jako ćwiczenie jako część pytania tutaj;).
Czy można zastosować wyjątek rzucania do skonstruowania monady?
Rzeczywiście, bardziej użyteczną monadą byłaby zamiast tego reguła, która mówi, że jeśli
f
zgłasza wyjątek dla niektórychx
, to i jego skład z każdymg
. Plus sprawiają, że wyjątek ten jestE
unikalny na całym świecie i ma tylko jedną możliwą wartość ( obiekt końcowy w teorii kategorii). Teraz dwa aksjomaty są natychmiast sprawdzalne i otrzymujemy bardzo przydatną monadę. Rezultatem jest tak zwana być może monada .źródło
Monada to typ danych, który hermetyzuje wartość i do którego zasadniczo można zastosować dwie operacje:
return x
tworzy wartość typu monada, która jest hermetyzowanax
m >>= f
(czytaj jako „operator powiązania”) stosuje funkcjęf
do wartości w monadziem
Taka jest monada. Jest jeszcze kilka szczegółów technicznych , ale zasadniczo te dwie operacje definiują monadę. Prawdziwe pytanie brzmi: „Co robi monada ?”, A to zależy od monady - listy to monady, Maybes to monady, operacje IO to monady. Gdy mówimy, że te rzeczy są monadami, oznacza to, że mają one interfejs monad
return
i>>=
.źródło
bind
funkcji, którą należy zdefiniować dla każdego typu monady , prawda? Byłby to dobry powód, aby nie mylić wiązania z kompozycją, ponieważ istnieje jedna definicja dla kompozycji, podczas gdy nie może istnieć tylko jedna definicja dla funkcji wiązania, istnieje jedna na typ monadyczny, jeśli dobrze rozumiem.Z wikipedii :
Wierzę, że to wyjaśnia to bardzo dobrze.
źródło
Spróbuję stworzyć najkrótszą definicję, którą mogę zarządzać, używając terminów OOP:
Klasa ogólna
CMonadic<T>
to monada, jeśli definiuje co najmniej następujące metody:oraz jeżeli następujące prawa mają zastosowanie do wszystkich typów T i ich możliwych wartości t
lewa tożsamość:
właściwa tożsamość
skojarzenie:
Przykłady :
Monada listy może mieć:
A flatMap na liście [1,2,3] może działać tak:
Iterabile i Obserwowalne mogą być również monadyczne, a także obietnice i zadania.
Komentarz :
Monady nie są takie skomplikowane.
flatMap
Funkcja jest bardzo podobny bardziej powszechnie spotykanemap
. Otrzymuje argument funkcji (zwany także delegatem), który może wywoływać (natychmiast lub później, zero lub więcej razy) z wartością pochodzącą z klasy ogólnej. Oczekuje, że przekazana funkcja zawinie również wartość zwracaną w ten sam rodzaj klasy ogólnej. Aby temu zaradzić, zapewniacreate
konstruktor, który może utworzyć instancję tej klasy ogólnej na podstawie wartości. Zwracany wynik flatMap jest także ogólną klasą tego samego typu, często pakującą te same wartości, które były zawarte w wynikach zwracanych przez jedną lub więcej aplikacji flatMap do wcześniej zawartych wartości. Pozwala to na łączenie flatMap w dowolny sposób:Tak się składa, że ten rodzaj klasy ogólnej jest użyteczny jako model podstawowy dla ogromnej liczby rzeczy. To (wraz z żargonizmem teorii kategorii) jest przyczyną, dla której Monady wydają się tak trudne do zrozumienia lub wyjaśnienia. Są bardzo abstrakcyjną rzeczą i stają się oczywiście przydatne dopiero po specjalizacji.
Na przykład można modelować wyjątki za pomocą kontenerów monadycznych. Każdy pojemnik będzie zawierał wynik operacji lub błąd, który wystąpił. Następna funkcja (delegata) w łańcuchu wywołań zwrotnych flatMap zostanie wywołana tylko wtedy, gdy poprzednia zapakowała wartość do kontenera. W przeciwnym razie, jeśli błąd zostanie spakowany, błąd będzie kontynuowany przez powiązane łańcuchy, aż do znalezienia kontenera z funkcją obsługi błędów podłączoną za pomocą metody o nazwie
.orElse()
(taka metoda byłaby dozwolonym rozszerzeniem)Uwagi : Języki funkcjonalne umożliwiają pisanie funkcji, które mogą działać na dowolnym rodzaju monadycznej klasy ogólnej. Aby to zadziałało, należałoby napisać ogólny interfejs dla monad. Nie wiem, czy można napisać taki interfejs w języku C #, ale o ile wiem, nie jest to:
źródło
To, czy monada ma „naturalną” interpretację w OO, zależy od monady. W języku takim jak Java możesz przetłumaczyć być może monadę na język sprawdzania zerowych wskaźników, dzięki czemu obliczenia, które zawodzą (tj. Nie produkują nic w Haskell), emitują zerowe wskaźniki jako wyniki. Możesz przetłumaczyć monadę stanu na język generowany przez utworzenie zmiennej zmiennej i metody zmiany jej stanu.
Monada to monoid w kategorii endofunkorów.
Informacje, które łączy zdanie, są bardzo głębokie. I pracujesz w monadzie z dowolnym imperatywnym językiem. Monada jest językiem specyficznym dla domeny. Spełnia pewne interesujące właściwości, które razem sprawiają, że monada jest matematycznym modelem „programowania imperatywnego”. Haskell ułatwia definiowanie małych (lub dużych) języków imperatywnych, które można łączyć na różne sposoby.
Jako programista OO, używasz hierarchii klas swojego języka do organizowania rodzajów funkcji lub procedur, które można wywoływać w kontekście, który nazywasz obiektem. Monada jest również abstrakcją tego pomysłu, o ile różne monady można łączyć w dowolny sposób, skutecznie „importując” wszystkie metody sub-monady do zakresu.
Architektonicznie następnie używa się podpisów typu, aby jednoznacznie wyrazić, które konteksty mogą być użyte do obliczenia wartości.
W tym celu można zastosować transformatory monadowe, a kolekcja „standardowych” monad jest wysokiej jakości:
z odpowiednimi transformatorami monadowymi i klasami typów. Klasy typów umożliwiają komplementarne podejście do łączenia monad poprzez ujednolicenie ich interfejsów, dzięki czemu konkretne monady mogą implementować standardowy interfejs dla monady „rodzaju”. Na przykład moduł Control.Monad.State zawiera klasę MonadState sm, a (stan) jest instancją formularza
Długa historia mówi, że monada jest funktorem, który łączy „kontekst” z wartością, który ma sposób na wstrzyknięcie wartości monadzie i który ma sposób oceny wartości w odniesieniu do kontekstu z nią związanego, przynajmniej w ograniczony sposób.
Więc:
jest funkcją, która wstrzykuje wartość typu a do monadowej „akcji” typu ma.
jest funkcją, która wykonuje akcję monady, ocenia jej wynik i stosuje funkcję do wyniku. Fajne w (>> =) jest to, że wynik jest w tej samej monadzie. Innymi słowy, wm >> = f, (>> =) wyciąga wynik z m i wiąże go zf, tak aby wynik był w monadzie. (Alternatywnie możemy powiedzieć, że (>> =) ściąga f do m i stosuje go do wyniku.) W konsekwencji, jeśli mamy f :: a -> mb i g :: b -> mc, możemy działania „sekwencyjne”:
Lub używając „notacji”
Typ dla (>>) może być podświetlony. To jest
Odpowiada operatorowi (;) w językach proceduralnych, takich jak C. Pozwala na notację:
W logice matematycznej i filozoficznej mamy ramy i modele, które są „naturalnie” modelowane monadyzmem. Interpretacja to funkcja, która analizuje domenę modelu i oblicza wartość prawdy (lub uogólnienia) zdania (lub formuły, w uogólnieniu). W logice modalnej z konieczności możemy powiedzieć, że propozycja jest konieczna, jeśli jest prawdziwa w „każdym możliwym świecie” - jeśli jest prawdziwa w odniesieniu do każdej dopuszczalnej dziedziny. Oznacza to, że model propozycji w języku może zostać zreifikowany jako model, którego domena składa się ze zbioru odrębnych modeli (jednego odpowiadającego każdemu możliwemu światowi). Każda monada ma metodę o nazwie „łączyć”, która spłaszcza warstwy, co oznacza, że każda akcja monady, której wynikiem jest akcja monady, może zostać osadzona w monadzie.
Co ważniejsze, oznacza to, że monada jest zamknięta w ramach operacji „układania warstw”. Oto jak działają transformatory monadowe: łączą one monady, zapewniając metody typu łączenia dla typów podobnych
abyśmy mogli przekształcić akcję w (MożeT m) w akcję wm, skutecznie zwijając warstwy. W tym przypadku metoda runMaybeT :: MaybeT ma -> m (Może a) jest naszą metodą łączenia. (Może T m) to monada, a Może T :: m (Może a) -> Może T ma skutecznie konstruktor nowego typu działania monady w m.
Wolna monada dla funktora to monada generowana przez układanie w stosy f, co implikuje, że każda sekwencja konstruktorów dla f jest elementem wolnej monady (lub, dokładniej, czymś o tym samym kształcie, co drzewo sekwencji konstruktorów dla fa). Darmowe monady to przydatna technika do konstruowania elastycznych monad przy minimalnej ilości płyty kotłowej. W programie Haskell mógłbym użyć wolnych monad do zdefiniowania prostych monad do „programowania systemu na wysokim poziomie”, aby pomóc w utrzymaniu bezpieczeństwa typów (używam tylko typów i ich deklaracji. Implementacje są proste przy użyciu kombinatorów):
Monadyzm jest architekturą leżącą u podstaw wzorca, który można by nazwać „tłumaczem” lub „dowództwem”, wyabstrahowanym do najczystszej postaci, ponieważ każde obliczenie monadyczne musi być „uruchomione”, przynajmniej trywialnie. (System wykonawczy uruchamia dla nas monadę IO i jest punktem wejścia do dowolnego programu Haskell. IO „steruje” resztą obliczeń, uruchamiając kolejno akcje IO).
Typ złączenia polega również na tym, że otrzymujemy stwierdzenie, że monada jest monoidem w kategorii endofunkcji. Łączenie jest zazwyczaj ważniejsze ze względów teoretycznych ze względu na swój typ. Ale zrozumienie tego typu oznacza zrozumienie monad. Złączenia i typy złącz transformatora monad są efektywnie kompozycjami endofunkcji w sensie kompozycji funkcji. Aby umieścić go w pseudojęzycznym języku podobnym do Haskella,
Foo :: m (ma) <-> (m. M) a
źródło
Monada to tablica funkcji
(Pst: tablica funkcji to tylko obliczenie).
W rzeczywistości zamiast prawdziwej tablicy (jedna funkcja w jednej tablicy komórkowej) masz te funkcje powiązane inną funkcją >> =. >> = pozwala dostosować wyniki z funkcji i do karmienia funkcji i + 1, wykonać obliczenia między nimi, a nawet nie wywoływać funkcji i + 1.
Zastosowane tutaj typy to „typy z kontekstem”. Jest to wartość z „znacznikiem”. Łańcuchowe funkcje muszą przyjąć „nagą wartość” i zwrócić oznaczony wynik. Jednym z obowiązków >> = jest wydobycie nagiej wartości z kontekstu. Istnieje również funkcja „return”, która przyjmuje pustą wartość i umieszcza ją w tagu.
Przykład z Może . Użyjmy go do zapisania prostej liczby całkowitej, na której dokonamy obliczeń.
Aby pokazać, że monady to tablica funkcji z operacjami pomocniczymi, rozważ odpowiednik powyższego przykładu, używając tylko prawdziwej tablicy funkcji
I byłby używany w następujący sposób:
źródło
>>=
jest operatorem\x -> x >>= k >>= l >>= m
jest tablicą funkcji, to znaczyh . g . f
, że w ogóle nie dotyczy to monad.Pod względem OO monada to płynny pojemnik.
Minimalne wymaganie to definicja,
class <A> Something
która obsługuje konstruktorSomething(A a)
i co najmniej jedną metodęSomething<B> flatMap(Function<A, Something<B>>)
Prawdopodobnie ma to również znaczenie, jeśli klasa monad ma jakieś metody z podpisem,
Something<B> work()
które zachowują reguły klasy - kompilator piecze w flatMapie podczas kompilacji.Dlaczego monada jest przydatna? Ponieważ jest to kontener, który umożliwia operacje łańcuchowe, które zachowują semantykę. Na przykład,
Optional<?>
zachowuje semantykę isPresent dlaOptional<String>
,Optional<Integer>
,Optional<MyClass>
, itd.Jako szorstki przykład
Uwaga: zaczynamy od łańcucha, a kończymy na liczbie całkowitej. Całkiem fajne.
W OO może to zająć trochę machania ręką, ale każda metoda na czymś, co zwraca inną podklasę czegoś, spełnia kryterium funkcji kontenera, która zwraca kontener oryginalnego typu.
W ten sposób zachowujesz semantykę - tzn. Znaczenie kontenera i operacje się nie zmieniają, po prostu zawijają i ulepszają obiekt wewnątrz kontenera.
źródło
Monady w typowym użyciu są funkcjonalnym odpowiednikiem mechanizmów obsługi wyjątków programowania proceduralnego.
W nowoczesnych językach proceduralnych program obsługi wyjątków umieszcza się wokół sekwencji instrukcji, z których każda może generować wyjątek. Jeśli którakolwiek z instrukcji zgłasza wyjątek, normalne wykonanie sekwencji instrukcji zostaje zatrzymane i przekazane do procedury obsługi wyjątku.
Funkcjonalne języki programowania jednak filozoficznie unikają funkcji obsługi wyjątków ze względu na ich charakter „goto”. Perspektywa programowania funkcjonalnego polega na tym, że funkcje nie powinny mieć „skutków ubocznych”, takich jak wyjątki, które zakłócają przebieg programu.
W rzeczywistości efektów ubocznych nie można wykluczyć w świecie rzeczywistym przede wszystkim ze względu na operacje wejścia / wyjścia. Monady w programowaniu funkcjonalnym służą do obsługi tego przez przyjęcie zestawu powiązanych funkcji (każde z nich może dać nieoczekiwany wynik) i przekształcenie dowolnego nieoczekiwanego wyniku w hermetyzowane dane, które nadal mogą bezpiecznie przepływać przez pozostałe wywołania funkcji.
Przepływ kontroli jest zachowany, ale nieoczekiwane zdarzenie jest bezpiecznie obudowane i obsługiwane.
źródło
Proste wyjaśnienie Monad z studium przypadku Marvela znajduje się tutaj .
Monady to abstrakcje używane do skutecznego działania funkcji zależnych od sekwencji. Skuteczne tutaj oznacza, że zwracają typ w postaci F [A], na przykład Opcja [A], gdzie Opcja to F, zwany konstruktorem typu. Zobaczmy to w 2 prostych krokach
Jeśli jednak funkcja zwraca typ efektu, taki jak Opcja [A], tj. A => F [B], kompozycja nie działa, aby przejść do B, potrzebujemy A => B, ale mamy A => F [B].
Potrzebujemy specjalnego operatora „bind”, który wie, jak połączyć te funkcje, które zwracają F [A].
„Przypisane” funkcja jest określona w odniesieniu do konkretnego F .
Istnieje również „zwrotny” , typu A => F [W], dla każdego A , zdefiniowanym w odniesieniu do konkretnego F także. Aby być Monadą, F musi mieć zdefiniowane dwie funkcje.
Zatem możemy zbudować skuteczną funkcję A => F [B] z dowolnej czystej funkcji A => B ,
ale dany F może również definiować własne nieprzezroczyste „wbudowane” funkcje specjalne tego typu, których użytkownik nie może sam zdefiniować (w czystym języku), jak
źródło
Dzielę się swoim rozumieniem Monad, które mogą nie być teoretycznie doskonałe. Monady dotyczą propagacji kontekstu . Monada polega na tym, że definiujesz kontekst dla niektórych danych (lub typów danych), a następnie definiujesz sposób, w jaki kontekst będzie przenoszony z danymi przez cały proces przetwarzania. Definiowanie propagacji kontekstu polega przede wszystkim na definiowaniu sposobu łączenia wielu kontekstów (tego samego typu). Korzystanie z Monad oznacza również, że konteksty te nie zostaną przypadkowo usunięte z danych. Z drugiej strony inne dane bezkontekstowe można przenieść do nowego lub istniejącego kontekstu. Następnie można zastosować tę prostą koncepcję, aby zapewnić poprawność czasu kompilacji programu.
źródło
Jeśli kiedykolwiek korzystałeś z PowerShell, wzory, które opisał Eric, powinny brzmieć znajomo. Polecenia cmdlet programu PowerShell to monady; skład funkcjonalny jest reprezentowany przez potok .
Wywiad Jeffreya Snovera z Erikiem Meijerem jest bardziej szczegółowy.
źródło
Zobacz moją odpowiedź na „Co to jest monada?”
Zaczyna się od motywującego przykładu, przechodzi przez przykład, czerpie przykład monady i formalnie definiuje „monadę”.
Nie zakłada znajomości programowania funkcjonalnego i używa pseudokodu ze
function(argument) := expression
składnią z najprostszymi możliwymi wyrażeniami.Ten program C ++ jest implementacją monady pseudokodu. (Dla porównania:
M
jest konstruktorem typu,feed
jest operacją „powiązania” i operacjąwrap
„powrotu”).źródło
Z praktycznego punktu widzenia (podsumowując to, co zostało powiedziane w wielu poprzednich odpowiedziach i powiązanych artykułach) wydaje mi się, że jednym z podstawowych „celów” (lub użyteczności) monady jest wykorzystanie zależności ukrytych w wywołaniach metod rekurencyjnych aka skład funkcji (tj. gdy f1 wywołuje f2 wywołuje f3, f3 należy ocenić przed f2 przed f1), aby w naturalny sposób reprezentować skład sekwencyjny, szczególnie w kontekście leniwego modelu oceny (to znaczy składu sekwencyjnego jako prostej sekwencji , np. „f3 (); f2 (); f1 ();” w C - sztuczka jest szczególnie oczywista, jeśli pomyślisz o przypadku, w którym f3, f2 i f1 faktycznie nic nie zwracają [ich łączenie jako f1 (f2 (f3)) jest sztuczny, służy wyłącznie tworzeniu sekwencji]).
Jest to szczególnie istotne, gdy występują skutki uboczne, tj. Gdy jakiś stan jest zmieniony (jeśli f1, f2, f3 nie miały skutków ubocznych, nie miałoby znaczenia, w jakiej kolejności są oceniane; co jest wielką właściwością czystego języki funkcjonalne, na przykład w celu zrównoleglenia tych obliczeń). Im bardziej czyste funkcje, tym lepiej.
Myślę, że z tego wąskiego punktu widzenia monady mogłyby być postrzegane jako cukier składniowy dla języków, które preferują leniwe ocenianie (oceniają rzeczy tylko wtedy, gdy jest to absolutnie konieczne, zgodnie z kolejnością, która nie opiera się na prezentacji kodu) i które nie mają inne sposoby reprezentowania sekwencyjnego składu. W efekcie sekcje kodu, które są „nieczyste” (tj. Mają skutki uboczne), mogą być prezentowane w sposób naturalny, w trybie rozkazującym, ale są wyraźnie oddzielone od czystych funkcji (bez skutków ubocznych), które mogą być ocenione leniwie.
Jest to jednak tylko jeden aspekt, jak tutaj ostrzegano .
źródło
Najprostszym wyjaśnieniem, jakie mogę wymyślić, jest to, że monady są sposobem na komponowanie funkcji z upiększonymi rezultatami (inaczej kompozycja Kleisli). Funkcja „embelished” ma podpis
a -> (b, smth)
gdziea
ib
są typy (myślęInt
,Bool
), które mogą różnić się od siebie, ale niekoniecznie - ismth
jest „kontekst” lub „embelishment”.Ten typ funkcji można również zapisać
a -> m b
tam, gdziem
jest on równoważny „upiększeniu”smth
. Są to zatem funkcje zwracające wartości w kontekście (pomyśl funkcje rejestrujące swoje działania, gdziesmth
jest komunikat rejestrowania; lub funkcje wykonujące dane wejściowe / wyjściowe, a ich wyniki zależą od wyniku operacji IO).Monada to interfejs („typeclass”), który sprawia, że implementator mówi mu, jak tworzyć takie funkcje. Implementator musi zdefiniować funkcję kompozycji
(a -> m b) -> (b -> m c) -> (a -> m c)
dla każdego typu,m
który chce zaimplementować interfejs (jest to kompozycja Kleisli).Jeśli więc powiemy, że mamy typ krotki
(Int, String)
reprezentujący wyniki obliczeń naInt
s, które również rejestrują ich działania, przy(_, String)
czym jest to „upiększenie” - dziennik akcji - i dwie funkcjeincrement :: Int -> (Int, String)
itwoTimes :: Int -> (Int, String)
chcemy uzyskać funkcję,incrementThenDouble :: Int -> (Int, String)
która jest kompozycją z dwóch funkcji, które również uwzględniają dzienniki.W podanym przykładzie implementacja monadowa dwóch funkcji dotyczy wartości całkowitej 2
incrementThenDouble 2
(która jest równatwoTimes (increment 2)
) zwróciłaby(6, " Adding 1. Doubling 3.")
wyniki pośrednieincrement 2
równe(3, " Adding 1.")
itwoTimes 3
równe(6, " Doubling 3.")
Z tej funkcji kompozycji Kleisli można wyprowadzić zwykłe funkcje monadyczne.
źródło