In che modo i tipi di unione sono migliori per la correttezza di un'interfaccia comune?

4

Ho appena iniziato a familiarizzare con la programmazione funzionale, principalmente tramite F #, e c'è un particolare idioma funzionale che non comprendo pienamente i vantaggi di. L'ho visto descritto in alcuni punti, ma mi riferirò alla versione in questo articolo .

L'articolo descrive un carrello della spesa con diversi requisiti, che in pratica lo pone in uno dei tre stati: Empty , Active (occupato da uno o più elementi ma non ancora pagato) o PaidFor . Si sostiene che usando F #, è facile garantire la correttezza perché il compilatore ti impedirà di eseguire operazioni illegali sugli stati del carrello. Ad esempio:

let addItemToCart cart item =  
   match cart with
   | Empty state -> state.Add item
   | Active state -> state.Add item
   | PaidFor state ->  
       printfn "ERROR: The cart is paid for"
       cart   

Solo gli stati Empty e Active hanno metodi associati a Add , quindi se provassimo a scrivere:

| PaidFor state -> state.Add item

o simili, otterremmo un errore del compilatore.

Per confronto, un approccio OO potrebbe (in C #) apparire come:

interface ICart
{
    ICart AddItem(CartItem item);
}

class EmptyCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        return new ActiveCart(item);
    }
}

class ActiveCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        return new ActiveCart(_items.Add(item));
    }
}

class PaidForCart : ICart
{
    public ICart AddItem(CartItem item)
    {
        throw new InvalidOperationException("Who cares about Liskov anyway?");
    }
}

Quello che sto cercando di capire è quale vantaggio ottiene un cliente dall'avere addItemToCart piuttosto che ICart.AddItem . In entrambi i casi, non esiste un controllo in fase di compilazione che impedisca al client di utilizzare quella funzione con un carrello a pagamento. In ogni caso, il cliente non ha il controllo su ciò che accade nel caso di errore.

La versione funzionale potrebbe essere modificata per dare qualche controllo al client su ciò che accade nel caso di errore (un callback, o qualche tipo di wrapper sul risultato), ma l'orientato agli oggetti potrebbe essere modificato per farlo altrettanto facilmente (magari con un metodo TryAddItem , che sarebbe un po 'più idiomatico OO).

Quindi cosa mi manca?

Modifica

È stato detto che nei commenti che nella versione C #, chiunque poteva creare un'implementazione ICart . Per risolvere questo problema: una versione alternativa sarebbe sostituire ICart con una classe Cart astratta e renderne interni i costruttori. Questo non è l'ideale e in alcune situazioni potrebbe non essere fattibile, ma penso che nel caso generale funzioni abbastanza bene.

    
posta Ben Aaronson 19.11.2014 - 19:53
fonte

2 risposte

5

L ' è non ha alcun beneficio dall'avere addItemToCart piuttosto che ICart.AddItem . Sono essenzialmente identici. Entrambi hanno necessariamente un controllo di runtime, perché vuoi essere in grado di avere una variabile con qualsiasi tipo di Cart in esso, e non saprai al momento della compilazione quale verrà passato nella funzione.

Dove si presenta il vantaggio è quando per la prima volta implementi addItemToCart , o più tardi quando aggiungi un'altra operazione allo stesso livello di astrazione, diciamo oneClickAddAndPay . Il tipo di unione ti darà un errore di compilazione se provi a Add di un articolo in un PaidFor carrello o se dimentichi completamente di tenere conto di PaidFor di carrelli. L'interfaccia ICart non può catturare quel tipo di errore di programmazione in fase di compilazione.

In altre parole, il tipo di unione non può spostare tutti i tipi di errori per compilare il tempo, ma sposta alcuni . Se non si aggiungono molte funzioni come oneClickAddAndPay che riutilizzano la funzionalità Add esistente, non ti compreranno molto.

    
risposta data 19.11.2014 - 23:13
fonte
2

Nel tuo esempio non vedo alcun beneficio, ma altri linguaggi (come Ceylon ) usano i tipi di unione in modi utili. Immagina di fare qualcosa di simile a questo (C #)

int Length<A> (String | List<A> obj) {...}

Sebbene questo esempio non sia molto significativo dato che String implementa IEnumerable, l'idea alla base di questo è che puoi ricevere un parametro che può essere di un tipo diverso, usare i campi / proprietà / metodi che questi tipi hanno in comune e usare switch / case quando è necessario il comportamento dipendente dal tipo.

In F # credo che potresti creare alcuni tipi di helper per realizzare piccole cose come questa. Preferisco il modo di Ceylon: Polymorphism + (principalmente anonimo) Tipi di sindacati.

I tipi di unione possono essere di grande aiuto per la correttezza se si assume che null sia l'unica istanza di tipo Null , quindi se si vuole dichiarare che qualcosa può essere null si dovrebbe usare il tipo T | Null o la sua abbreviazione T? .

    
risposta data 11.07.2015 - 04:53
fonte

Leggi altre domande sui tag