Catturare / lanciare eccezioni rende impuro un metodo altrimenti puro?


27

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

La classe Room viene inizializzata con un delegato. Nella prima implementazione della classe Room, non ci sono guardie contro i delegati che generano eccezioni. Tali eccezioni saliranno alla proprietà North, in cui 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

    }
}

Dato che preferirei non riuscire a creare l'oggetto piuttosto che leggere la proprietà North, cambio il costruttore in privato e introduco un metodo factory statico chiamato Create (). Questo metodo rileva 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 impuro il metodo Create ()?


1
Qual è il vantaggio della funzione Room Create; se stai usando un delegato perché chiamarlo prima che il client chiami "Nord"?
SH-

1
@SH Se il delegato genera un'eccezione, voglio scoprire al momento della creazione dell'oggetto Room. Non voglio che il cliente trovi l'eccezione durante l'utilizzo, ma piuttosto sulla creazione. Il metodo Create è il luogo perfetto per esporre delegati malvagi.
Rock Anthony Johnson,

3
@RockAnthonyJohnson, Sì. Cosa fai quando esegui il delegato puoi lavorare solo la prima volta o restituire una stanza diversa alla seconda chiamata? Chiamare il delegato può essere considerato / causare un effetto collaterale stesso?
SH-

4
Una funzione che chiama una funzione impura è una funzione impura, quindi se il delegato è impuro Createè anche impuro, perché lo chiama.
Idan Arye,

2
La tua Createfunzione non ti protegge dall'ottenere un'eccezione quando ottieni la proprietà. Se il tuo delegato lancia, nella vita reale è molto probabile che venga lanciato solo in determinate condizioni. È probabile che le condizioni per il lancio non siano presenti durante la costruzione, ma sono presenti quando si ottiene la proprietà.
Bart van Ingen Schenau,

Risposte:


26

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

Per renderlo una funzione pura, returnun oggetto reale che incapsula il valore atteso dalla funzione e un valore che indica una possibile condizione di errore, come un Maybeoggetto o un oggetto Unità di lavoro.


16
Ricorda, "puro" è solo una definizione. Se hai bisogno di una funzione che lancia, ma in ogni altro modo è referenzialmente trasparente, allora è quello che scrivi.
Robert Harvey,

1
In haskell almeno qualsiasi funzione può essere lanciata, ma devi essere in IO per catturare, una funzione pura che a volte getta verrebbe normalmente chiamata parziale
jk.

4
Ho avuto un'epifania a causa del tuo commento. Mentre sono intellettualmente incuriosito dalla purezza, ciò di cui ho davvero bisogno nella praticità è il determinismo e la trasparenza riverenziale. Se raggiungo la purezza, questo è solo un ulteriore vantaggio. Grazie. Cordiali saluti, la cosa che mi ha spinto in questo percorso è il fatto che Microsoft IntelliTest è efficace solo contro il codice che è deterministico.
Rock Anthony Johnson,

4
@RockAnthonyJohnson: Beh, tutto il codice dovrebbe essere deterministico, o almeno il più deterministico possibile. La trasparenza referenziale è solo un modo per ottenere quel determinismo. Alcune tecniche, come i thread, hanno la tendenza a lavorare contro quel determinismo, quindi ci sono altre tecniche come Task che migliorano le tendenze non deterministiche del modello di threading. Anche cose come generatori di numeri casuali e simulatori Monte-Carlo sono deterministici, ma in modo diverso in base alle probabilità.
Robert Harvey,

3
@zzzzBov: non considero la definizione effettiva e rigorosa di "puro" così importante. Vedi il secondo commento, sopra.
Robert Harvey,

12

Bene, sì ... e no.

Una funzione pura deve avere la Trasparenza referenziale , ovvero dovresti essere in grado di sostituire qualsiasi possibile chiamata a una funzione pura con il valore restituito, senza modificare il comportamento del programma. * La tua funzione è garantita sempre per determinati argomenti, quindi non esiste un valore di ritorno con cui sostituire la chiamata di funzione, quindi ignoriamola . Considera questo codice:

{
    var ignoreThis = func(arg);
}

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

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

Ma...

In pratica, questo conta molto poco. 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 lo fai prendi qualcosa, gestisci l'errore per ripristinare ciò che stavi facendo o in qualche modo ancora realizzarlo. Se il tuo programma non funziona correttamente perché un codice che avrebbe fallito è stato ottimizzato, stai usando le eccezioni sbagliate (a meno che non sia un codice di test e quando costruisci per le eccezioni dei test non dovrebbe essere ottimizzato).

Detto ciò...

Non gettare eccezioni dalle funzioni pure con l'intenzione che vengano catturate - c'è una buona ragione per cui i linguaggi funzionali preferiscono usare le monadi invece delle eccezioni impilabili.

Se C # avesse una Errorclasse come Java (e molte altre lingue), avrei suggerito di lanciare un Errorinvece di un Exception. Indica che l'utente della funzione ha fatto qualcosa di sbagliato (ha superato una funzione che genera) e tali cose sono consentite nelle funzioni pure. Ma C # non ha una Errorclasse e le eccezioni dell'errore di utilizzo sembrano derivare Exception. Il mio suggerimento è di lanciare un ArgumentException, chiarendo che la funzione è stata chiamata con un argomento negativo.


* Computazionalmente parlando. Una funzione di Fibonacci implementata usando l'ingenua ricorsione richiederà molto tempo per grandi numeri e potrebbe esaurire le risorse della macchina, ma queste poiché con un tempo e una memoria illimitati la funzione restituirà sempre lo stesso valore e non avrà effetti collaterali (oltre all'allocazione memoria e alterare quella memoria) - è ancora considerato puro.


1
+1 Per l'uso suggerito di ArgumentException e per la raccomandazione di non "gettare eccezioni da funzioni pure con l'intenzione che vengano catturate".
Rock Anthony Johnson,

Il tuo primo argomento (fino al "Ma ...") non mi sembra giusto. L'eccezione fa parte del contratto della funzione. Puoi, concettualmente, riscrivere qualsiasi funzione (value, exception) = func(arg)e rimuovere la possibilità di generare un'eccezione (in alcune lingue e per alcuni tipi di eccezioni devi effettivamente elencarla con la definizione, come argomenti o valore restituito). Ora sarebbe puro come prima (a condizione che ritorni sempre aka genera un'eccezione dato lo stesso argomento). Lanciare un'eccezione non è un effetto collaterale, se si utilizza questa interpretazione.
AnoE,

@AnoE Se avessi scritto in funcquel modo, il blocco che ho dato potrebbe essere stato ottimizzato, perché invece di srotolare lo stack l'eccezione generata sarebbe stata codificata ignoreThis, che ignoriamo. A differenza delle eccezioni che svolgono lo stack, l'eccezione codificata nel tipo restituito non viola la purezza - ed è per questo che molti linguaggi funzionali preferiscono usarli.
Idan Arye,

Esatto ... non avresti ignorato l'eccezione per questa trasformazione immaginata. Immagino sia tutta una questione di semantica / definizione, volevo solo sottolineare che l'esistenza o il verificarsi di eccezioni non annullano necessariamente tutte le nozioni di purezza.
AnoE,

1
@AnoE Ma il codice che ho writted sarebbe ignorare l'eccezione per questa trasformazione immaginato. func(arg)restituirebbe la tupla (<junk>, TheException()), e quindi ignoreThisconterrà la tupla (<junk>, TheException()), ma poiché non usiamo mai ignoreThisl'eccezione non avrà alcun effetto su nulla.
Idan Arye,

1

Una considerazione è che il blocco try - catch non è il problema. (Basato sui miei commenti alla domanda sopra).

Il problema principale è che la struttura a North è una chiamata di I / O .

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

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


Non sono chiaro il motivo per cui non si desidera controllare la chiamata a Sposta stanza [Controlla]? Secondo il mio commento alla domanda:

Sì. Cosa fai quando esegui il delegato puoi lavorare solo la prima volta o restituire una stanza diversa alla seconda chiamata? Chiamare il delegato può essere considerato / causare un effetto collaterale stesso?

Come ha detto Bart van Ingen Schenau sopra,

La tua funzione Crea non ti protegge dall'ottenere un'eccezione quando ottieni la proprietà. Se il tuo delegato lancia, nella vita reale è molto probabile che venga lanciato solo in determinate condizioni. È probabile che le condizioni per il lancio non siano presenti durante la costruzione, ma sono presenti quando si ottiene la proprietà.

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


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

Simile alla risposta di Robert Harvey :

Per renderlo una funzione pura, restituisci 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.

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

Dipende da questo punto:

  • Il dominio Room considera le eccezioni room come Null o Something Peggio.
  • Come notificare il codice client chiamando North su una Null / Exception Room. (Cauzione / Variabile di stato / Stato globale / Restituzione di una monade / Qualunque cosa; alcuni sono più puri di altri :)).

-1

Hmm ... Non mi sentivo bene. Penso che quello che stai cercando di fare sia assicurarti che altre persone facciano bene il loro lavoro, senza sapere cosa stanno facendo.

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

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

Dal codice, non potevo essere sicuro di cosa stesse facendo il Nord.

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

E se fosse una funzione di lunga durata? Non vorrai bloccare il tuo programma durante la creazione di un oggetto Room, cosa succede se devi creare un oggetto 1000 Room?

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

Se c'è un'altra persona che scrive al consumatore, potrebbe non conoscere la condizione "speciale" di creare un oggetto Room, che potrebbe causare esecuzione e si griderebbero 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 di avere un errore (un mesaage amichevole per un'eccezione generica), ma non sapresti quale sia l'errore (riferimento null, indice fuori limite, dividere per zero ecc.). Questa informazione è molto importante quando dobbiamo investigare.

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

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


1
Guarda di nuovo il secondo esempio di codice; Inizializzo la nuova eccezione con l'eccezione inferiore. L'intera traccia dello stack viene conservata.
Rock Anthony Johnson,

Ops. Errore mio.
user251630,

-1

Non sarò d'accordo con le altre risposte e dirò di no . Le eccezioni non fanno impurire una funzione altrimenti pura.

Qualsiasi uso di eccezioni potrebbe essere riscritto per utilizzare controlli espliciti per risultati di errore. Le eccezioni potrebbero essere considerate zucchero sintattico. Lo zucchero sintattico conveniente non impura il puro codice.


1
Il fatto di poter riscrivere l'impurità non rende pura la funzione. Haskell è una prova che l'uso di funzioni impure può essere riscritto con funzioni pure: utilizza monadi per codificare qualsiasi comportamento impuro nelle funzioni pure pure del tipo restituito. L'esistenza di monadi IO non implica che l'IO normale sia pura - perché l'esistenza di eccezioni monadi dovrebbe implicare che le eccezioni regolari siano pure?
Idan Arye,

@IdanArye: sto sostenendo che non c'è impurità in primo luogo. L'esempio di riscrittura è stato solo per dimostrarlo. L'IO normale è impuro perché provoca effetti collaterali osservabili o può restituire valori diversi con lo stesso input a causa di un input esterno. Lanciare un'eccezione non fa nessuna di queste cose.
Jacques B

Il codice libero degli effetti collaterali non è sufficiente. La trasparenza referenziale è anche un requisito per la purezza delle funzioni.
Idan Arye,

1
Il determinismo non è sufficiente per la trasparenza referenziale: gli effetti collaterali possono anche essere deterministici. Trasparenza referenziale significa che l'espressione può essere sostituita con i suoi risultati - quindi, indipendentemente da quante volte si valutano espressioni trasparenti referenziali, in quale ordine e se le si valuta o meno - il risultato dovrebbe essere lo stesso. Se un'eccezione viene determinata in modo deterministico, siamo bravi con i criteri "non importa quante volte", ma non con gli altri. Ho discusso dei criteri "se o no" nella mia risposta, (cont ...)
Idan Arye,

1
(... cont) e per quanto riguarda "in quale ordine" - se fe gsono entrambe funzioni pure, allora f(x) + g(x)dovrebbe avere lo stesso risultato indipendentemente dall'ordine che si valuta f(x)e g(x). Ma se f(x)genera Fe g(x)genera G, l'eccezione generata da questa espressione sarebbe Fse f(x)viene valutata per prima e Gseg(x) viene valutata per prima - non è così che dovrebbero comportarsi le pure funzioni! Quando l'eccezione viene codificata nel tipo restituito, quel risultato finirebbe come qualcosa di Error(F) + Error(G)indipendente dall'ordine di valutazione.
Idan Arye,
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.