idea di abbinamento interruttore / modello

Di recente ho guardato a F # e, anche se non sono in grado di saltare la recinzione in tempi brevi, evidenzia in modo evidente alcune aree in cui C # (o supporto della libreria) potrebbe semplificare la vita.

In particolare, sto pensando alla capacità di corrispondenza dei pattern di F #, che consente una syntax molto ricca – molto più espressiva rispetto all’attuale switch / C # equivalente. Non cercherò di dare un esempio diretto (il mio F # non è all’altezza), ma in breve permette:

  • corrisponde per tipo (con controllo a copertura completa per unioni discriminate) [nota che questo deduce anche il tipo per la variabile associata, dando accesso ai membri ecc]
  • partita per predicato
  • combinazioni di quanto sopra (e possibilmente alcuni altri scenari di cui non sono a conoscenza)

Mentre sarebbe bello per C # prendere in prestito [ahem] parte di questa ricchezza, nel frattempo ho visto cosa si può fare in fase di esecuzione – per esempio, è abbastanza facile raggruppare alcuni oggetti per consentire:

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

dove getRentPrice è Func .

[nota – forse Switch / Case qui è i termini sbagliati … ma mostra l’idea]

Per me, questo è molto più chiaro dell’equivalente usando ripetuto se / else, o un condizionale ternario composito (che diventa molto disordinato per espressioni non banali – a intervalli abbondanti). Evita anche molti lanci e consente l’estensione semplice (direttamente o tramite metodi di estensione) a corrispondenze più specifiche, ad esempio una partita InRange (…) paragonabile al VB Seleziona … Caso “x A y “utilizzo.

Sto solo cercando di valutare se le persone pensano che ci sia molto beneficio da costrutti come sopra (in assenza di supporto linguistico)?

Nota inoltre che ho giocato con 3 varianti di quanto sopra:

  • una versione Func per la valutazione – paragonabile alle dichiarazioni condizionali composte da ternari
  • una versione di Action – paragonabile a if / else se / else se / else se / else
  • un’espressione <Func > versione – come prima, ma utilizzabile da provider LINQ arbitrari

Inoltre, l’utilizzo della versione basata su espressioni abilita la riscrittura di una struttura ad albero di espressioni, in pratica integrando tutti i rami in un’unica espressione condizionale composita, anziché utilizzare l’invocazione ripetuta. Non l’ho verificato di recente, ma in alcune prime build di Entity Framework mi sembra di ricordare che questo è necessario, in quanto non piaceva molto a InvocationExpression. Consente inoltre un utilizzo più efficiente con LINQ-to-Objects, poiché evita ripetute invocazioni di delegati: i test mostrano una corrispondenza come quella sopra (utilizzando il modulo Expression) con la stessa velocità [marginalmente più veloce, di fatto] rispetto all’equivalente C # dichiarazione condizionale composita. Per completezza, la versione basata su Func ha impiegato 4 volte più tempo dell’istruzione condizionale C #, ma è ancora molto veloce ed è improbabile che sia un collo di bottiglia principale nella maggior parte dei casi d’uso.

Accolgo con favore qualsiasi pensiero / input / critica / etc su quanto sopra (o sulle possibilità di un supporto linguistico C # più ricco … ecco sperando ;-p).

So che è un vecchio argomento, ma nel c # 7 puoi fare:

 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(""); break; case null: throw new ArgumentNullException(nameof(shape)); } 

L’ eccellente blog di Bart De Smet ha una serie di 8 parti sul fare esattamente ciò che descrivi. Trova la prima parte qui .

Dopo aver provato a fare cose così “funzionali” in C # (e anche tentare un libro su di esso), sono giunto alla conclusione che no, con alcune eccezioni, queste cose non aiutano troppo.

La ragione principale è che linguaggi come F # ottengono molto del loro potere dal supportare veramente queste funzionalità. Non “puoi farlo”, ma “è semplice, è chiaro, è previsto”.

Ad esempio, nel pattern matching, ottieni il compilatore che ti dice se c’è una corrispondenza incompleta o quando non verrà mai colpita un’altra partita. Questo è meno utile con i tipi aperti, ma quando si abbina un’unione o tuple discriminata, è molto elegante. In F #, ti aspetti che le persone modellano le corrispondenze, e ha immediatamente un senso.

Il “problema” è che una volta che inizi a utilizzare alcuni concetti funzionali, è naturale voler continuare. Tuttavia, sfruttando tuple, funzioni, applicazione di metodi parziali e currying, corrispondenza di modelli, funzioni annidate, generici, supporto di monad, ecc. In C # diventa molto brutto, molto rapidamente. È divertente, e alcune persone molto intelligenti hanno fatto cose molto interessanti in C #, ma in realtà usarlo sembra pesante.

Quello che ho finito per usare spesso (across-projects) in C #:

  • Funzioni di sequenza, tramite metodi di estensione per IEnumerable. Cose come ForEach o Process (“Applica”? – eseguono un’azione su un elemento della sequenza così come è elencato) si adattano perché la syntax C # lo supporta bene.
  • Astrarre schemi di dichiarazione comuni. Complicato try / catch / finally blocchi o altri blocchi di codice coinvolti (spesso molto generici). L’estensione di LINQ-to-SQL si adatta anche qui.
  • Tuple, in una certa misura.

** Ma fai attenzione: la mancanza di generalizzazione automatica e di inferenza del tipo ostacola davvero l’utilizzo di queste stesse funzionalità. **

Tutto questo ha detto, come qualcun altro ha menzionato, in una piccola squadra, per uno scopo specifico, sì, forse possono aiutare se sei bloccato con C #. Ma nella mia esperienza, di solito sentivano più fastidio di quanto valessero – YMMV.

Alcuni altri link:

  • Il parco giochi Mono.Rocks ha molte cose simili (così come aggiunte non funzionali alla programmazione ma utili).
  • La libreria funzionale C # di Luca Bolognese
  • C # funzionale di Matthew Podwysocki su MSDN

Probabilmente il motivo per cui C # non rende semplice l’triggerszione del tipo è perché è principalmente un linguaggio orientato agli oggetti, e il modo ‘corretto’ per farlo in termini orientati agli oggetti sarebbe definire un metodo GetRentPrice su Vehicle e sostituirlo nelle classi derivate.

Detto questo, ho passato un po ‘di tempo a giocare con linguaggi multi-paradigma e funzionali come F # e Haskell che hanno questo tipo di capacità, e mi sono imbattuto in un certo numero di posti in cui sarebbe stato utile prima (es. non sto scrivendo i tipi che devi triggersre per non implementare un metodo virtuale su di essi) ed è qualcosa che accetterei nella lingua insieme a unioni discriminate.

[Modifica: parte rimossa relativa alla performance, in quanto Marc ha indicato che potrebbe essere cortocircuitato]

Un altro potenziale problema è l’usabilità: dall’ultima chiamata è chiaro che cosa succede se la partita non riesce a soddisfare alcuna condizione, ma qual è il comportamento se corrisponde a due o più condizioni? Dovrebbe generare un’eccezione? Dovrebbe restituire la prima o l’ultima partita?

Un modo che tendo ad usare per risolvere questo tipo di problema è usare un campo dizionario con il tipo come chiave e il lambda come valore, che è piuttosto difficile da build usando la syntax di inizializzazione dell’object; tuttavia, questo rappresenta solo il tipo concreto e non consente ulteriori predicati, pertanto potrebbe non essere adatto per casi più complessi. [Nota a margine: se si guarda l’output del compilatore C #, converte frequentemente le istruzioni switch in tabelle di salto basate sul dizionario, quindi non sembra esserci una buona ragione per cui non possa supportare l’triggerszione dei tipi]

Non credo che questo genere di librerie (che si comportano come estensioni di un linguaggio) possano ottenere un’ampia accettazione, ma sono divertenti da giocare e possono essere davvero utili per i piccoli team che lavorano in domini specifici in cui ciò è utile. Ad esempio, se si scrivono tonnellate di “regole aziendali / logica” che eseguono test di tipo arbitrario come questo e qualcos’altro, posso vedere come sarebbe utile.

Non ho idea se questa sia probabilmente una caratteristica del linguaggio C # (sembra dubbio, ma chi può vedere il futuro?).

Per riferimento, il corrispondente F # è approssimativamente:

 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" 

supponendo che tu abbia definito una gerarchia di classi sulla falsariga di

 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 

Per rispondere alla tua domanda, sì, penso che i costrutti sintattici di corrispondenza dei modelli siano utili. Per uno vorrei vedere il supporto sintattico in C # per questo.

Ecco la mia implementazione di una class che fornisce (quasi) la stessa syntax che descrivi

 public class PatternMatcher { List, Func>> cases = new List,Func>>(); public PatternMatcher() { } public PatternMatcher Case(Predicate condition, Func function) { cases.Add(new Tuple, Func>(condition, function)); return this; } public PatternMatcher Case(Predicate condition, Func function) { return Case( o => o is T && condition((T)o), o => function((T)o)); } public PatternMatcher Case(Func function) { return Case( o => o is T, o => function((T)o)); } public PatternMatcher Case(Predicate condition, Output o) { return Case(condition, x => o); } public PatternMatcher Case(Output o) { return Case(x => o); } public PatternMatcher Default(Func function) { return Case(o => true, function); } public PatternMatcher 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"); } } 

Ecco alcuni codici di 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() .Case(bike => 100 + bike.Cylinders * 10) .Case(30) .Case(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case(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)); } } 

Pattern matching (come descritto qui ), il suo scopo è quello di debuild i valori in base alle loro specifiche di tipo. Tuttavia, il concetto di una class (o tipo) in C # non è d’accordo con te.

Non c’è niente di sbagliato nella progettazione del linguaggio multi-paradigma, al contrario, è molto bello avere lambda in C #, e Haskell può fare cose imperative per es. IO. Ma non è una soluzione molto elegante, non nella moda Haskell.

Ma dal momento che i linguaggi di programmazione procedurale sequenziale possono essere compresi in termini di lambda calcolo e C # accade di adattarsi bene all’interno dei parametri di un linguaggio procedurale sequenziale, è una buona misura. Ma, prendendo qualcosa dal puro contesto funzionale di Haskell, e poi mettendo quella caratteristica in un linguaggio che non è puro, beh, facendo proprio questo, non garantirà un risultato migliore.

Il mio punto è questo, ciò che rende il tick matching del modello è legato al design del linguaggio e al modello dei dati. Detto questo, non credo che la corrispondenza dei pattern sia una caratteristica utile di C # perché non risolve i problemi tipici di C # né si adatta bene al paradigma di programmazione imperativo.

IMHO il modo OO di fare queste cose è il pattern Visitor. I metodi dei membri dei visitatori agiscono semplicemente come costrutti maiuscoli e consentono al linguaggio stesso di gestire la distribuzione appropriata senza dover “sbirciare” i tipi.

Anche se non è molto “C-sharpey” per accendere il tipo, so che il costrutto sarebbe abbastanza utile in generale – ho almeno un progetto personale che potrebbe usarlo (anche se il suo ATM gestibile). C’è un problema di prestazioni molto compilato, con la riscrittura delle espressioni?

Penso che questo sia davvero interessante (+1), ma una cosa da tenere a mente: il compilatore C # è piuttosto bravo nell’ottimizzare le dichiarazioni switch. Non solo per cortocircuiti: ottieni un IL completamente diverso a seconda di quanti casi hai e così via.

Il tuo esempio specifico fa qualcosa che troverei molto utile – non esiste syntax equivalente a caso per tipo, poiché (per esempio) typeof(Motorcycle) non è una costante.

Ciò diventa più interessante nell’applicazione dynamic: qui la tua logica potrebbe essere facilmente basata sui dati, dando l’esecuzione in stile ‘regola-motore’.

Puoi ottenere ciò che cerchi utilizzando una libreria che ho scritto, chiamata OneOf

Il principale vantaggio rispetto a switch (e if e ad exceptions as control flow ) è che è sicuro in fase di compilazione – non esiste un gestore predefinito o una caduta

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

È su Nuget e prende di mira net451 e netstandard1.6