idée de correspondance de commutateur / modèle

151

J'ai regardé F # récemment, et bien que je ne sois pas susceptible de sauter la clôture de si tôt, cela met définitivement en évidence certains domaines où C # (ou le support de la bibliothèque) pourrait rendre la vie plus facile.

En particulier, je pense à la capacité de correspondance de modèle de F #, qui permet une syntaxe très riche - beaucoup plus expressive que le commutateur actuel / les équivalents C # conditionnels. Je n'essaierai pas de donner un exemple direct (mon F # n'est pas à la hauteur), mais en bref cela permet:

  • correspondance par type (avec vérification de la couverture complète pour les unions discriminées) [notez que cela déduit également le type de la variable liée, donnant l'accès aux membres, etc.]
  • correspondance par prédicat
  • combinaisons de ce qui précède (et peut-être d'autres scénarios dont je ne suis pas au courant)

Alors qu'il serait bien que C # emprunte éventuellement [ahem] une partie de cette richesse, dans l'intervalle, j'ai examiné ce qui peut être fait au moment de l'exécution - par exemple, il est assez facile d'assembler certains objets pour permettre:

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

où getRentPrice est un Func <Vehicle, int>.

[note - peut-être que Switch / Case est ici les mauvais termes ... mais cela montre l'idée]

Pour moi, c'est beaucoup plus clair que l'équivalent en utilisant if / else répété, ou un conditionnel ternaire composite (qui devient très compliqué pour les expressions non triviales - des crochets à gogo). Cela évite également beaucoup de cast, et permet une simple extension (soit directement, soit via des méthodes d'extension) à des correspondances plus spécifiques, par exemple une correspondance InRange (...) comparable au VB Select ... Case "x To y "utilisation.

J'essaie juste de déterminer si les gens pensent qu'il y a beaucoup d'avantages à des constructions comme celles-ci (en l'absence de support linguistique)?

Notez en outre que j'ai joué avec 3 variantes de ce qui précède:

  • une version Func <TSource, TValue> pour l'évaluation - comparable aux instructions conditionnelles ternaires composites
  • une version Action <TSource> - comparable à if / else if / else if / else if / else
  • une version Expression <Func <TSource, TValue >> - comme la première, mais utilisable par des fournisseurs LINQ arbitraires

De plus, l'utilisation de la version basée sur les expressions permet la réécriture de l'arborescence des expressions, en incorporant essentiellement toutes les branches dans une seule expression conditionnelle composite, plutôt que d'utiliser des appels répétés. Je n'ai pas vérifié récemment, mais dans certaines premières versions d'Entity Framework, je semble me souvenir que cela était nécessaire, car il n'aimait pas beaucoup InvocationExpression. Il permet également une utilisation plus efficace avec LINQ-to-Objects, car il évite les invocations répétées de délégués - les tests montrent une correspondance comme celle ci-dessus (en utilisant le formulaire Expression) fonctionnant à la même vitesse [légèrement plus rapide, en fait] par rapport à l'équivalent C # instruction conditionnelle composite. Par souci d'exhaustivité, la version basée sur Func <...> a pris 4 fois plus de temps que l'instruction conditionnelle C #, mais elle est toujours très rapide et ne sera probablement pas un goulot d'étranglement majeur dans la plupart des cas d'utilisation.

J'apprécie toute réflexion / entrée / critique / etc sur ce qui précède (ou sur les possibilités d'un support plus riche du langage C # ... ici en espérant ;-p).

Marc Gravell
la source
"J'essaie juste de déterminer si les gens pensent qu'il y a beaucoup d'avantages à des constructions comme celles-ci (en l'absence de support linguistique)?" IMHO, oui. N'existe-t-il pas déjà quelque chose de similaire? Sinon, n'hésitez pas à écrire une bibliothèque légère.
Konrad Rudolph
10
Vous pouvez utiliser VB .NET qui prend en charge cela dans son instruction de sélection de cas. Eek!
Jim Burger
Je vais aussi toot ma propre corne et ajouter un lien vers ma bibliothèque: fonctionnelle-dotnet
Alexey Romanov
1
J'aime cette idée et cela en fait une forme très agréable et beaucoup plus flexible de boîtier de commutation; cependant, n'est-ce pas vraiment une façon enrichie d'utiliser la syntaxe de type Linq comme un wrapper if-then? Je découragerais quelqu'un d'utiliser cela à la place de la vraie affaire, c'est-à-dire une switch-casedéclaration. Ne vous méprenez pas, je pense qu'il a sa place et je chercherai probablement un moyen de le mettre en œuvre.
IAbstract
2
Bien que cette question ait plus de deux ans, il semble pertinent de mentionner que C # 7 sortira bientôt (ish) avec des capacités de correspondance de modèles.
Abion47

Réponses:

22

Je sais que c'est un vieux sujet, mais en c # 7, vous pouvez faire:

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
la source
La différence notable ici entre C # et F # est l'exhaustivité de la correspondance de modèle. Que la correspondance de modèle couvre tous les cas possibles disponibles, entièrement décrits, des avertissements du compilateur si vous ne le faites pas. Bien que vous puissiez légitimement affirmer que le cas par défaut fait cela, il s'agit également souvent en pratique d'une exception d'exécution.
VoronoiPotato
37

Après avoir essayé de faire de telles choses "fonctionnelles" en C # (et même essayé un livre dessus), je suis arrivé à la conclusion que non, à quelques exceptions près, de telles choses n'aident pas trop.

La raison principale est que des langages tels que F # tirent beaucoup de leur puissance de la prise en charge réelle de ces fonctionnalités. Pas "tu peux le faire", mais "c'est simple, c'est clair, c'est attendu".

Par exemple, dans la correspondance de modèles, vous obtenez le compilateur vous indiquant s'il y a une correspondance incomplète ou lorsqu'une autre correspondance ne sera jamais atteinte. C'est moins utile avec les types ouverts, mais lors de la mise en correspondance d'une union discriminée ou de tuples, c'est très pratique. En F #, vous vous attendez à ce que les gens correspondent aux modèles, et cela a instantanément du sens.

Le «problème» est qu'une fois que vous commencez à utiliser certains concepts fonctionnels, il est naturel de vouloir continuer. Cependant, tirer parti des tuples, des fonctions, de l'application de méthode partielle et du currying, de la correspondance de modèles, des fonctions imbriquées, des génériques, du support des monades, etc. en C # devient très laid, très rapidement. C'est amusant, et certaines personnes très intelligentes ont fait des choses très cool en C #, mais en fait, l' utiliser semble lourd.

Ce que j'ai fini par utiliser souvent (entre projets) en C #:

  • Fonctions de séquence, via des méthodes d'extension pour IEnumerable. Des choses comme ForEach ou Process ("Apply"? - faire une action sur un élément de séquence tel qu'il est énuméré) s'intègrent car la syntaxe C # le prend bien en charge.
  • Abstrait des modèles de déclaration courants. Blocs try / catch / finally compliqués ou autres blocs de code impliqués (souvent très génériques). L'extension de LINQ-to-SQL s'inscrit également ici.
  • Tuples, dans une certaine mesure.

** Mais notez: le manque de généralisation automatique et d'inférence de type entrave vraiment l'utilisation même de ces fonctionnalités. **

Tout cela dit, comme quelqu'un l'a mentionné, dans une petite équipe, dans un but précis, oui, peut-être qu'ils peuvent vous aider si vous êtes coincé avec C #. Mais d'après mon expérience, ils se sentaient généralement plus compliqués qu'ils n'en valaient la peine - YMMV.

Quelques autres liens:

MichaelGG
la source
25

On peut soutenir que la raison pour laquelle C # ne facilite pas l'activation du type est qu'il s'agit principalement d'un langage orienté objet, et la manière `` correcte '' de le faire en termes orientés objet serait de définir une méthode GetRentPrice sur Vehicle et le remplacer dans les classes dérivées.

Cela dit, j'ai passé un peu de temps à jouer avec des langages multi-paradigmes et fonctionnels comme F # et Haskell qui ont ce type de capacité, et je suis tombé sur un certain nombre d'endroits où cela serait utile auparavant (par exemple, lorsque vous n'écrivez pas les types que vous devez activer afin que vous ne puissiez pas implémenter une méthode virtuelle sur eux) et c'est quelque chose que je serais heureux de retrouver dans la langue avec les syndicats discriminés.

[Edit: Suppression de la partie sur les performances car Marc a indiqué qu'elle pourrait être court-circuitée]

Un autre problème potentiel est celui de la convivialité - il est clair à partir de l'appel final que se passe-t-il si la correspondance ne remplit aucune condition, mais quel est le comportement si elle correspond à deux conditions ou plus? Doit-il lever une exception? Doit-il renvoyer le premier ou le dernier match?

Une façon que j'ai tendance à utiliser pour résoudre ce genre de problème est d'utiliser un champ de dictionnaire avec le type comme clé et le lambda comme valeur, ce qui est assez laconique à construire en utilisant la syntaxe d'initialisation d'objet; cependant, cela ne tient compte que du type concret et n'autorise pas de prédicats supplémentaires et peut donc ne pas convenir à des cas plus complexes. [Note latérale - si vous regardez la sortie du compilateur C #, il convertit fréquemment les instructions switch en tables de sauts basées sur le dictionnaire, il ne semble donc pas y avoir de bonne raison pour laquelle il ne pouvait pas prendre en charge le changement de types]

Greg Beech
la source
1
En fait, la version que j'ai court-circuite dans les versions délégué et expression. La version de l'expression se compile en un conditionnel composé; la version déléguée est simplement un ensemble de prédicats et de fonctions / actions - une fois qu'elle a une correspondance, elle s'arrête.
Marc Gravell
Intéressant - d'un coup d'œil rapide, j'ai supposé qu'il faudrait effectuer au moins une vérification de base de chaque condition car elle ressemblait à une chaîne de méthodes, mais maintenant je me rends compte que les méthodes enchaînent en fait une instance d'objet pour la construire afin que vous puissiez le faire. Je modifierai ma réponse pour supprimer cette déclaration.
Greg Beech
22

Je ne pense pas que ces types de bibliothèques (qui agissent comme des extensions de langage) sont susceptibles d'être largement acceptées, mais elles sont amusantes à jouer et peuvent être vraiment utiles pour les petites équipes travaillant dans des domaines spécifiques où cela est utile. Par exemple, si vous écrivez des tonnes de «règles / logiques métier» qui effectuent des tests de type arbitraires comme celui-ci et ainsi de suite, je peux voir à quel point ce serait pratique.

Je ne sais pas si cela est susceptible d'être une fonctionnalité du langage C # (cela semble douteux, mais qui peut voir l'avenir?).

Pour référence, le F # correspondant est approximativement:

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"

en supposant que vous ayez défini une hiérarchie de classes sur le modèle de

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
la source
2
Merci pour la version F #. Je suppose que j'aime la façon dont F # gère cela, mais je ne suis pas sûr que (globalement) F # soit le bon choix pour le moment, alors je dois marcher sur ce terrain d'entente ...
Marc Gravell
13

Pour répondre à votre question, oui, je pense que les constructions syntaxiques de correspondance de motifs sont utiles. Pour ma part, j'aimerais voir le support syntaxique en C # pour cela.

Voici mon implémentation d'une classe qui fournit (presque) la même syntaxe que celle que vous décrivez

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");
    }
}

Voici un code de test:

    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
la source
9

Correspondance de modèle (comme décrit ici ), son but est de déconstruire les valeurs en fonction de leur spécification de type. Cependant, le concept d'une classe (ou d'un type) en C # ne vous convient pas.

Il n'y a pas de problème avec la conception de langage multi-paradigme, au contraire, c'est très bien d'avoir des lambdas en C #, et Haskell peut faire des choses impératives, par exemple IO. Mais ce n'est pas une solution très élégante, pas à la mode Haskell.

Mais comme les langages de programmation procédurale séquentielle peuvent être compris en termes de calcul lambda, et que C # se trouve bien dans les paramètres d'un langage procédural séquentiel, c'est un bon ajustement. Mais, prendre quelque chose du pur contexte fonctionnel de dire Haskell, puis mettre cette caractéristique dans un langage qui n'est pas pur, eh bien, faire juste cela ne garantira pas un meilleur résultat.

Mon point est le suivant: ce qui fait cocher la correspondance de modèle est lié à la conception du langage et au modèle de données. Cela dit, je ne pense pas que la correspondance de modèles soit une fonctionnalité utile de C # car elle ne résout pas les problèmes typiques de C # et ne rentre pas bien dans le paradigme de programmation impératif.

John Leidegren
la source
1
Peut être. En effet, j'aurais du mal à penser à un argument "tueur" convaincant pour expliquer pourquoi il serait nécessaire (par opposition à "peut-être bien dans quelques cas extrêmes au prix de rendre le langage plus complexe").
Marc Gravell
5

IMHO la façon OO de faire de telles choses est le modèle de visiteur. Vos méthodes de membre visiteur agissent simplement comme des constructions de cas et vous laissez le langage lui-même gérer la répartition appropriée sans avoir à «jeter un coup d'œil» aux types.

bacila
la source
4

Bien que ce ne soit pas très «C-sharpey» pour activer le type, je sais que cette construction serait très utile en utilisation générale - j'ai au moins un projet personnel qui pourrait l'utiliser (bien que son ATM gérable). Y a-t-il un gros problème de performances de compilation, avec la réécriture de l'arbre d'expression?

Simon Buchan
la source
Pas si vous mettez en cache l'objet pour réutilisation (ce qui est en grande partie le fonctionnement des expressions lambda C #, sauf que le compilateur masque le code). La réécriture améliore définitivement les performances compilées - cependant, pour une utilisation régulière (plutôt que LINQ-to-Something), je pense que la version déléguée pourrait être plus utile.
Marc Gravell
Notez également - ce n'est pas nécessairement un type d'activation - il pourrait également être utilisé comme conditionnel composite (même via LINQ) - mais sans un x => Test désordonné? Résultat1: (Test2? Résultat2: (Test3? Résultat 3: Résultat4))
Marc Gravell
Bon à savoir, même si je parlais des performances de la compilation actuelle : combien de temps prend csc.exe - je ne suis pas assez familier avec C # pour savoir si cela pose vraiment un problème, mais c'est un gros problème pour C ++.
Simon Buchan
csc ne clignera pas des yeux - c'est tellement similaire au fonctionnement de LINQ, et le compilateur C # 3.0 est assez bon pour les méthodes LINQ / extension, etc.
Marc Gravell
3

Je pense que cela semble vraiment intéressant (+1), mais une chose à faire attention: le compilateur C # est assez bon pour optimiser les instructions de commutation. Pas seulement pour les courts-circuits - vous obtenez une IL complètement différente en fonction du nombre de cas que vous avez et ainsi de suite.

Votre exemple spécifique fait quelque chose que je trouverais très utile - il n'y a pas de syntaxe équivalente à cas par type, car (par exemple) typeof(Motorcycle)n'est pas une constante.

Cela devient plus intéressant dans les applications dynamiques - votre logique ici pourrait être facilement basée sur les données, donnant une exécution de style «moteur de règles».

Keith
la source
0

Vous pouvez réaliser ce que vous recherchez en utilisant une bibliothèque que j'ai écrite, appelée OneOf

Le principal avantage par rapport à switch(et ifet exceptions as control flow) est qu'il est sûr au moment de la compilation - il n'y a pas de gestionnaire par défaut ou d'interruption

   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
            )
        );

Il est sur Nuget et cible net451 et netstandard1.6

mcintyre321
la source