Switch / Pattern Matching Idee

151

Ich habe mir kürzlich F # angesehen, und obwohl ich wahrscheinlich nicht bald über den Zaun springen werde, werden definitiv einige Bereiche hervorgehoben, in denen C # (oder Bibliotheksunterstützung) das Leben erleichtern könnte.

Insbesondere denke ich über die Musteranpassungsfähigkeit von F # nach, die eine sehr reichhaltige Syntax ermöglicht - viel aussagekräftiger als die aktuellen Schalter- / bedingten C # -Äquivalente. Ich werde nicht versuchen, ein direktes Beispiel zu geben (meine F # ist nicht dazu in der Lage), aber kurz gesagt, es erlaubt:

  • Übereinstimmung nach Typ (mit vollständiger Überprüfung auf diskriminierte Gewerkschaften) [Beachten Sie, dass dies auch den Typ für die gebundene Variable ableitet und den Mitgliedern Zugriff gewährt usw.]
  • Übereinstimmung nach Prädikat
  • Kombinationen der oben genannten (und möglicherweise einige andere Szenarien, die mir nicht bekannt sind)

Während es für C # schön wäre, irgendwann etwas von diesem Reichtum auszuleihen, habe ich mir in der Zwischenzeit angesehen, was zur Laufzeit getan werden kann - zum Beispiel ist es ziemlich einfach, einige Objekte zusammenzuschlagen, um Folgendes zu ermöglichen:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

Dabei ist getRentPrice ein Func <Vehicle, int>.

[Anmerkung - vielleicht ist Switch / Case hier die falsche Bezeichnung ... aber es zeigt die Idee]

Für mich ist dies viel klarer als das Äquivalent mit wiederholtem if / else oder einer zusammengesetzten ternären Bedingung (die für nicht triviale Ausdrücke sehr chaotisch wird - Klammern in Hülle und Fülle). Es vermeidet auch viel Casting und ermöglicht eine einfache Erweiterung (entweder direkt oder über Erweiterungsmethoden) auf spezifischere Übereinstimmungen, z. B. eine InRange-Übereinstimmung (...), die mit der VB Select ... -Fall "x bis y" vergleichbar ist " Verwendung.

Ich versuche nur zu beurteilen, ob die Leute glauben, dass Konstrukte wie die oben genannten von großem Nutzen sind (ohne Sprachunterstützung).

Beachten Sie außerdem, dass ich mit 3 Varianten der oben genannten gespielt habe:

  • eine Func <TSource, TValue> -Version zur Auswertung - vergleichbar mit zusammengesetzten ternären bedingten Anweisungen
  • eine Action <TSource> -Version - vergleichbar mit if / else if / else if / else if / else
  • eine Expression <Func <TSource, TValue >> -Version - als erste, aber von beliebigen LINQ-Anbietern verwendbar

Darüber hinaus ermöglicht die Verwendung der Ausdrucks-basierten Version das Umschreiben des Ausdrucksbaums, wobei im Wesentlichen alle Zweige zu einem einzigen zusammengesetzten bedingten Ausdruck zusammengefasst werden, anstatt einen wiederholten Aufruf zu verwenden. Ich habe es in letzter Zeit nicht überprüft, aber in einigen frühen Entity Framework-Builds erinnere ich mich anscheinend daran, dass dies notwendig ist, da InvocationExpression nicht besonders gut gefallen hat. Es ermöglicht auch eine effizientere Verwendung mit LINQ-to-Objects, da wiederholte Delegatenaufrufe vermieden werden. Tests zeigen, dass eine Übereinstimmung wie oben (unter Verwendung des Ausdrucksformulars) mit derselben Geschwindigkeit [tatsächlich geringfügig schneller] im Vergleich zum entsprechenden C # ausgeführt wird zusammengesetzte bedingte Anweisung. Der Vollständigkeit halber dauerte die auf Func <...> basierende Version viermal so lange wie die C # -bedingte Anweisung, ist jedoch immer noch sehr schnell und in den meisten Anwendungsfällen wahrscheinlich kein großer Engpass.

Ich freue mich über alle Gedanken / Eingaben / Kritik / usw. zu den oben genannten Themen (oder zu den Möglichkeiten einer umfassenderen Unterstützung der C # -Sprache ... hier ist die Hoffnung ;-p).

Marc Gravell
quelle
"Ich versuche nur zu beurteilen, ob die Leute glauben, dass Konstrukte wie die oben genannten von großem Nutzen sind (ohne Sprachunterstützung)." IMHO ja. Gibt es nicht schon etwas ähnliches? Wenn nicht, fühlen Sie sich ermutigt, eine leichte Bibliothek zu schreiben.
Konrad Rudolph
10
Sie können VB .NET verwenden, das dies in seiner select case-Anweisung unterstützt. Eek!
Jim Burger
Ich werde auch mein eigenes Horn betätigen
Alexey Romanov
1
Ich mag diese Idee und sie ergibt eine sehr schöne und viel flexiblere Form eines Schaltkastens. Ist dies jedoch nicht wirklich eine verschönerte Methode, Linq-ähnliche Syntax als Wenn-Dann-Wrapper zu verwenden? Ich würde jemanden davon abhalten, dies anstelle des eigentlichen Geschäfts zu verwenden, dh eine switch-caseErklärung. Versteh mich nicht falsch, ich denke, es hat seinen Platz und ich werde wahrscheinlich nach einer Möglichkeit suchen, es umzusetzen.
IAbstract
2
Obwohl diese Frage älter als zwei Jahre ist, sollte erwähnt werden, dass C # 7 bald (ish) mit Mustervergleichsfunktionen herauskommt.
Abion47

Antworten:

22

Ich weiß, dass es ein altes Thema ist, aber in c # 7 können Sie Folgendes tun:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}
Marcus Pierce
quelle
Der bemerkenswerte Unterschied zwischen C # und F # ist die Vollständigkeit der Musterübereinstimmung. Dass die Musterübereinstimmung alle möglichen verfügbaren, vollständig beschriebenen Warnungen des Compilers abdeckt, wenn Sie dies nicht tun. Während Sie zu Recht argumentieren können, dass der Standardfall dies tut, handelt es sich in der Praxis häufig um eine Laufzeitausnahme.
VoronoiPotato
37

Nachdem ich versucht habe, solche "funktionalen" Dinge in C # zu tun (und sogar ein Buch darüber zu versuchen), bin ich zu dem Schluss gekommen, dass solche Dinge mit wenigen Ausnahmen nicht allzu viel helfen.

Der Hauptgrund ist, dass Sprachen wie F # einen großen Teil ihrer Leistung erhalten, wenn sie diese Funktionen wirklich unterstützen. Nicht "du kannst es schaffen", sondern "es ist einfach, es ist klar, es wird erwartet".

Beim Pattern Matching wird Ihnen beispielsweise vom Compiler mitgeteilt, ob eine unvollständige Übereinstimmung vorliegt oder wann eine andere Übereinstimmung niemals getroffen wird. Dies ist bei offenen Typen weniger nützlich, aber wenn eine diskriminierte Vereinigung oder Tupel gefunden werden, ist es sehr geschickt. In F # erwarten Sie, dass Personen Musterübereinstimmungen vornehmen, und dies ist sofort sinnvoll.

Das "Problem" ist, dass es natürlich ist, fortfahren zu wollen, sobald Sie anfangen, einige Funktionskonzepte zu verwenden. Die Nutzung von Tupeln, Funktionen, partieller Methodenanwendung und -currying, Mustervergleich, verschachtelten Funktionen, Generika, Monadenunterstützung usw. in C # wird jedoch sehr schnell sehr hässlich. Es macht Spaß und einige sehr kluge Leute haben einige sehr coole Dinge in C # gemacht, aber es fühlt sich schwer an , es tatsächlich zu benutzen .

Was ich in C # häufig (projektübergreifend) verwendet habe:

  • Sequenzfunktionen über Erweiterungsmethoden für IEnumerable. Dinge wie ForEach oder Process ("Apply"? - Führen Sie eine Aktion für ein Sequenzelement aus, wie es aufgelistet ist) passen, weil die C # -Syntax dies gut unterstützt.
  • Gemeinsame Anweisungsmuster abstrahieren. Komplizierte try / catch / finally-Blöcke oder andere beteiligte (oft stark generische) Codeblöcke. Auch hier passt die Erweiterung von LINQ-to-SQL.
  • Bis zu einem gewissen Grad Tupel.

** Beachten Sie jedoch: Das Fehlen einer automatischen Verallgemeinerung und Typinferenz behindert die Verwendung selbst dieser Funktionen erheblich. ** **.

All dies sagte, wie jemand anderes in einem kleinen Team für einen bestimmten Zweck erwähnte, ja, vielleicht können sie helfen, wenn Sie mit C # nicht weiterkommen. Aber meiner Erfahrung nach fühlten sie sich normalerweise stressiger an, als sie es wert waren - YMMV.

Einige andere Links:

MichaelGG
quelle
25

Der Grund, warum C # das Einschalten des Typs nicht einfach macht, liegt wahrscheinlich darin, dass es sich in erster Linie um eine objektorientierte Sprache handelt. Der "richtige" Weg, dies objektorientiert zu tun, besteht darin, eine GetRentPrice-Methode für Vehicle und zu definieren Überschreiben Sie es in abgeleiteten Klassen.

Trotzdem habe ich ein bisschen Zeit damit verbracht, mit Multiparadigmen und funktionalen Sprachen wie F # und Haskell zu spielen, die diese Art von Fähigkeit haben, und ich bin auf eine Reihe von Stellen gestoßen, an denen es vorher nützlich gewesen wäre (z. B. wenn Sie schreiben nicht die Typen, die Sie zum Einschalten benötigen, damit Sie keine virtuelle Methode auf sie implementieren können) und ich würde dies zusammen mit diskriminierten Gewerkschaften in der Sprache begrüßen.

[Bearbeiten: Teil über die Leistung entfernt, da Marc angedeutet hat, dass er kurzgeschlossen werden könnte]

Ein weiteres potenzielles Problem ist die Benutzerfreundlichkeit. Aus dem letzten Aufruf geht hervor, was passiert, wenn das Spiel keine Bedingungen erfüllt. Wie verhält es sich jedoch, wenn es zwei oder mehr Bedingungen erfüllt? Sollte es eine Ausnahme auslösen? Sollte es das erste oder das letzte Spiel zurückgeben?

Eine Möglichkeit, diese Art von Problem zu lösen, besteht darin, ein Wörterbuchfeld mit dem Typ als Schlüssel und dem Lambda als Wert zu verwenden, was mit der Objektinitialisierersyntax ziemlich knapp zu konstruieren ist. Dies berücksichtigt jedoch nur den konkreten Typ und lässt keine zusätzlichen Prädikate zu, sodass es möglicherweise nicht für komplexere Fälle geeignet ist. [Randnotiz - Wenn Sie sich die Ausgabe des C # -Compilers ansehen, werden switch-Anweisungen häufig in wörterbuchbasierte Sprungtabellen konvertiert. Es scheint also keinen guten Grund zu geben, warum das Einschalten von Typen nicht unterstützt werden kann.]

Greg Beech
quelle
1
Eigentlich - die Version, die ich habe, schließt sowohl in der Delegaten- als auch in der Ausdrucksversion kurz. Die Ausdrucksversion wird zu einer zusammengesetzten Bedingung kompiliert. Die Delegatenversion besteht einfach aus einer Reihe von Prädikaten und Funktionen / Aktionen. Sobald eine Übereinstimmung vorliegt, wird sie gestoppt.
Marc Gravell
Interessant - von einem flüchtigen Blick aus ging ich davon aus, dass jede Bedingung zumindest grundlegend überprüft werden muss, da sie wie eine Methodenkette aussieht, aber jetzt ist mir klar, dass die Methoden tatsächlich eine Objektinstanz verketten, um sie zu erstellen, damit Sie dies tun können. Ich werde meine Antwort bearbeiten, um diese Aussage zu entfernen.
Greg Beech
22

Ich denke nicht, dass diese Art von Bibliotheken (die sich wie Spracherweiterungen verhalten) eine breite Akzeptanz finden, aber es macht Spaß, mit ihnen zu spielen, und sie können für kleine Teams, die in bestimmten Bereichen arbeiten, in denen dies nützlich ist, sehr nützlich sein. Wenn Sie zum Beispiel Tonnen von 'Geschäftsregeln / Logik' schreiben, die beliebige Typprüfungen wie diese und so weiter durchführen, kann ich sehen, wie praktisch es wäre.

Ich habe keine Ahnung, ob dies jemals eine C # -Sprachenfunktion sein dürfte (scheint zweifelhaft, aber wer kann die Zukunft sehen?).

Als Referenz ist das entsprechende F # ungefähr:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

Angenommen, Sie hätten eine Klassenhierarchie nach dem Vorbild von definiert

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors
Brian
quelle
2
Danke für die F # Version. Ich denke, ich mag die Art und Weise, wie F # damit umgeht, aber ich bin mir nicht sicher, ob (insgesamt) F # im Moment die richtige Wahl ist, also muss ich diesen Mittelweg gehen ...
Marc Gravell
13

Um Ihre Frage zu beantworten: Ja, ich denke, syntaktische Konstrukte mit Mustervergleich sind nützlich. Ich würde gerne syntaktische Unterstützung in C # dafür sehen.

Hier ist meine Implementierung einer Klasse, die (fast) dieselbe Syntax bietet, die Sie beschreiben

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

Hier ist ein Testcode:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }
cdiggins
quelle
9

Mustervergleich (wie hier beschrieben ) dient dazu, Werte gemäß ihrer Typspezifikation zu dekonstruieren. Das Konzept einer Klasse (oder eines Typs) in C # stimmt jedoch nicht mit Ihnen überein.

Es ist nichts Falsches am Multi-Paradigma-Sprachdesign, im Gegenteil, es ist sehr schön, Lambdas in C # zu haben, und Haskell kann z. B. IO zwingende Dinge tun. Aber es ist keine sehr elegante Lösung, nicht in Haskell-Manier.

Da jedoch sequentielle prozedurale Programmiersprachen als Lambda-Kalkül verstanden werden können und C # zufällig gut in die Parameter einer sequentiellen prozeduralen Sprache passt, ist dies eine gute Anpassung. Wenn Sie jedoch etwas aus dem rein funktionalen Kontext von beispielsweise Haskell nehmen und dieses Merkmal dann in eine Sprache umwandeln, die nicht rein ist, wird dies kein besseres Ergebnis garantieren.

Mein Punkt ist, dass das, was Pattern Matching Tick macht, an das Sprachdesign und das Datenmodell gebunden ist. Trotzdem glaube ich nicht, dass Pattern Matching ein nützliches Merkmal von C # ist, da es weder typische C # -Probleme löst noch gut in das imperative Programmierparadigma passt.

John Leidegren
quelle
1
Vielleicht. In der Tat würde ich mich schwer tun, mir ein überzeugendes "Killer" -Argument auszudenken, warum es nötig wäre (im Gegensatz zu "vielleicht in ein paar Randfällen nett auf Kosten einer komplexeren Sprache").
Marc Gravell
5

IMHO ist die OO-Methode, solche Dinge zu tun, das Besuchermuster. Die Methoden Ihrer Besuchermitglieder fungieren einfach als Fallkonstrukte, und Sie lassen die Sprache selbst den entsprechenden Versand abwickeln, ohne die Typen "einsehen" zu müssen.

Bacila
quelle
4

Obwohl es nicht sehr "C-scharf" ist, den Typ einzuschalten, weiß ich, dass das Konstrukt im allgemeinen Gebrauch ziemlich hilfreich wäre - ich habe mindestens ein persönliches Projekt, das es verwenden könnte (obwohl es überschaubar ist). Gibt es ein großes Problem mit der Kompilierungsleistung, wenn der Ausdrucksbaum neu geschrieben wird?

Simon Buchan
quelle
Nicht, wenn Sie das Objekt zur Wiederverwendung zwischenspeichern (so funktionieren C # -Lambda-Ausdrücke weitgehend, außer der Compiler verbirgt den Code). Das Umschreiben verbessert definitiv die kompilierte Leistung - für die regelmäßige Verwendung (anstelle von LINQ-to-Something) erwarte ich jedoch, dass die Delegate-Version möglicherweise nützlicher ist.
Marc Gravell
Beachten Sie auch - es ist nicht unbedingt ein Einschalttyp - es könnte auch als zusammengesetzte Bedingung verwendet werden (sogar durch LINQ) - aber ohne einen chaotischen x => Test? Ergebnis1: (Test2? Ergebnis2: (Test3? Ergebnis 3: Ergebnis4))
Marc Gravell
Gut zu wissen, obwohl ich die Leistung der eigentlichen Kompilierung meinte: wie lange csc.exe dauert - ich bin mit C # nicht vertraut genug, um zu wissen, ob dies jemals wirklich ein Problem ist, aber es ist ein großes Problem für C ++.
Simon Buchan
csc wird hier nicht blinken - es ist so ähnlich wie LINQ, und der C # 3.0-Compiler ist ziemlich gut in LINQ / Erweiterungsmethoden usw.
Marc Gravell
3

Ich denke, das sieht wirklich interessant aus (+1), aber eines ist zu beachten: Der C # -Compiler ist ziemlich gut darin, switch-Anweisungen zu optimieren. Nicht nur für Kurzschlüsse - Sie erhalten eine völlig andere IL, je nachdem, wie viele Fälle Sie haben und so weiter.

Ihr spezielles Beispiel macht etwas, das ich sehr nützlich finde - es gibt keine Syntax, die Groß- und Kleinschreibung nach Typ entspricht, wie zum Beispiel) typeof(Motorcycle) keine Konstante ist.

Dies wird in dynamischen Anwendungen interessanter - Ihre Logik hier könnte leicht datengesteuert sein und eine Ausführung im Stil einer Regel-Engine ermöglichen.

Keith
quelle
0

Sie können das erreichen, wonach Sie suchen, indem Sie eine von mir geschriebene Bibliothek namens OneOf verwenden

Der Hauptvorteil gegenüber switch(und ifund exceptions as control flow) besteht darin, dass es zur Kompilierungszeit sicher ist - es gibt keinen Standardhandler oder Durchfall

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

Es ist auf Nuget und zielt auf net451 und netstandard1.6 ab

mcintyre321
quelle