Le eccezioni catching / throw rendono impuro un metodo altrimenti puro?

27

I seguenti esempi di codice forniscono un contesto alla mia domanda.

La classe Room è inizializzata con un delegato. Nella prima implementazione della classe Room, non ci sono guardie contro i delegati che lanciano eccezioni. Tali eccezioni diventeranno la proprietà North, dove viene valutato il delegato (nota: il metodo Main () dimostra come viene utilizzata un'istanza Room nel codice client):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Essendo che preferirei fallire nella creazione di un oggetto piuttosto che durante la lettura della proprietà North, cambio il costruttore in privato e introduco un metodo factory statico chiamato Create (). Questo metodo intercetta l'eccezione generata dal delegato e genera un'eccezione wrapper con un messaggio di eccezione significativo:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Il blocco try-catch rende il metodo Create () impuro?

    
posta Rock Anthony Johnson 26.10.2016 - 17:43
fonte

5 risposte

25

Sì. Questa è effettivamente una funzione impura. Crea un effetto collaterale: l'esecuzione del programma continua in un punto diverso dal luogo in cui si prevede che la funzione restituisca.

Per renderlo una funzione pura, return un oggetto reale che incapsula il valore atteso dalla funzione e un valore che indica una possibile condizione di errore, come un oggetto Maybe o un oggetto Unit of Work.

    
risposta data 26.10.2016 - 17:50
fonte
12

Bene, sì ... e no.

Una funzione pura deve avere Trasparenza referenziale - cioè, dovresti essere in grado di sostituire qualsiasi chiamata possibile a un puro funziona con il valore restituito, senza modificare il comportamento del programma. * La tua funzione è sempre lanciata per determinati argomenti, quindi non c'è alcun valore di ritorno per sostituire la chiamata di funzione con, quindi cerchiamo invece ignoralo . Considera questo codice:

{
    var ignoreThis = func(arg);
}

Se func è puro, un ottimizzatore potrebbe decidere che func(arg) possa essere sostituito con il risultato. Non sa ancora quale sia il risultato, ma può dire che non viene utilizzato, quindi può solo dedurre che questa affermazione non ha alcun effetto e rimuoverla.

Ma se func(arg) capita di lanciare, questa affermazione fa qualcosa - lancia un'eccezione! Quindi l'ottimizzatore non può rimuoverlo: importa se la funzione viene chiamata o meno.

Ma ...

In pratica, questo è molto poco importante. Un'eccezione, almeno in C #, è qualcosa di eccezionale. Non dovresti usarlo come parte del tuo normale flusso di controllo - dovresti provare a prenderlo, e se riesci a catturare qualcosa, gestisci l'errore per ripristinare ciò che stavi facendo o in qualche modo farlo comunque. Se il tuo programma non funziona correttamente perché un codice che avrebbe avuto esito negativo è stato ottimizzato, stai usando le eccezioni errate (a meno che non si tratti di codice di prova, e quando costruisci per test le eccezioni non dovrebbero essere ottimizzate).

Detto questo ...

Non lanciare eccezioni dalle funzioni pure con l'intenzione che vengano catturate - esiste un buon motivo per cui i linguaggi funzionali preferiscono utilizzare le monadi anziché le eccezioni di stack-unwinding.

Se C # aveva una classe Error come Java (e molti altri linguaggi), avrei suggerito di lanciare un Error invece di un Exception . Indica che l'utente della funzione ha fatto qualcosa di sbagliato (ha passato una funzione che lancia), e tali cose sono permesse in pure funzioni. Ma C # non ha una classe Error e le eccezioni degli errori di utilizzo sembrano derivare da Exception . Il mio suggerimento è di lanciare un ArgumentException , rendendo chiaro che la funzione è stata chiamata con un argomento non valido.

* Computazionalmente parlando. Una funzione di Fibonacci implementata utilizzando la ricognizione naive richiederà molto tempo per i grandi numeri e potrebbe esaurire le risorse della macchina, ma poiché, con il tempo e la memoria illimitati, la funzione restituirà sempre lo stesso valore e non avrà effetti collaterali (oltre all'allocazione memoria e alterando quella memoria) - è ancora considerato puro.

    
risposta data 26.10.2016 - 19:58
fonte
1

Una considerazione è che il blocco try-catch non è il problema. (In base ai miei commenti alla domanda sopra).

Il problema principale è che la proprietà North è una chiamata I / O .

A quel punto nell'esecuzione del codice, il programma deve controllare l'I / O fornito dal codice client. (Non sarebbe rilevante che l'input abbia la forma di un delegato, o che l'input fosse, nominalmente, già passato).

Una volta perso il controllo dell'input, non è possibile garantire che la funzione sia pura. (Soprattutto se la funzione può lanciare).

Non sono chiaro perché non vuoi controllare la chiamata per spostare [Controlla] stanza? Come per il mio commento alla domanda:

Yes. What do you when executing the delegate may only work the first time, or return a different room on the second call? Calling the delegate can be considered / cause a side effect itself?

Come ha detto in precedenza Bart van Ingen Schenau,

Your Create function does not protect you from getting an exception when getting the property. If your delegate throws, in real life it is very likely that it will thrown only under some conditions. Chances are that the conditions for throwing are not present during construction, but they are present when getting the property.

In generale, qualsiasi tipo di caricamento lento definisce implicitamente gli errori fino a quel momento.

Suggerirei di utilizzare un metodo Sposta [Controlla]. Ciò ti consentirebbe di separare gli aspetti I / O impuri in un unico posto.

Simile alla risposta di Robert Harvey :

To make it a pure function, return an actual object that encapsulates the expected value from the function and a value indicating a possible error condition, like a Maybe object or a Unit of Work object.

Spetterebbe al writer di codice determinare come gestire l'eccezione (possibile) dall'input. Quindi il metodo può restituire un oggetto Room o un oggetto Null Room, o forse svelare l'eccezione.

Questo punto dipende da:

  • Il dominio Room considera le eccezioni Room come Null o Something Worse.
  • Come notificare al client il codice che chiama North su una Null / Exception Room. (Bail / Status Variable / Global State / Return a Monad / Whatever, Alcuni sono più puri degli altri :)).
risposta data 27.10.2016 - 23:51
fonte
-1

Hmm ... Non mi sentivo a posto. Penso che quello che stai cercando di fare è assicurarti che gli altri facciano bene il loro lavoro, senza sapere cosa stanno facendo.

Poiché la funzione è passata dal consumatore, che potrebbe essere scritta da te o da un'altra persona.

Cosa succede se la funzione non è valida per essere eseguita al momento della creazione dell'oggetto Room?

Dal codice, non potrei essere sicuro di ciò che sta facendo il Nord.

Diciamo che se la funzione è quella di prenotare la stanza, e ha bisogno del tempo e del periodo di prenotazione, otterresti un'eccezione se non hai le informazioni pronte.

Che cosa succede se è una funzione di lunga durata? Non vuoi bloccare il tuo programma mentre crei un oggetto Room, e se devi creare 1000 oggetti Room?

Se tu fossi la persona che scriverà il consumatore che consuma questa classe, ti assicureresti che la funzione passata sia scritta correttamente, vero?

Se c'è un'altra persona che scrive il consumatore, potrebbe non conoscere la condizione "speciale" di creare un oggetto Room, che potrebbe causare execption e si grattano la testa cercando di scoprire perché.

Un'altra cosa è che non è sempre una buona cosa lanciare una nuova eccezione. Il motivo è che non conterrà le informazioni dell'eccezione originale. Sai che ottieni un errore (una mesa amichevole per un'eccezione generica), ma non sapresti qual è l'errore (riferimento nullo, indice fuori limite, deviato per zero ecc.). Questa informazione è molto importante quando abbiamo bisogno di fare indagini.

Dovresti gestire l'eccezione solo quando sai cosa e come gestirlo.

Penso che il tuo codice originale sia abbastanza buono, l'unica cosa è che potresti voler gestire l'eccezione sul "Main" (o su qualsiasi livello che saprebbe come gestirlo).

    
risposta data 27.10.2016 - 02:35
fonte
-1

Non sono d'accordo con le altre risposte e dico No . Le eccezioni non rendono impura una funzione altrimenti pura.

Qualsiasi utilizzo di eccezioni potrebbe essere riscritto per utilizzare controlli espliciti per i risultati degli errori. Le eccezioni potrebbero essere considerate zucchero sintattico in aggiunta a questo. Conveniente zucchero sintattico non rende impuro il puro codice.

    
risposta data 27.10.2016 - 08:54
fonte

Leggi altre domande sui tag