Ist Pattern Matching idiomatisch oder schlecht designt?


18

Es scheint, als würde F # -Code häufig Muster mit Typen vergleichen. Bestimmt

match opt with 
| Some val -> Something(val) 
| None -> Different()

scheint üblich zu sein.

Aus OOP-Sicht ähnelt das einem Kontrollfluss auf der Grundlage einer Laufzeit-Typprüfung, die normalerweise verpönt wäre. Um es auszudrücken, würden Sie in OOP wahrscheinlich lieber eine Überladung verwenden:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

Das ist sicherlich mehr Code. OTOH, meines Erachtens haben strukturelle Vorteile:

  • die erweiterung auf eine neue form von Tist einfach;
  • Ich brauche mir keine Sorgen zu machen, ob der Kontrollfluss für die Routenwahl doppelt vorhanden ist. und
  • Die Wahl der Route ist in dem Sinne unveränderlich, dass ich Foomich nie mehr um Bar.Route()die Implementierung kümmern muss , wenn ich eine in der Hand habe

Gibt es Vorteile beim Mustervergleich gegenüber Typen, die ich nicht sehe? Wird es als idiomatisch angesehen oder ist es eine Fähigkeit, die nicht allgemein verwendet wird?


3
Wie viel Sinn macht es, eine funktionale Sprache aus einer OOP-Perspektive zu betrachten? Wie auch immer, die wahre Kraft des Pattern Matching kommt mit verschachtelten Mustern. Es ist möglich, nur den äußersten Konstruktor zu überprüfen, aber keineswegs die ganze Geschichte.
Ingo

Das But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.klingt zu dogmatisch. Manchmal möchten Sie Ihre Ops von Ihrer Hierarchie trennen: 1) Sie können einer Hierarchie keine Ops hinzufügen, b / c Sie besitzen die Hierarchie nicht. 2) Die Klassen, für die Sie die Operation durchführen möchten, stimmen nicht mit Ihrer Hierarchie überein. 3) Sie könnten die Option zu Ihrer Hierarchie hinzufügen, möchten aber nicht, dass die API Ihrer Hierarchie mit ein paar Drecksachen überfrachtet wird, die die meisten Clients nicht verwenden.

4
Nur um zu klären, Someund Nonesind keine Typen. Sie sind beide Konstruktoren, deren Typen forall a. a -> option aund forall a. option a(leider nicht sicher, wie die Syntax für Typanmerkungen in F # lautet).

Antworten:


20

Sie haben Recht, dass OOP-Klassenhierarchien sehr eng mit diskriminierten Gewerkschaften in F # verwandt sind und dass der Mustervergleich sehr eng mit dynamischen Typentests verwandt ist. Tatsächlich kompiliert F # auf diese Weise diskriminierte Gewerkschaften nach .NET!

In Bezug auf die Erweiterbarkeit gibt es zwei Seiten des Problems:

  • Mit OO können Sie neue Unterklassen hinzufügen, es ist jedoch schwierig, neue (virtuelle) Funktionen hinzuzufügen
  • Mit FP können Sie neue Funktionen hinzufügen, es ist jedoch schwierig, neue Verbindungsfälle hinzuzufügen

Das heißt, F # gibt Ihnen eine Warnung, wenn Sie einen Fall beim Pattern Matching verpassen, so dass das Hinzufügen neuer Verbindungsfälle eigentlich nicht so schlimm ist.

In Bezug auf das Finden von Duplikaten in der Stammauswahl gibt F # eine Warnung aus, wenn Sie eine Übereinstimmung haben, die doppelt vorhanden ist, z. B .:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

Die Tatsache, dass die Routenwahl unveränderlich ist, könnte ebenfalls problematisch sein. Wenn Sie zum Beispiel die Implementierung einer Funktion zwischen Foound BarFällen teilen möchten, aber etwas anderes für den ZooFall tun möchten , können Sie dies einfach mithilfe des Mustervergleichs codieren:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

Im Allgemeinen konzentriert sich FP eher darauf, zuerst die Typen zu entwerfen und dann Funktionen hinzuzufügen. Es profitiert also wirklich von der Tatsache, dass Sie Ihre Typen (Domain-Modell) in ein paar Zeilen in einer einzigen Datei unterbringen und dann einfach die Funktionen hinzufügen können, die für das Domain-Modell gelten.

Die beiden Ansätze - OO und FP - ergänzen sich sehr gut und haben Vor- und Nachteile. Das Knifflige (aus der OO-Perspektive) ist, dass F # normalerweise den FP-Stil als Standard verwendet. Wenn jedoch wirklich mehr Unterklassen hinzugefügt werden müssen, können Sie immer Schnittstellen verwenden. Aber in den meisten Systemen müssen Sie gleichermaßen Typen und Funktionen hinzufügen, sodass die Auswahl wirklich keine Rolle spielt - und die Verwendung diskriminierter Gewerkschaften in F # ist besser.

Ich würde diese großartige Blogserie für weitere Informationen empfehlen .


3
Obwohl Sie Recht haben, möchte ich hinzufügen, dass dies nicht so sehr ein Problem von OO vs. FP ist, sondern ein Problem von Objekten vs. Summentypen. Abgesehen von der Besessenheit von OOP, gibt es nichts an Objekten, was sie funktionsunfähig macht. Und wenn Sie durch genügend Rahmen springen, können Sie Summentypen auch in den gängigen OOP-Sprachen implementieren (auch wenn dies nicht besonders hübsch ist).
Doval

1
"Und wenn Sie durch genügend Rahmen springen, können Sie Summentypen auch in den gängigen OOP-Sprachen implementieren (obwohl es nicht schön sein wird)." -> Ich denke, Sie werden mit etwas ähnlichem enden, wie F #
Summentypen im Typensystem von

7

Sie haben richtig beobachtet, dass der Mustervergleich (im Wesentlichen eine aufgeladene switch-Anweisung) und der dynamische Versand Ähnlichkeiten aufweisen. Sie koexistieren auch in einigen Sprachen, mit einem sehr erfreulichen Ergebnis. Es gibt jedoch leichte Unterschiede.

Ich könnte das Typensystem verwenden, um einen Typ zu definieren, der nur eine feste Anzahl von Untertypen haben kann:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

Es wird niemals einen anderen Subtyp von Booloder geben Option, daher erscheint eine Subklassifizierung nicht sinnvoll (einige Sprachen wie Scala haben einen Subklassifizierungsbegriff, der dies handhabt - eine Klasse kann außerhalb der aktuellen Kompilierungseinheit als „endgültig“ markiert werden, Subtypen jedoch kann innerhalb dieser Zusammenstellungseinheit definiert werden).

Da die Untertypen eines Typs wie Optionjetzt statisch bekannt sind , kann der Compiler warnen, wenn wir vergessen, einen Fall in unserer Musterübereinstimmung zu behandeln. Dies bedeutet, dass eine Musterübereinstimmung eher einem speziellen Downcast ähnelt, der uns zwingt, alle Optionen zu handhaben.

Darüber hinaus impliziert der dynamische Methodenversand (der für OOP erforderlich ist) auch eine Laufzeittypprüfung, jedoch auf eine andere Art. Es ist daher ziemlich irrelevant, ob wir diese Typprüfung explizit durch eine Musterübereinstimmung oder implizit durch einen Methodenaufruf durchführen.


"Dies bedeutet, dass eine Musterübereinstimmung eher einem speziellen Downcast ähnelt, der uns dazu zwingt, alle Optionen zu handhaben." Platzieren einer abstrakten virtuellen Methode in der Oberklasse.
Jules

2

Der F # -Musterabgleich erfolgt in der Regel mit einer diskriminierten Vereinigung und nicht mit Klassen (und ist daher technisch überhaupt keine Typprüfung). Auf diese Weise kann der Compiler Sie warnen, wenn Sie Fälle in einer Musterübereinstimmung nicht berücksichtigt haben.

Beachten Sie außerdem, dass Sie in einem funktionalen Stil die Dinge eher nach Funktionen als nach Daten organisieren. Mit Hilfe von Musterübereinstimmungen können Sie die verschiedenen Funktionen an einem Ort zusammenfassen, anstatt sie über Klassen zu verteilen. Dies hat auch den Vorteil, dass Sie sehen können, wie andere Fälle direkt neben dem Ort behandelt werden, an dem Sie Ihre Änderungen vornehmen müssen.

Das Hinzufügen einer neuen Option sieht dann so aus:

  1. Fügen Sie Ihrer diskriminierten Gewerkschaft eine neue Option hinzu
  2. Korrigieren Sie alle Warnungen bei unvollständigen Musterübereinstimmungen

2

Teilweise sehen Sie es häufiger in der funktionalen Programmierung, weil Sie Typen verwenden, um häufiger Entscheidungen zu treffen. Mir ist klar, dass Sie wahrscheinlich nur zufällig ausgewählte Beispiele ausgewählt haben, aber das OOP-Äquivalent zu Ihrem Mustervergleichsbeispiel würde häufiger so aussehen:

if (opt != null)
    opt.Something()
else
    Different()

Mit anderen Worten, es ist relativ selten, Polymorphismus zu verwenden, um Routineprobleme wie Nullprüfungen in OOP zu vermeiden. So wie ein OO-Programmierer nicht in jeder Situation ein Null-Objekt erzeugt , überlastet ein funktionaler Programmierer nicht immer eine Funktion, insbesondere wenn Sie wissen, dass Ihre Musterliste garantiert vollständig ist. Wenn Sie das Typsystem in mehreren Situationen verwenden, werden Sie feststellen, dass es so verwendet wird, wie Sie es nicht gewohnt sind.

Im Gegensatz dazu würde die idiomatische funktionale Programmierung entspricht Ihr OOP Beispiel höchstwahrscheinlich nicht Pattern - Matching verwenden, sondern müßte fooRouteund barRouteFunktionen , die als Argumente an dem anrufenden Code übergeben bekommen würden. Wenn jemand in dieser Situation Pattern Matching verwendet, wird dies normalerweise als falsch angesehen, genauso wie das Einschalten von Typen in OOP als falsch angesehen wird.

Wann wird Pattern Matching als guter funktionaler Programmcode angesehen? Wenn Sie mehr als nur die Typen betrachten und die Anforderungen erweitern, müssen Sie keine weiteren Fälle hinzufügen. Zum Beispiel Some valwird nicht nur überprüft, ob optder Typ vorhanden ist Some, sondern es wird auch eine Bindung valan den zugrunde liegenden Typ hergestellt, um eine typsichere Verwendung auf der anderen Seite von zu ermöglichen ->. Sie wissen, dass Sie höchstwahrscheinlich nie einen dritten Fall benötigen werden, also ist es eine gute Verwendung.

Der Mustervergleich ähnelt möglicherweise oberflächlich einer objektorientierten switch-Anweisung, es ist jedoch noch viel mehr im Gange, insbesondere bei längeren oder verschachtelten Mustern. Stellen Sie sicher, dass Sie alles berücksichtigen, was es tut, bevor Sie es für einen schlecht gestalteten OOP-Code erklären. Häufig wird eine Situation, die in einer Vererbungshierarchie nicht sauber dargestellt werden kann, kurz und bündig behandelt.


Ich weiß , dass Sie das wissen, und es rutschte wahrscheinlich Ihren Verstand , während die Antwort zu schreiben, aber beachten Sie, dass Someund Nonesind nicht Typen, so dass Sie auf Typen nicht Musterabgleich sind. Sie Muster-Match auf Konstrukteuren des gleichen Typs . Das ist nicht so, als würde man "instanceof" fragen.
Andres F.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.