La corrispondenza del modello rispetto a tipi di design idiomatici o scadenti?

16

Sembra che il codice F # spesso disegni le corrispondenze con i tipi. Certamente

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

sembra comune.

Ma da una prospettiva OOP, sembra molto simile al flusso di controllo basato su un controllo di tipo runtime, che in genere sarebbe disapprovato. Per scriverlo, in OOP probabilmente preferiresti usare l'overloading:

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, sembra che la mia mente OOP-y abbia vantaggi strutturali:

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

Ci sono vantaggi rispetto alla corrispondenza del modello rispetto ai tipi che non vedo? È considerato idiomatico o è una capacità che non è comunemente usata?

    
posta Larry OBrien 03.03.2014 - 19:10
fonte

4 risposte

18

Sei corretto in quanto le gerarchie di classe OOP sono strettamente correlate alle unioni discriminate in F # e quella corrispondenza del modello è strettamente correlata ai test di tipo dinamico. In realtà, questo è in realtà come F # compila le unioni discriminate su .NET!

Riguardo all'estensibilità, ci sono due lati del problema:

  • OO ti consente di aggiungere nuove sottoclassi, ma rende difficile aggiungere nuove funzioni (virtuali)
  • FP ti consente di aggiungere nuove funzioni, ma rende difficile aggiungere nuovi casi di unione

Detto questo, F # ti darà un avviso quando ti perdi un caso nel pattern matching, quindi aggiungere nuovi casi di unione non è poi così male.

Riguardo alla ricerca di duplicazioni nella scelta di root - F # ti darà un avviso quando hai una corrispondenza che è 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 dell'itinerario sia immutabile" potrebbe anche essere problematico. Ad esempio, se desideri condividere l'implementazione di una funzione tra Foo e Bar casi, ma fai qualcos'altro per il caso Zoo , puoi codificarlo facilmente utilizzando la corrispondenza del modello:

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

In generale, FP è più focalizzato sulla prima progettazione dei tipi e quindi sull'aggiunta di funzioni. Quindi, trae vantaggio dal 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 e hanno entrambi vantaggi e svantaggi. La cosa complicata (proveniente dalla prospettiva OO) è che F # usa solitamente 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 ugualmente aggiungere tipi e funzioni, quindi la scelta non ha molta importanza e usare le unioni discriminate in F # è più bello.

Consiglierei questa fantastica serie di blog per ulteriori informazioni.

    
risposta data 03.03.2014 - 19:26
fonte
7

Hai osservato correttamente che la corrispondenza del modello (essenzialmente un'istruzione switch sovralimentata) e la spedizione dinamica 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)

Ci sarà mai un altro sottotipo di Bool o Option , quindi la sottoclasse non sembra essere utile (alcuni linguaggi come Scala hanno una nozione di sottoclasse che può gestire questo - una classe può essere contrassegnato come "finale" al di fuori dell'unità di compilazione corrente, ma i sottotipi possono essere definiti all'interno di questa unità di compilazione).

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

Inoltre, la spedizione del metodo dinamico (che è richiesta per OOP) implica anche un controllo del tipo di runtime, ma di un tipo diverso. Pertanto è abbastanza irrilevante se facciamo questo tipo di controllo esplicitamente attraverso una corrispondenza di modello o implicitamente attraverso una chiamata di metodo.

    
risposta data 03.03.2014 - 19:28
fonte
2

La corrispondenza del modello F # viene tipicamente eseguita con un'unione discriminata piuttosto che con le classi (e quindi non è tecnicamente un controllo di tipo). Ciò consente al compilatore di darti un avvertimento quando hai smarrito i casi in un pattern match.

Un'altra cosa da notare è che in uno stile funzionale, organizzi le cose in base alla funzionalità piuttosto che ai dati, quindi le corrispondenze di pattern ti consentono di riunire le diverse funzionalità in un unico posto piuttosto che sparse tra le classi. Questo 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 è la seguente:

  1. Aggiungi una nuova opzione al tuo sindacato discriminato
  2. Correggi tutti gli avvertimenti su corrispondenze di pattern incomplete
risposta data 03.03.2014 - 19:25
fonte
2

In parte, lo vedi più spesso nella programmazione funzionale perché usi i tipi per prendere decisioni più spesso. Mi rendo conto che probabilmente hai scelto gli esempi più o meno a caso, ma l'equivalente OOP del tuo esempio di abbinamento di pattern sarebbe più 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 nulli in OOP. Proprio come un programmatore OO non crea un oggetto nullo in ogni piccola situazione, un programmatore funzionale non sovraccaricherà sempre un funzione, soprattutto quando sai che la tua lista di modelli è garantita per essere esauriente. Se utilizzi il sistema di tipi in più situazioni, lo vedrai usato in modi in cui non sei abituato.

Al contrario, la programmazione funzionale idiomatica equivalente al tuo esempio di OOP molto probabilmente non userebbe la corrispondenza del modello, ma avrebbe fooRoute e barRoute funzioni che sarebbero passate come argomenti al codice chiamante. Se qualcuno utilizzava la corrispondenza dei pattern in quella situazione, di solito sarebbe considerato sbagliato, proprio come qualcuno che accendesse i tipi sarebbe considerato sbagliato in OOP.

Quindi quando l'abbinamento di modelli considera un buon codice di programmazione funzionale? Quando fai qualcosa di più che limitarti a guardare i tipi e quando estendi i requisiti non è necessario aggiungere altri casi. Ad esempio, Some val non si limita a verificare che opt abbia tipo Some , ma associa anche val al tipo sottostante per l'utilizzo sicuro dal testo sull'altro lato di -> . Probabilmente non avrai mai più bisogno di un terzo caso, quindi è un buon uso.

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

    
risposta data 03.03.2014 - 21:02
fonte