idea di abbinamento / modello


151

Ho esaminato F # di recente e, sebbene non sia probabile che salti presto la recinzione, evidenzia sicuramente alcune aree in cui C # (o il supporto della libreria) potrebbe semplificare la vita.

In particolare, sto pensando alla capacità di corrispondenza dei pattern di F #, che consente una sintassi molto ricca, molto più espressiva dell'attuale switch / equivalenti C # condizionali. Non proverò a fare un esempio diretto (il mio F # non è all'altezza), ma in breve permette:

  • abbina per tipo (con verifica della copertura completa per i sindacati discriminati) [nota che questo influisce anche sul tipo per la variabile associata, dando accesso ai membri ecc.]
  • partita per predicato
  • combinazioni di quanto sopra (e forse alcuni altri scenari di cui non sono a conoscenza)

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

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

dove getRentPrice è un Func <Veicolo, int>.

[nota - forse Switch / Case qui sono i termini sbagliati ... ma mostra l'idea]

Per me, questo è molto più chiaro dell'equivalente usando ripetuto if / else, o un condizionale ternario composito (che diventa molto caotico per le espressioni non banali - parentesi a bizzeffe). Evita anche un sacco di casting e consente una semplice estensione (direttamente o tramite metodi di estensione) a corrispondenze più specifiche, ad esempio una corrispondenza InRange (...) paragonabile a VB Select ... Caso "x To y "utilizzo.

Sto solo cercando di valutare se la gente pensa che ci siano molti benefici da costrutti come sopra (in assenza di supporto linguistico)?

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

  • una versione Func <TSource, TValue> per la valutazione - paragonabile alle dichiarazioni condizionali ternarie composte
  • una versione Action <TSource> - paragonabile a if / else if / else if / else if / else
  • una versione di Express <Func <TSource, TValue >> - come la prima, ma utilizzabile da provider LINQ arbitrari

Inoltre, l'utilizzo della versione basata su Expression consente di riscrivere l'albero di Expression, fondendo essenzialmente tutti i rami in una singola espressione condizionale composita, piuttosto che utilizzare ripetute invocazioni. Non ho verificato di recente, ma in alcune build di Entity Framework sembra ricordare che ciò sia necessario, poiché InvocationExpression non è piaciuto molto. Inoltre, consente un utilizzo più efficiente con LINQ-to-Objects, poiché evita ripetute invocazioni dei delegati: i test mostrano una corrispondenza come quella sopra (utilizzando il modulo Espressione) che si comporta alla stessa velocità [marginalmente più veloce, in effetti] rispetto all'equivalente C # istruzione condizionale composita. Per completezza, la versione basata su Func <...> ha impiegato 4 volte più dell'istruzione condizionale C #, ma è ancora molto rapida ed è improbabile che rappresenti un grosso collo di bottiglia nella maggior parte dei casi d'uso.

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


"Sto solo cercando di valutare se la gente pensa che ci siano molti benefici da costrutti come sopra (in assenza di supporto linguistico)?" IMHO, si. Non esiste già qualcosa di simile? In caso contrario, sentiti incoraggiato a scrivere una libreria leggera.
Konrad Rudolph,

10
È possibile utilizzare VB .NET che lo supporta nell'istruzione case selezionata. Eek!
Jim Burger,

Anche io suonerò il mio corno e aggiungerò un link alla mia biblioteca: funzionale-dotnet
Alexey Romanov,

1
Mi piace questa idea e crea una custodia molto bella e molto più flessibile; tuttavia, non è davvero un modo abbellito di usare la sintassi simile a Linq come wrapper if-then? Scoraggerei qualcuno dall'usarlo al posto del vero affare, cioè una switch-casedichiarazione. Non fraintendetemi, penso che abbia il suo posto e probabilmente cercherò un modo per implementare.
Estratto

2
Sebbene questa domanda abbia più di due anni, è opportuno menzionare che C # 7 uscirà presto (ish) con capacità di abbinamento dei modelli.
Abion47

Risposte:


22

So che è un vecchio argomento, ma in 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("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

La notevole differenza qui tra C # e F # è la completezza della corrispondenza del modello. Che la corrispondenza del modello copra tutti gli eventuali casi disponibili, completamente descritti, avvisi del compilatore, in caso contrario. Mentre si può legittimamente sostenere che il caso predefinito fa questo, spesso è in pratica un'eccezione di runtime.
VoronoiPotato,

37

Dopo aver provato a fare cose "funzionali" in C # (e anche aver tentato un libro su di esso), sono giunto alla conclusione che no, con poche eccezioni, tali cose non aiutano molto.

Il motivo principale è che linguaggi come F # ottengono molto del loro potere nel supportare veramente queste funzionalità. Non "puoi farlo", ma "è semplice, è chiaro, è previsto".

Ad esempio, nella corrispondenza dei modelli, il compilatore ti dice se c'è una partita incompleta o quando un'altra partita non verrà mai colpita. Questo è meno utile con i tipi aperti, ma quando si abbina un'unione o tuple discriminate, è molto elegante. In F #, ti aspetti che le persone modellino la corrispondenza e ha immediatamente senso.

Il "problema" è che una volta che inizi a utilizzare alcuni concetti funzionali, è naturale voler continuare. Tuttavia, sfruttare le tuple, le funzioni, l'applicazione parziale del metodo e il curry, la corrispondenza dei modelli, le funzioni nidificate, i generici, il supporto della monade, 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 (tra progetti) 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 come elencato) perché si adattano perché la sintassi C # la supporta bene.
  • Astrarre modelli di dichiarazioni comuni. Blocchi complicati try / catch / finally o altri blocchi di codice coinvolti (spesso fortemente generici). L'estensione di LINQ-to-SQL si adatta anche qui.
  • Tuple, in una certa misura.

** Ma nota: la mancanza di generalizzazione automatica e inferenza del tipo ostacolano davvero l'uso anche di queste funzionalità. **

Tutto ciò ha detto, come ha detto qualcun altro, in una piccola squadra, per uno scopo specifico, sì, forse possono aiutarti se sei bloccato con C #. Ma nella mia esperienza, di solito sembravano più seccature di quanto valessero: YMMV.

Alcuni altri link:


25

Probabilmente il motivo per cui C # non semplifica l'attivazione del tipo è perché si tratta principalmente di un linguaggio orientato agli oggetti, e il modo "corretto" per farlo in termini orientati agli oggetti sarebbe definire un metodo GetRentPrice su Vehicle e sovrascriverlo in classi derivate.

Detto questo, ho trascorso 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 numero di posti in cui sarebbe stato utile prima (ad esempio quando tu non stanno scrivendo i tipi che è necessario attivare in modo da non poter implementare un metodo virtuale su di essi) ed è qualcosa che accolgo con favore nella lingua insieme ai sindacati discriminati.

[Modifica: parte rimossa sull'esecuzione come indicato da Marc potrebbe essere in cortocircuito]

Un altro potenziale problema è quello dell'usabilità: dall'ultima chiamata è chiaro cosa succede se la partita non soddisfa 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 a usare per risolvere questo tipo di problema è usare un campo del dizionario con il tipo come chiave e lambda come valore, che è piuttosto conciso da costruire usando la sintassi dell'inizializzatore di oggetti; tuttavia, ciò rappresenta solo il tipo concreto e non consente ulteriori predicati, pertanto potrebbe non essere adatto a casi più complessi. [Nota a margine: se guardi l'output del compilatore C #, spesso converte le istruzioni switch in tabelle jump basate su dizionario, quindi non sembra esserci una buona ragione per cui non potrebbe supportare l'attivazione dei tipi]


1
In realtà - la versione che ho ha un corto circuito sia nella versione delegata che in quella espressiva. La versione dell'espressione viene compilata in un condizionale composto; la versione delegata è semplicemente un insieme di predicati e funzioni / azioni - una volta che ha una corrispondenza si ferma.
Marc Gravell

Interessante: da uno sguardo superficiale ho supposto che avrebbe dovuto eseguire almeno il controllo di base di ogni condizione in quanto sembrava una catena di metodi, ma ora mi rendo conto che i metodi stanno effettivamente concatenando un'istanza di oggetto per costruirla in modo da poterlo fare. Modificherò la mia risposta per rimuovere quella dichiarazione.
Greg Beech,

22

Non credo che questo tipo di biblioteche (che si comportano come estensioni di lingua) possano ottenere ampia accettazione, ma sono divertenti da giocare e possono essere davvero utili per i piccoli team che lavorano in domini specifici dove questo è utile. Ad esempio, se stai scrivendo tonnellate di "regole / logiche aziendali" che eseguono test di tipo arbitrari come questo e quant'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, l'F # corrispondente è 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 lungo le linee 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

2
Grazie per la versione F #. Immagino che mi piaccia il modo in cui F # gestisce questo, ma non sono sicuro che F # (nel complesso) sia la scelta giusta al momento, quindi devo camminare per quella via di mezzo ...
Marc Gravell

13

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

Ecco la mia implementazione di una classe che fornisce (quasi) la stessa sintassi che descrivi

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

Ecco un po 'di codice di prova:

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

9

Corrispondenza del modello (come descritto qui ), il suo scopo è di decostruire i valori in base alle loro specifiche di tipo. Tuttavia, il concetto di una classe (o tipo) in C # non è d'accordo con te.

Non c'è nulla di sbagliato nel design del linguaggio multi-paradigma, al contrario, è molto bello avere lambda in C #, e Haskell può fare cose imperative, ad esempio IO. Ma non è una soluzione molto elegante, non alla moda di Haskell.

Ma poiché i linguaggi di programmazione procedurale sequenziale possono essere compresi in termini di calcolo lambda e C # sembra adattarsi bene ai parametri di un linguaggio procedurale sequenziale, è una buona scelta. Ma, prendendo qualcosa dal puro contesto funzionale di dire Haskell, e poi mettere quella caratteristica in un linguaggio che non è puro, bene, facendo proprio questo, non garantirà un risultato migliore.

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


1
Può essere. Anzi, farei fatica a pensare a un convincente argomento "killer" sul perché sarebbe necessario (al contrario di "forse bello in alcuni casi marginali al costo di rendere il linguaggio più complesso").
Marc Gravell

5

IMHO il modo OO di fare queste cose è il modello Visitatore. I metodi del membro visitatore agiscono semplicemente come costrutti del caso e si lascia che la lingua stessa gestisca l'invio appropriato senza dover "sbirciare" i tipi.


4

Anche se non è molto "C-sharpey" per attivare il tipo, so che il costrutto sarebbe piuttosto utile in uso generale - ho almeno un progetto personale che potrebbe usarlo (anche se il suo ATM gestibile). C'è un grosso problema di compilazione delle prestazioni, con l'albero di espressione riscritto?


Non se si memorizza nella cache l'oggetto per il riutilizzo (che è in gran parte il modo in cui funzionano le espressioni lambda C #, tranne che il compilatore nasconde il codice). La riscrittura migliora decisamente le prestazioni compilate - tuttavia, per un uso regolare (piuttosto che LINQ-to-Something) mi aspetto che la versione delegata possa essere più utile.
Marc Gravell

Nota anche - non è necessariamente un tipo di accensione - potrebbe anche essere usato come condizionale composito (anche tramite LINQ) - ma senza un test disordinato x =>? Risultato1: (Test2? Risultato2: (Test3? Risultato 3: Risultato4))
Marc Gravell

Bello da sapere, anche se intendevo dire le prestazioni della compilation attuale : quanto tempo impiega csc.exe - non ho abbastanza familiarità con C # per sapere se questo è davvero un problema, ma è un grosso problema per C ++.
Simon Buchan,

csc non lampeggerà a questo - è così simile a come funziona LINQ, e il compilatore C # 3.0 è abbastanza buono con i metodi LINQ / extension ecc.
Marc Gravell

3

Penso che questo appaia davvero interessante (+1), ma una cosa da fare attenzione: il compilatore C # è abbastanza bravo nell'ottimizzare le istruzioni switch. Non solo per i cortocircuiti: ottieni IL completamente diverso a seconda di quanti casi hai e così via.

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

Questo diventa più interessante nell'applicazione dinamica: la tua logica qui potrebbe essere facilmente guidata dai dati, dando un'esecuzione in stile "motore delle regole".


0

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

Il vantaggio principale rispetto a switch(e ife exceptions as control flow) è che è sicuro in fase di compilazione: non esiste un gestore predefinito o fallito

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

È su Nuget e punta a net451 e netstandard1.6

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.