Czy monady są realną (być może lepszą) alternatywą dla hierarchii dziedziczenia?

20

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 Birdklasę 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.)

Obrabować
źródło
1
Nie jestem pewien, czy rozumiem, jak to działa: wiewiórka i kaczka nie latają w ten sam sposób - więc w tych klasach należy wdrożyć „akcję w locie” ... I lotnik potrzebuje metody, aby zrobić wiewiórkę i kaczkę latać ... Może we wspólnym interfejsie Flier ... Ups, poczekaj chwilę ... Coś przeoczyłem?
assylias
Interfejsy różnią się od dziedziczenia klas, ponieważ interfejsy definiują możliwości, a dziedziczenie funkcjonalne określa rzeczywiste zachowanie. Nawet w „kompozycji nad dziedziczeniem” definiowanie interfejsów jest nadal ważnym mechanizmem (np. Polimorfizm). Interfejsy nie napotykają na te same problemy z wielokrotnym dziedziczeniem. Ponadto każda ulotka może zapewniać (poprzez interfejs i polimorfizm) właściwości takie jak „getFlightSpeed ​​()” lub „getManuverability ()” do użycia przez kontener.
Rob
3
Czy zastanawiasz się, czy stosowanie polimorfizmu parametrycznego jest zawsze realną alternatywą dla polimorfizmu podtypu?
ChaosPandion
tak, ze zmarszczeniem dodawania funkcji do komponowania, które zachowują semantykę. Sparametryzowane typy kontenerów istnieją już od dawna, ale same w sobie nie wydają mi się kompletną odpowiedzią. Dlatego zastanawiam się, czy wzorzec monady ma do odegrania bardziej fundamentalną rolę.
Rob
6
Nie rozumiem twojego opisu monoidów i monad. Kluczową właściwością monoidów jest to, że obejmuje asocjacyjną operację binarną (pomyśl dodawanie zmiennoprzecinkowe, mnożenie liczb całkowitych lub konkatenacja łańcuchów). Monada to abstrakcja, która obsługuje sekwencjonowanie różnych (ewentualnie zależnych) obliczeń w określonej kolejności.
Rufflewind

Odpowiedzi:

15

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:

  1. Można je stosować synergicznie w co najmniej dwóch zmysłach:
    • sprawdź Typeclassopedia , która obejmuje wiele klas typów Haskell. Zauważysz, że istnieją między nimi relacje podobne do dziedziczenia. Na przykład Monada pochodzi od Applicative, który sam pochodzi od Functor.
    • typy danych, które są instancjami Monad, mogą uczestniczyć w hierarchiach klas. Pamiętaj, że Monada jest bardziej interfejsem - implementacja go dla danego typu mówi ci kilka rzeczy o typie danych, ale nie wszystko.
  2. Próba użycia jednego do drugiego będzie trudna i brzydka.

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.

  1. Monada to zestaw funkcji, które przyjmują typ kontenera jako parametr i zwracają ten sam typ kontenera.

    Nie, to jest Monadw Haskell: sparametryzowany typ m az implementacją return :: a -> m ai (>>=) :: m a -> (a -> m b) -> m bspełniający następujące prawa:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    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 (z Setpowodu ograniczenia klasy typu). Zatem intuicja „kontenerowa” jest kiepska. Zobacz to, aby uzyskać więcej przykładów.

  2. Tak więc w języku OO monada pozwala na takie kompozycje operacji, jak:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    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:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Uważam, że jest to wzorzec znany jako „płynny interfejs” lub „łączenie metod” (ale nie jestem pewien).

  3. Zauważ, że monada definiuje i kontroluje semantykę tych operacji, a nie klasę zawartą.

    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ę:

    \predicate -> length . filter predicate . reverse
    
  4. 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ą:

    • Wykonaj dobrą robotę w tych rzeczach, w których dziedzictwo jest dobre
    • Wykonaj dobrą robotę przy tych rzeczach, w których dziedziczenie jest złe
Społeczność
źródło
3
Dziękuję Ci! Wiele do przetworzenia. Nie czuję się źle - bardzo doceniam wgląd. Czułbym się gorzej, niosąc złe pomysły. :) (Idzie do sedna
Rob
1
@RobY Nie ma za co! Nawiasem mówiąc, jeśli nie słyszałeś o tym wcześniej, polecam LYAH, ponieważ jest doskonałym źródłem do nauki monad (i Haskell!), Ponieważ ma mnóstwo przykładów (i uważam, że robienie wielu przykładów jest najlepszym sposobem na monady).
Jest tu dużo; Nie chcę bagatelizować komentarzy, ale kilka komentarzy: # 2 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”.
Rob
# 1, niestety staram się zatrzeć ścisłą definicję Monady w Haskell, ponieważ mapowanie czegokolwiek na Haskell ma duży problem: kompozycję funkcji, w tym kompozycję na konstruktorach , której nie można łatwo zrobić w języku pieszym, takim jak Java. Tak więc unitstaje się (głównie) konstruktorem zawartego typu i bindstaje 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>>, bindmetoda może wykonać późniejsze wiązanie, ale przejdę do tego nadużycia w następnej kolejności . ;)
Rob
# 3 zgadzają się i na tym polega piękno. Jeśli 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.
Rob
1

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?

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>i Flier<Bird>mieć różne implementacje. Musisz zrobić coś takiego static 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.

Telastyn
źródło
Dziękuję za komentarz. Mam jeszcze kilka przemyśleń, ale po prostu trywialnie, mimo że Flier<Bird>jest sparametryzowanym kontenerem, nikt nie uznałby go za Bird(!?) List<String>Listę, a nie łańcuch.
Rob
@RobY - Flierto nie tylko kontener. Jeśli uważasz, że to tylko kontener, dlaczego miałbyś kiedykolwiek pomyśleć, że może zastąpić wykorzystanie dziedziczenia?
Telastyn
Zgubiłem cię tam ... chodzi mi o to, że monada to ulepszony pojemnik. Animal / Bird / Penguinjest 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() Resourcedodaje semantykę nad String(lub innym typem), więc oczywiście nie jest String.
Rob
@RobY - ale wtedy też nie jest w żaden sposób związany z dziedziczeniem.
Telastyn
Tyle że jest to inny rodzaj zamknięcia. Mogę wstawić ciąg znaków do zasobu lub abstrakcję klasy ResourceString i użyć dziedziczenia. Myślę, że umieszczenie klasy w kontenerze łańcuchowym jest lepszym sposobem na abstrakcyjne zachowanie niż umieszczenie jej w hierarchii klas z dziedziczeniem. Zatem „nie ma związku” w znaczeniu „zastępowania / usuwania” - tak.
Rob