Sind Monaden eine gangbare (vielleicht vorzuziehende) Alternative zu Vererbungshierarchien?

20

Ich werde eine sprachunabhängige Beschreibung von Monaden wie diese verwenden und zuerst Monoide beschreiben:

Ein Monoid ist (ungefähr) eine Menge von Funktionen, die einen bestimmten Typ als Parameter annehmen und denselben Typ zurückgeben.

Eine Monade ist (ungefähr) eine Reihe von Funktionen, die einen Wrapper- Typ als Parameter verwenden und denselben Wrapper-Typ zurückgeben.

Beachten Sie, dass dies Beschreibungen und keine Definitionen sind. Fühlen Sie sich frei, diese Beschreibung anzugreifen!

In einer OO-Sprache erlaubt eine Monade Operationskompositionen wie:

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

Beachten Sie, dass die Monade die Semantik dieser Operationen und nicht die enthaltene Klasse definiert und steuert.

Traditionell verwenden wir in einer OO-Sprache eine Klassenhierarchie und -vererbung, um diese Semantik bereitzustellen. So hätten wir eine haben BirdKlasse mit Methoden takeOff(), flyAround()und land(), und Ente würden diejenigen erben.

Aber dann bekommen wir Ärger mit flugunfähigen Vögeln, weil es penguin.takeOff()versagt. Wir müssen auf das Ausnahmewerfen und -handling zurückgreifen.

BirdWenn wir einmal sagen, dass Pinguin ein Pinguin ist , haben wir Probleme mit der Mehrfachvererbung, zum Beispiel wenn wir auch eine Hierarchie von Pinguinen haben Swimmer.

Im Wesentlichen versuchen wir, Klassen in Kategorien einzuteilen (mit Entschuldigung an die Kategorietheoretiker) und die Semantik eher nach Kategorien als nach einzelnen Klassen zu definieren. Aber Monaden scheinen ein viel klarerer Mechanismus dafür zu sein als Hierarchien.

In diesem Fall hätten wir also eine Flier<T>Monade wie das obige Beispiel:

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

... und wir würden niemals a instanziieren Flier<Penguin>. Wir könnten sogar statische Typisierung verwenden, um dies zu verhindern, möglicherweise mit einer Markierungsschnittstelle. Oder Laufzeitfähigkeitsprüfung zur Rettung. Aber wirklich, ein Programmierer sollte niemals einen Pinguin in Flier stecken, in dem gleichen Sinne, in dem sie niemals durch Null teilen sollten.

Es ist auch allgemeiner anwendbar. Ein Flieger muss kein Vogel sein. Zum Beispiel Flier<Pterodactyl>oder Flier<Squirrel>, ohne die Semantik dieser einzelnen Typen zu ändern.

Sobald wir die Semantik durch zusammensetzbare Funktionen in einem Container klassifizieren - anstelle von Typhierarchien - werden die alten Probleme mit Klassen behoben, die "irgendwie tun, irgendwie nicht" in eine bestimmte Hierarchie passen. Es erlaubt auch leicht und klar mehrere Semantiken für eine Klasse, wie Flier<Duck>auch Swimmer<Duck>. Es scheint, als hätten wir mit einer Impedanzinkongruenz zu kämpfen, indem wir das Verhalten anhand von Klassenhierarchien klassifizierten. Monaden gehen elegant damit um.

Meine Frage ist also, in der gleichen Weise, wie wir die Komposition der Vererbung vorgezogen haben, ist es auch sinnvoll, Monaden der Vererbung vorzuziehen?

(Übrigens war ich mir nicht sicher, ob dies hier oder in Comp Sci sein sollte, aber dies scheint eher ein praktisches Modellierungsproblem zu sein. Aber vielleicht ist es dort besser.)

rauben
quelle
1
Ich verstehe nicht, wie es funktioniert: Ein Eichhörnchen und eine Ente fliegen nicht auf die gleiche Weise - also muss die "Flugaktion" in diesen Klassen implementiert werden ... Und der Flieger benötigt eine Methode, um das Eichhörnchen und die Ente herzustellen fly ... Vielleicht in einer normalen Flier-Oberfläche ... Ups, warte mal ... Habe ich etwas verpasst?
Assylias
Schnittstellen unterscheiden sich von der Klassenvererbung, da Schnittstellen Funktionen definieren, während die funktionale Vererbung das tatsächliche Verhalten definiert. Auch bei "Komposition über Vererbung" ist das Definieren von Schnittstellen ein wichtiger Mechanismus (z. B. Polymorphismus). Schnittstellen stoßen nicht auf dieselben Probleme bei der Mehrfachvererbung. Außerdem könnte jeder Flieger (über eine Schnittstelle und einen Polymorphismus) Eigenschaften wie "getFlightSpeed ​​()" oder "getManuverability ()" für den zu verwendenden Container bereitstellen.
Rob
3
Versuchen Sie zu fragen, ob die Verwendung von parametrischem Polymorphismus immer eine gangbare Alternative zum Subtyp-Polymorphismus ist?
ChaosPandion
Ja, mit der Mühe, zusammensetzbare Funktionen hinzuzufügen, die die Semantik bewahren. Parametrisierte Containertypen gibt es schon lange, aber für sich genommen sind sie für mich keine vollständige Antwort. Deshalb frage ich mich, ob das Monadenmuster eine grundlegendere Rolle spielt.
Rob
6
Ich verstehe deine Beschreibung von Monoiden und Monaden nicht. Die Schlüsseleigenschaft von Monoiden besteht darin, dass es sich um eine assoziative Binäroperation handelt (denken Sie an Gleitkommaaddition, Ganzzahlmultiplikation oder Zeichenfolgenverkettung). Eine Monade ist eine Abstraktion, die die Sequenzierung verschiedener (möglicherweise abhängiger) Berechnungen in einer bestimmten Reihenfolge unterstützt.
Rufflewind

Antworten:

15

Die kurze Antwort lautet: Nein , Monaden sind keine Alternative zu Vererbungshierarchien (auch als Subtyp-Polymorphismus bezeichnet). Sie scheinen den parametrischen Polymorphismus zu beschreiben , den Monaden nutzen, aber nicht die einzigen sind, die dies tun.

Nach meinem Verständnis haben Monaden im Wesentlichen nichts mit Vererbung zu tun. Ich würde sagen, dass die beiden Dinge mehr oder weniger orthogonal sind: Sie sollen verschiedene Probleme angehen, und so:

  1. Sie können in mindestens zwei Richtungen synergistisch eingesetzt werden:
    • Schauen Sie sich die Typeclassopedia an , die viele der Haskell- Typenklassen abdeckt. Sie werden feststellen, dass zwischen ihnen vererbungsähnliche Beziehungen bestehen. Zum Beispiel stammt Monad von Applicative ab, das selbst von Functor abstammt.
    • Datentypen, die Instanzen von Monaden sind, können an Klassenhierarchien teilnehmen. Denken Sie daran, Monad ist eher eine Schnittstelle - die Implementierung für einen bestimmten Typ sagt Ihnen etwas über den Datentyp aus, aber nicht alles.
  2. Der Versuch, eins zu benutzen, um das andere zu tun, wird schwierig und hässlich sein.

Obwohl dies tangential zu Ihrer Frage ist, sind Sie vielleicht interessiert zu erfahren, dass Monaden unglaublich mächtige Möglichkeiten zum Komponieren haben. Lesen Sie mehr über Monadentransformatoren. Dies ist jedoch immer noch ein aktives Forschungsgebiet, da wir (und damit meine ich Leute, die 100.000-mal schlauer sind als ich) keine großartigen Möglichkeiten gefunden haben, Monaden zu komponieren, und es scheint, als würden einige Monaden nicht willkürlich komponieren.


Nun, um Ihre Frage zu beantworten (Entschuldigung, ich beabsichtige, dass dies hilfreich ist und Sie sich nicht schlecht fühlen lässt): Ich habe das Gefühl, dass es viele fragwürdige Prämissen gibt, auf die ich versuchen werde, etwas Licht ins Dunkel zu bringen.

  1. Eine Monade ist eine Reihe von Funktionen, die einen Containertyp als Parameter verwenden und denselben Containertyp zurückgeben.

    Nein, dies ist Monadin Haskell: ein parametrisierter Typ m amit einer Implementierung von return :: a -> m aund (>>=) :: m a -> (a -> m b) -> m b, der die folgenden Gesetze erfüllt:

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

    Es gibt einige Instanzen von Monad, bei denen es sich nicht um container ( (->) b) handelt, und einige Container, bei denen es sich nicht um Instanzen von Monad handelt ( Setund die aufgrund der Einschränkung der Typklasse nicht erstellt werden können ). Die "Container" -Intuition ist also schlecht. Sehen Sie dies für weitere Beispiele.

  2. In einer OO-Sprache erlaubt eine Monade Operationskompositionen wie:

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

    Nein überhaupt nicht. Für dieses Beispiel ist keine Monade erforderlich. Es werden lediglich Funktionen mit passenden Ein- und Ausgabetypen benötigt. Hier ist eine andere Möglichkeit, es zu schreiben, die unterstreicht, dass es sich nur um eine Funktionsanwendung handelt:

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

    Ich glaube, dies ist ein Muster, das als "flüssige Schnittstelle" oder "Methodenverkettung" bekannt ist (aber ich bin mir nicht sicher).

  3. Beachten Sie, dass die Monade die Semantik dieser Operationen und nicht die enthaltene Klasse definiert und steuert.

    Datentypen, die auch Monaden sind, können (und tun dies fast immer!) Operationen haben, die nicht mit Monaden zusammenhängen. Hier ist ein Haskell-Beispiel, das aus drei Funktionen besteht, []die nichts mit Monaden zu tun haben: []"Definiert und steuert die Semantik der Operation" und die "enthaltene Klasse" nicht, aber das reicht nicht aus, um eine Monade zu erstellen:

    \predicate -> length . filter predicate . reverse
    
  4. Sie haben richtig bemerkt, dass es Probleme mit der Verwendung von Klassenhierarchien zum Modellieren von Dingen gibt. Ihre Beispiele weisen jedoch nicht darauf hin, dass Monaden Folgendes können:

    • Machen Sie einen guten Job in dem Zeug, in dem Vererbung gut ist
    • Machen Sie einen guten Job in dem Zeug, in dem Vererbung schlecht ist
Gemeinschaft
quelle
3
Vielen Dank! Viel für mich zu verarbeiten. Ich fühle mich nicht schlecht - ich schätze die Einsicht sehr. Ich würde mich schlechter fühlen, wenn ich die schlechten Ideen herumtrage. :) (Geht auf den Punkt Stackexchange!)
Rob
1
@RobY Gern geschehen! Übrigens, wenn Sie noch nie davon gehört haben, empfehle ich LYAH, da es eine großartige Quelle zum Lernen von Monaden (und Haskell!) Ist Monaden bekämpfen).
Es gibt viel hier; Ich möchte die Kommentare nicht überschwemmen, aber ein paar Kommentare: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))funktioniert nicht (zumindest in OO), da diese Konstruktion eine Unterbrechung der Kapselung erfordert, um an die Details von Flier zu gelangen. Durch die Verkettung von Operationen in der Klasse bleiben die Details von Flier verborgen, und die Semantik bleibt erhalten. Das ähnelt dem Grund, warum in Haskell eine Monade bindet (a, M b)und nicht, (M a, M b)damit die Monade ihren Zustand nicht der "Action" -Funktion aussetzen muss.
Rob
# 1, leider versuche ich , die strenge Definition von Monad in Haskell zu verwischen, da die Zuordnung von Elementen zu Haskell ein großes Problem darstellt: Funktionskomposition, einschließlich Komposition für Konstruktoren , die in einer Fußgängersprache wie Java nicht einfach möglich ist. So unitwird (meistens) Konstruktor für den enthaltenen Typ und bindwird (meistens) eine implizite Kompilierungszeitoperation (dh eine frühe Bindung), die die "Aktions" -Funktionen mit der Klasse verbindet. Wenn Sie erstklassige Funktionen oder eine Function <A-, Monad <B >> -Klasse haben, kann eine bindMethode eine späte Bindung ausführen, aber ich werde diesen Missbrauch als nächstes übernehmen. ;)
Rob
# 3 stimmen zu, und das ist das Schöne daran. Wenn Flier<Thing>die Semantik des Flugs gesteuert wird, können viele Daten und Operationen verfügbar gemacht werden, die die Flugsemantik beibehalten, während die "monad" -spezifische Semantik eigentlich nur die Verkettung und Verkapselung bewirkt. Diese Bedenken betreffen möglicherweise nicht (und mit denen, die ich verwendet habe, auch nicht) die Klasse innerhalb der Monade: Resource<String>Hat z. B. eine httpStatus-Eigenschaft, String jedoch nicht.
Rob
1

Meine Frage ist also, in der gleichen Weise, wie wir die Komposition der Vererbung vorgezogen haben, ist es auch sinnvoll, Monaden der Vererbung vorzuziehen?

In Nicht-OO-Sprachen ja. In traditionelleren OO-Sprachen würde ich nein sagen.

Das Problem ist , dass die meisten Sprachen nicht Typ Spezialisierung haben, so dass Sie nicht machen können Flier<Squirrel>und Flier<Bird>verschiedene Implementierungen haben. Sie müssen so etwas tun static Flier Flier::Create(Squirrel)(und dann für jeden Typ überladen). Dies bedeutet wiederum, dass Sie diesen Typ jedes Mal ändern müssen, wenn Sie ein neues Tier hinzufügen, und wahrscheinlich ziemlich viel Code duplizieren müssen, damit es funktioniert.

Oh, und in nicht wenigen Sprachen (C # zum Beispiel) public class Flier<T> : T {}ist illegal. Es wird nicht einmal bauen. Die meisten, wenn nicht alle OO-Programmierer, würden damit rechnen Flier<Bird>, immer noch einer zu sein Bird.

Telastyn
quelle
danke für den Kommentar. Ich habe noch ein paar Gedanken, aber nur trivial, obwohl Flier<Bird>es sich um einen parametrisierten Container handelt, würde es niemand als Bird(!?) List<String>Eine Liste und nicht als String ansehen.
Rob
@RobY - Flierist nicht nur ein Container. Wenn Sie es nur für einen Container halten, warum würden Sie jemals denken, dass es die Verwendung von Vererbung ersetzen könnte?
Telastyn
Ich habe dich dort verloren. Mein Punkt ist, dass die Monade ein verbesserter Container ist. Animal / Bird / Penguinist in der Regel ein schlechtes Beispiel, weil es alle Arten von Semantik bringt. Ein praktisches Beispiel ist eine REST-ische Monade, die wir verwenden: Resource<String>.from(uri).get() Resourcefügt der Semantik String(oder einem anderen Typ) eine Semantik hinzu , es ist also offensichtlich keine String.
Rob
@RobY - aber es hat auch nichts mit Vererbung zu tun.
Telastyn
Abgesehen davon, dass es sich um eine andere Art der Eindämmung handelt. Ich kann String in Resource einfügen oder eine ResourceString-Klasse abstrahieren und die Vererbung verwenden. Meiner Meinung nach ist das Einfügen einer Klasse in einen Verkettungscontainer ein besserer Weg, um Verhalten zu abstrahieren, als es in eine Klassenhierarchie mit Vererbung einzufügen. Also "no way related" im Sinne von "Ersetzen / Vermeiden" - ja.
Rob