Będę używał agnostycznego opisu takich monad, opisując najpierw monoidy:
Monoid jest (w przybliżeniu) zestaw funkcji, które mają jakiś rodzaj jako parametr i zwraca ten sam typ.
Monada jest (w przybliżeniu) zestaw funkcji, które wymagają owinięcia typu jako parametr i zwraca samego rodzaju owijki.
Zauważ, że są to opisy, a nie definicje. Zapraszam do ataku na ten opis!
Tak więc w języku OO monada pozwala na takie kompozycje operacji, jak:
Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
Zauważ, że monada definiuje i kontroluje semantykę tych operacji, a nie klasę zawartą.
Tradycyjnie w języku OO używamy hierarchii klas i dziedziczenia, aby zapewnić semantykę. Tak, że mamy Bird
klasę z metodami takeOff()
, flyAround()
a land()
, a Duck odziedziczy tych.
Ale potem wpadamy w kłopoty z nielotnymi ptakami, ponieważ penguin.takeOff()
zawodzą. Musimy uciekać się do rzucania wyjątków i obsługi.
Ponadto, gdy powiemy, że Pingwin jest a Bird
, napotkamy problemy z wielokrotnym dziedziczeniem, na przykład jeśli mamy również hierarchię Swimmer
.
Zasadniczo staramy się podzielić klasy na kategorie (z przeprosinami dla osób z Teorii Kategorii) i zdefiniować semantykę według kategorii, a nie poszczególnych klas. Monady wydają się jednak znacznie bardziej przejrzystym mechanizmem niż hierarchie.
W takim przypadku mielibyśmy Flier<T>
monadę jak w powyższym przykładzie:
Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
... i nigdy nie będzie instancji Flier<Penguin>
. Możemy nawet użyć pisania statycznego, aby temu zapobiec, być może z interfejsem znacznika. Lub sprawdzanie zdolności środowiska wykonawczego w celu ratowania. Ale tak naprawdę, programista nigdy nie powinien umieszczać pingwina we Flier, w tym samym sensie, że nigdy nie powinien dzielić przez zero.
Ponadto ma to bardziej ogólne zastosowanie. Lotnik nie musi być ptakiem. Na przykład Flier<Pterodactyl>
lub Flier<Squirrel>
bez zmiany semantyki tych poszczególnych typów.
Gdy klasyfikujemy semantykę według funkcji składanych w kontenerze - zamiast hierarchii typów - rozwiązuje stare problemy z klasami, które „w pewnym sensie robią, w pewnym sensie nie pasują” do określonej hierarchii. Pozwala również łatwo i wyraźnie dopuszczać wiele semantyki dla klasy, jak Flier<Duck>
również Swimmer<Duck>
. Wygląda na to, że walczyliśmy z niedopasowaniem impedancji, klasyfikując zachowanie za pomocą hierarchii klas. Monady radzą sobie z tym elegancko.
Tak więc moje pytanie brzmi: w taki sam sposób, w jaki preferujemy kompozycję nad dziedziczeniem, czy sens ma również faworyzowanie monad nad dziedziczeniem?
(BTW, nie byłem pewien, czy to powinno być tutaj, czy w Comp Sci, ale wydaje się to bardziej praktycznym problemem modelowania. Ale może tam jest lepiej.)
źródło
Odpowiedzi:
Krótka odpowiedź brzmi: nie , monady nie są alternatywą dla hierarchii dziedziczenia (znanej również jako polimorfizm podtypu). Wygląda na to, że opisujesz polimorfizm parametryczny , z którego korzystają monady, ale nie tylko.
O ile je rozumiem, monady zasadniczo nie mają nic wspólnego z dziedziczeniem. Powiedziałbym, że te dwie rzeczy są mniej więcej ortogonalne: mają na celu rozwiązanie różnych problemów, a więc:
Wreszcie, chociaż jest to styczne do twojego pytania, być może zainteresuje Cię informacja, że monady mają niewiarygodnie potężne sposoby komponowania; przeczytaj o transformatorach monad, aby dowiedzieć się więcej. Jest to jednak nadal aktywny obszar badań, ponieważ my (i przez nas, mam na myśli ludzi o 100 000 razy mądrzejszych ode mnie) nie opracowaliśmy świetnych sposobów komponowania monad i wydaje się, że niektóre monady nie komponują arbitralnie.
Teraz, aby wybrać swoje pytanie (przepraszam, zamierzam, aby było to pomocne i nie sprawiało, że czujesz się źle): Czuję, że istnieje wiele wątpliwych przesłanek, które spróbuję rzucić nieco światła.
Nie, to jest
Monad
w Haskell: sparametryzowany typm a
z implementacjąreturn :: a -> m a
i(>>=) :: m a -> (a -> m b) -> m b
spełniający następujące prawa:Istnieją pewne instancje Monady, które nie są kontenerami (
(->) b
), i niektóre kontenery, które nie są (i nie można ich utworzyć) instancjami Monady (zSet
powodu ograniczenia klasy typu). Zatem intuicja „kontenerowa” jest kiepska. Zobacz to, aby uzyskać więcej przykładów.Nie, wcale nie. Ten przykład nie wymaga Monady. Potrzebne są tylko funkcje z dopasowanymi typami wejść i wyjść. Oto inny sposób na napisanie tego, co podkreśla, że jest to po prostu funkcja aplikacji:
Uważam, że jest to wzorzec znany jako „płynny interfejs” lub „łączenie metod” (ale nie jestem pewien).
Typy danych, które są również monadami, mogą (i prawie zawsze mają!) Operacje, które nie są powiązane z monadami. Oto przykład Haskella złożony z trzech funkcji,
[]
które nie mają nic wspólnego z monadami:[]
„definiuje i kontroluje semantykę operacji”, a „klasa zawarta” nie, ale to nie wystarczy, aby stworzyć monadę:Prawidłowo zauważyłeś, że występują problemy z użyciem hierarchii klas do modelowania rzeczy. Jednak twoje przykłady nie przedstawiają żadnych dowodów na to, że monady mogą:
źródło
land(flyAround(takeOff(new Flier<Duck>(duck))))
nie działa (przynajmniej w OO), ponieważ ta konstrukcja wymaga przerwania enkapsulacji, aby dostać się do szczegółów Fliera. Łącząc możliwości w klasie, szczegóły Flier pozostają ukryte i może zachować semantykę. Jest to podobne do powodu, dla którego w Haskell monada wiąże się,(a, M b)
a nie(M a, M b)
dlatego, że monada nie musi narażać swojego stanu na funkcję „akcji”.unit
staje się (głównie) konstruktorem zawartego typu ibind
staje się (głównie) domyślną operacją czasu kompilacji (tj. Wczesnego wiązania), która wiąże funkcje „akcji” z klasą. Jeśli masz funkcje pierwszej klasy lub klasę Function <A, Monad <B>>,bind
metoda może wykonać późniejsze wiązanie, ale przejdę do tego nadużycia w następnej kolejności . ;)Flier<Thing>
kontroluje semantykę lotu, może ujawnić wiele danych i operacji, które utrzymują semantykę lotu, podczas gdy semantyka specyficzna dla „monady” naprawdę polega na tym, aby była łańcuchowa i hermetyzowana. Te obawy mogą nie (i te, których używam, nie są) obawy klasy wewnątrz monady: np.Resource<String>
Ma właściwość httpStatus, ale String nie.W językach innych niż OO tak. W bardziej tradycyjnych językach OO powiedziałbym „nie”.
Kwestia jest taka, że większość języków nie mają typu specjalizacji, co oznacza, że nie może zrobić
Flier<Squirrel>
iFlier<Bird>
mieć różne implementacje. Musisz zrobić coś takiegostatic Flier Flier::Create(Squirrel)
(a następnie przeciążenie dla każdego typu). Co z kolei oznacza, że musisz modyfikować ten typ za każdym razem, gdy dodajesz nowe zwierzę, i prawdopodobnie powielasz sporo kodu, aby działało.Aha, i w nielicznych językach (na przykład C #)
public class Flier<T> : T {}
jest nielegalne. Nawet się nie zbuduje. Większość, jeśli nie wszyscy programiści OO oczekują,Flier<Bird>
że nadal będąBird
.źródło
Flier<Bird>
jest sparametryzowanym kontenerem, nikt nie uznałby go zaBird
(!?)List<String>
Listę, a nie łańcuch.Flier
to nie tylko kontener. Jeśli uważasz, że to tylko kontener, dlaczego miałbyś kiedykolwiek pomyśleć, że może zastąpić wykorzystanie dziedziczenia?Animal / Bird / Penguin
jest zwykle złym przykładem, ponieważ wprowadza wszystkie rodzaje semantyki. Praktycznym przykładem jest monada REST, której używamy:Resource<String>.from(uri).get()
Resource
dodaje semantykę nadString
(lub innym typem), więc oczywiście nie jestString
.