Il pattern-matching contro tipi idiomatici o design scadente?


18

Sembra che il codice F # spesso corrisponda al modello rispetto ai tipi. Certamente

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

sembra comune.

Ma dal punto di vista OOP, assomiglia moltissimo al flusso di controllo basato su un controllo del tipo di runtime, che sarebbe normalmente disapprovato. Per spiegarlo, in OOP probabilmente preferiresti usare il sovraccarico:

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"

Questo è sicuramente più codice. OTOH, mi sembra OOP-y avere vantaggi strutturali:

  • l'estensione a una nuova forma di Tè facile;
  • Non devo preoccuparmi di trovare la duplicazione del flusso di controllo di scelta del percorso; e
  • la scelta della rotta è immutabile, nel senso che una volta che ne ho una Fooin mano, non devo mai preoccuparmi Bar.Route()dell'implementazione

Ci sono vantaggi nella corrispondenza dei modelli rispetto ai tipi che non vedo? È considerato idiomatico o è una capacità che non è comunemente usata?


3
Quanto ha senso visualizzare un linguaggio funzionale dal punto di vista OOP? Ad ogni modo, il vero potere della corrispondenza dei motivi è rappresentato dai motivi nidificati. È possibile solo controllare il costruttore più esterno, ma in nessun modo l'intera storia.
Ingo

Questo - 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.- sembra troppo dogmatico. A volte, vuoi separare le tue operazioni dalla tua gerarchia: forse 1) non puoi aggiungere un'operazione a una gerarchia b / c che non possiedi la gerarchia; 2) le classi che vuoi avere non corrispondono alla tua gerarchia; 3) potresti aggiungere l'op alla tua gerarchia, ma non vuoi b / c non vuoi ingombrare l'API della tua gerarchia con un mucchio di schifezze che la maggior parte dei client non usa.

4
Giusto per chiarire, Somee Nonenon sono tipi. Sono entrambi costruttori i cui tipi sono forall a. a -> option ae forall a. option a(scusate, non sono sicuro di quale sia la sintassi per le annotazioni dei tipi in F #).

Risposte:


20

Hai ragione nel dire che le gerarchie di classi OOP sono strettamente correlate ai sindacati discriminati in F # e che la corrispondenza dei modelli è strettamente correlata ai test di tipo dinamici. In effetti, è così che F # compila i sindacati discriminati in .NET!

Per quanto riguarda l'estensibilità, ci sono due lati del problema:

  • OO ti consente di aggiungere nuove sottoclassi, ma rende difficile l'aggiunta di nuove funzioni (virtuali)
  • FP ti consente di aggiungere nuove funzioni, ma rende difficile l'aggiunta di nuovi casi sindacali

Detto questo, F # ti avvertirà quando perdi un caso nella corrispondenza dei modelli, quindi l'aggiunta di nuovi casi sindacali non è poi così male.

Per quanto riguarda la ricerca di duplicati nella scelta della radice, F # ti darà un avviso quando hai una corrispondenza duplicata, ad esempio:

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

Il fatto che "la scelta del percorso sia immutabile" potrebbe anche essere problematico. Ad esempio, se si desidera condividere l'implementazione di una funzione tra Fooe Barcase, ma fare qualcos'altro per il Zoocaso, è possibile codificarlo facilmente utilizzando la corrispondenza del modello:

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

In generale, FP si concentra maggiormente sulla progettazione dei tipi e quindi sull'aggiunta di funzioni. Quindi beneficia davvero del fatto che puoi adattare i tuoi tipi (modello di dominio) in un paio di righe in un singolo file e quindi aggiungere facilmente le funzioni che operano sul modello di dominio.

I due approcci - OO e FP sono abbastanza complementari ed entrambi hanno vantaggi e svantaggi. La cosa difficile (proveniente dalla prospettiva OO) è che F # di solito usa lo stile FP come predefinito. Ma se c'è davvero più bisogno di aggiungere nuove sottoclassi, puoi sempre usare le interfacce. Ma nella maggior parte dei sistemi, devi anche aggiungere tipi e funzioni, quindi la scelta non ha molta importanza - e usare i sindacati discriminati in F # è più bello.

Consiglierei questa fantastica serie di blog per ulteriori informazioni.


3
Anche se hai ragione, vorrei aggiungere che questo non è tanto un problema di OO vs FP quanto di un problema di oggetti rispetto ai tipi di somma. A parte l'ossessione di OOP per loro, non c'è nulla sugli oggetti che li rende non funzionali. E se salti attraverso abbastanza cerchi, puoi implementare i tipi di somma anche nei linguaggi OOP tradizionali (anche se non sarà carino).
Doval

1
"E se salti attraverso abbastanza cerchi, puoi implementare i tipi di somma anche nei linguaggi OOP tradizionali (anche se non sarà carino)." -> Immagino che finirai con qualcosa di simile a come i tipi di somma F # sono codificati nel sistema di tipi di .NET :)
Tarmil

7

Hai correttamente osservato che la corrispondenza dei modelli (essenzialmente un'istruzione switch sovralimentata) e l'invio dinamico hanno somiglianze. Inoltre coesistono in alcune lingue, con un risultato molto piacevole. Tuttavia, ci sono lievi differenze.

Potrei usare il sistema di tipi per definire un tipo che può avere solo un numero fisso di sottotipi:

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

Non ci sarà mai un altro sottotipo di Boolo Option, quindi la sottoclasse non sembra essere utile (alcune lingue come Scala hanno una nozione di sottoclasse in grado di gestirla - una classe può essere contrassegnata come "finale" al di fuori dell'unità di compilazione corrente, ma i sottotipi può essere definito all'interno di questa unità di compilazione).

Poiché i sottotipi di un tipo come Optionsono ora noti staticamente , il compilatore può avvisare se dimentichiamo di gestire un caso nella nostra corrispondenza del modello. Ciò significa che una corrispondenza del modello è più simile a un downcast speciale che ci costringe a gestire tutte le opzioni.

Inoltre, l'invio di metodi dinamici (richiesto per OOP) implica anche un controllo del tipo di runtime, ma di tipo diverso. È quindi abbastanza irrilevante se eseguiamo questo tipo di controllo in modo esplicito tramite una corrispondenza del modello o implicitamente tramite una chiamata al metodo.


"Ciò significa che una corrispondenza del modello è più simile a un downcast speciale che ci costringe a gestire tutte le opzioni" - in effetti, credo che (fintanto che stai abbinando solo contro costruttori e non valori o struttura nidificata) è isomorfo a inserendo un metodo virtuale astratto nella superclasse.
Jules,

2

La corrispondenza del modello F # viene in genere eseguita con un'unione discriminata anziché con le classi (e quindi tecnicamente non è affatto un controllo del tipo). Ciò consente al compilatore di avvisarti quando non hai tenuto conto dei casi in una corrispondenza di modelli.

Un'altra cosa da notare è che in uno stile funzionale, organizzi le cose in base alla funzionalità piuttosto che ai dati, quindi le corrispondenze dei modelli ti consentono di riunire le diverse funzionalità in un posto anziché disperse tra le classi. Ciò ha anche il vantaggio di poter vedere come vengono gestiti gli altri casi proprio accanto a dove è necessario apportare le modifiche.

L'aggiunta di una nuova opzione è simile a:

  1. Aggiungi una nuova opzione alla tua unione discriminata
  2. Correggi tutti gli avvisi su corrispondenze di pattern incomplete

2

Parzialmente, lo vedi più spesso nella programmazione funzionale perché usi i tipi per prendere decisioni più spesso. Mi rendo conto che probabilmente hai appena scelto degli esempi più o meno a caso, ma l'equivalente OOP del tuo esempio di corrispondenza dei modelli sarebbe più spesso simile a:

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

In altre parole, è relativamente raro usare il polimorfismo per evitare cose di routine come i controlli null in OOP. Proprio come un programmatore OO non crea un oggetto null in ogni piccola situazione, un programmatore funzionale non sempre sovraccarica una funzione, specialmente quando sai che il tuo elenco di schemi è garantito per essere esaustivo. Se usi il sistema dei tipi in più situazioni, lo vedrai usato in modi a cui non sei abituato.

Al contrario, la programmazione funzionale idiomatica equivalente al tuo esempio OOP molto probabilmente non userebbe il pattern matching, ma avrebbe fooRoutee barRoutefunzioni che verrebbero passate come argomenti al codice chiamante. Se qualcuno usasse il pattern matching in quella situazione, di solito sarebbe considerato sbagliato, proprio come qualcuno che accendesse i tipi sarebbe considerato sbagliato in OOP.

Quindi, quando il pattern matching è considerato un buon codice di programmazione funzionale? Quando stai facendo molto di più che guardare solo i tipi e quando estendi i requisiti non dovrai aggiungere più casi. Ad esempio, Some valnon si limita a verificare che optabbia tipo Some, ma si lega anche valal tipo sottostante per un uso sicuro sull'altro lato del file ->. Sai che molto probabilmente non avrai mai bisogno di un terzo caso, quindi è un buon uso.

La corrispondenza dei modelli può assomigliare superficialmente a un'istruzione switch orientata agli oggetti, ma c'è molto di più in corso, specialmente con modelli più lunghi o nidificati. Assicurati di prendere in considerazione tutto ciò che sta facendo prima di dichiararlo equivalente a un codice OOP mal progettato. Spesso gestisce in modo succinto una situazione che non può essere rappresentata in modo chiaro in una gerarchia ereditaria.


So che lei sa questo, e probabilmente scivolato la vostra mente durante la scrittura la risposta, ma nota che Somee Nonenon sono i tipi, in modo da non siete pattern matching sui tipi. Corrispondenza di modelli su costruttori dello stesso tipo . Non è come chiedere "instanceof".
Andres F.
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.