Forse monade vs eccezioni


Risposte:


57

L'uso Maybe(o suo cugino Eitherche funziona sostanzialmente allo stesso modo ma consente di restituire un valore arbitrario al posto di Nothing) ha uno scopo leggermente diverso dalle eccezioni. In termini Java, è come avere un'eccezione controllata piuttosto che un'eccezione di runtime. Rappresenta qualcosa di previsto che devi affrontare, piuttosto che un errore che non ti aspettavi.

Quindi una funzione come indexOfrestituirebbe un Maybevalore perché ci si aspetta la possibilità che l'elemento non sia nell'elenco. Questo è molto simile al ritorno nullda una funzione, tranne che in un modo sicuro che ti costringe ad affrontare il nullcaso. Eitherfunziona allo stesso modo tranne per il fatto che è possibile restituire informazioni associate al caso di errore, quindi in realtà è più simile a un'eccezione di Maybe.

Quindi quali sono i vantaggi dell'approccio Maybe/ Either? Per uno, è un cittadino di prima classe della lingua. Confrontiamo una funzione usando Eitherquella che genera un'eccezione. Nel caso dell'eccezione, l'unica vera risorsa è una try...catchdichiarazione. Per la Eitherfunzione, è possibile utilizzare combinatori esistenti per rendere più chiaro il controllo del flusso. Qui ci sono un paio di esempi:

Innanzitutto, supponiamo che tu voglia provare diverse funzioni che potrebbero sbagliarsi di seguito fino a quando non ne ottieni una che non funziona. Se non ne ricevi nessuno senza errori, desideri restituire un messaggio di errore speciale. Questo è in realtà un modello molto utile, ma sarebbe un dolore orribile usando try...catch. Fortunatamente, poiché Eitherè solo un valore normale, puoi utilizzare le funzioni esistenti per rendere il codice molto più chiaro:

firstThing <|> secondThing <|> throwError (SomeError "error message")

Un altro esempio è avere una funzione opzionale. Supponiamo che tu abbia diverse funzioni da eseguire, inclusa una che tenta di ottimizzare una query. In caso contrario, si desidera comunque eseguire tutto il resto. Puoi scrivere un codice simile a:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

Entrambi questi casi sono più chiari e più brevi dell'uso try..catche, soprattutto, più semantici. L'uso di una funzione come <|>o optionalrende le tue intenzioni molto più chiare rispetto all'utilizzo try...catchper gestire sempre le eccezioni.

Si noti inoltre che non è necessario sporcare il codice con righe come if a == Nothing then Nothing else ...! Il punto centrale del trattamento Maybee Eithercome monade è evitare questo. È possibile codificare la semantica di propagazione nella funzione bind in modo da ottenere gratuitamente i controlli null / error. L'unica volta che devi controllare esplicitamente è se vuoi restituire qualcosa di diverso da Nothingun dato Nothing, e anche così è facile: ci sono un sacco di funzioni di libreria standard per rendere quel codice più piacevole.

Infine, un altro vantaggio è che un Maybe/ Eithertipo è solo più semplice. Non è necessario estendere la lingua con parole chiave o strutture di controllo aggiuntive: tutto è solo una libreria. Dato che sono solo valori normali, rende il sistema dei tipi più semplice - in Java, devi differenziare i tipi (ad esempio il tipo di ritorno) e gli effetti (ad esempio le throwsdichiarazioni) dove non utilizzeresti Maybe. Si comportano anche come qualsiasi altro tipo definito dall'utente: non è necessario disporre di un codice di gestione degli errori speciale inserito nella lingua.

Un'altra vittoria è che Maybe/ Eithersono funzionali e monadi, il che significa che possono trarre vantaggio dalle funzioni di flusso di controllo delle monadi esistenti (di cui esiste un numero equo) e, in generale, giocare bene insieme ad altre monadi.

Detto questo, ci sono alcuni avvertimenti. Per uno, MaybeEithersostituire né le eccezioni non controllate . Avrai bisogno di un altro modo per gestire cose come la divisione per 0 semplicemente perché sarebbe un dolore avere ogni singola divisione restituire un Maybevalore.

Un altro problema è la restituzione di più tipi di errori (vale solo per Either). Con le eccezioni, è possibile generare qualsiasi tipo diverso di eccezioni nella stessa funzione. con Either, ottieni solo un tipo. Questo può essere superato con la sotto-digitazione o un ADT contenente tutti i diversi tipi di errori come costruttori (questo secondo approccio è quello che viene solitamente utilizzato in Haskell).

Soprattutto, preferisco l' approccio Maybe/ Eitherperché lo trovo più semplice e flessibile.


Per lo più d'accordo, ma non credo che l'esempio "opzionale" abbia senso - sembra che tu possa farlo altrettanto bene con le eccezioni: void Facoltativo (Action act) {try {act (); } catch (Exception) {}}
Erroratz

11
  1. Un'eccezione può contenere ulteriori informazioni sulla fonte di un problema. OpenFile()può lanciare FileNotFoundo NoPermissiono TooManyDescriptorsecc. A Nessuno non contiene queste informazioni.
  2. Le eccezioni possono essere utilizzate in contesti privi di valori di ritorno (ad es. Con costruttori in linguaggi che li hanno).
  3. Un'eccezione ti consente di inviare facilmente le informazioni nello stack, senza molte if None return Nonedichiarazioni di stile.
  4. La gestione delle eccezioni comporta quasi sempre un impatto sulle prestazioni maggiore rispetto alla semplice restituzione di un valore.
  5. Soprattutto, un'eccezione e una forse monade hanno scopi diversi: un'eccezione viene utilizzata per indicare un problema, mentre forse non lo è.

    "Infermiera, se c'è un paziente nella stanza 5, puoi chiedergli di aspettare?"

    • Forse monade: "Dottore, non ci sono pazienti nella stanza 5."
    • Eccezione: "Dottore, non c'è spazio 5!"

    (nota il "se" - questo significa che il dottore si aspetta una monade Maybe)


7
Non concordo con i punti 2 e 3. In particolare, 2 in realtà non rappresenta un problema nelle lingue che usano le monadi perché quelle lingue hanno convenzioni che non generano l'enigma di dover gestire i guasti durante la costruzione. Per quanto riguarda 3, le lingue con monadi hanno una sintassi appropriata per gestire le monadi in modo trasparente che non richiede un controllo esplicito (ad esempio, i Nonevalori possono essere semplicemente propagati). Il tuo punto 5 è solo giusto ... la domanda è: quali situazioni sono inequivocabilmente eccezionali? A quanto pare ... non molti .
Konrad Rudolph,

3
Per quanto riguarda (2), puoi scrivere bindin modo tale che il test Nonenon comporti comunque un sovraccarico sintattico. Un esempio molto semplice, C # sovraccarica gli Nullableoperatori in modo appropriato. Nessun controllo Nonenecessario, anche quando si utilizza il tipo. Ovviamente il controllo è ancora fatto (è sicuro per il tipo), ma dietro le quinte e non ingombra il codice. Lo stesso vale in un certo senso per la tua obiezione alla mia obiezione a (5), ma sono d'accordo che potrebbe non essere sempre applicabile.
Konrad Rudolph,

4
@Oak: per quanto riguarda (3), il punto centrale di trattare Maybecome una monade è di rendere Noneimplicita la propagazione . Ciò significa che se si desidera restituire un Nonedato None, non è necessario scrivere alcun codice speciale. L'unica volta che devi abbinare è se vuoi fare qualcosa di speciale None. Non hai mai bisogno di una if None then Nonesorta di affermazioni.
Tikhon Jelvis,

5
@Oak: è vero, ma non era proprio il mio punto. Quello che sto dicendo è che stai nullcontrollando esattamente così (es. if Nothing then Nothing) Gratuitamente perché Maybeè una monade. È codificato nella definizione di bind ( >>=) per Maybe.
Tikhon Jelvis,

2
Inoltre, per quanto riguarda (1): potresti facilmente scrivere una monade che può contenere informazioni di errore (ad es. Either) Che si comportano proprio come Maybe. Il passaggio tra i due è in realtà piuttosto semplice perché Maybeè davvero solo un caso speciale di Either. (A Haskell, potresti pensare a Maybecome Either ().)
Tikhon Jelvis

6

"Forse" non sostituisce le eccezioni. Le eccezioni sono pensate per essere utilizzate in casi eccezionali (ad esempio: l'apertura di una connessione db e il server db non è presente sebbene dovrebbe esserlo). "Forse" è per modellare una situazione in cui potresti avere o meno un valore valido; supponiamo che tu stia ottenendo un valore da un dizionario per una chiave: potrebbe essere lì o potrebbe non esserlo - non c'è nulla di "eccezionale" in nessuno di questi risultati.


2
In realtà, ho un bel po 'di codice che coinvolge dizionari in cui una chiave mancante significa che le cose sono andate terribilmente storto ad un certo punto, molto probabilmente un bug nel mio codice o nel codice che ha convertito l'input in qualunque cosa venga usata in quel punto.

3
@delnan: non sto dicendo che un valore mancante non possa mai essere un segno di uno stato eccezionale - semplicemente non deve esserlo. Forse modella davvero una situazione in cui una variabile può o meno avere un valore valido - pensa a tipi nullable in C #.
Nemanja Trifunovic,

6

Secondo la risposta di Tikhon, ma penso che ci sia un punto pratico molto importante che manca a tutti:

  1. I meccanismi di gestione delle eccezioni, almeno nei linguaggi tradizionali, sono intimamente accoppiati ai singoli thread.
  2. Il Eithermeccanismo non è affatto accoppiato ai thread.

Quindi qualcosa che vediamo oggi nella vita reale è che molte soluzioni di programmazione asincrona stanno adottando una variante dello Eitherstile di gestione degli errori. Considera le promesse di Javascript , come dettagliato in uno di questi link:

Il concetto di promesse ti consente di scrivere codice asincrono come questo (preso dall'ultimo link):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

Fondamentalmente, una promessa è un oggetto che:

  1. Rappresenta il risultato di un calcolo asincrono, che potrebbe non essere stato ancora completato;
  2. Consente di concatenare ulteriori operazioni da eseguire sul suo risultato, che verrà attivato quando tale risultato è disponibile e i cui risultati a loro volta sono disponibili come promesse;
  3. Consente di collegare un gestore errori che verrà richiamato se il calcolo della promessa fallisce. Se non è presente alcun gestore, l'errore viene propagato ai gestori successivi nella catena.

Fondamentalmente, poiché il supporto delle eccezioni nativo del linguaggio non funziona quando il calcolo avviene su più thread, un'implementazione promettente deve fornire un meccanismo di gestione degli errori, e questi si rivelano essere monadi simili ai tipi Maybe/ di Haskell Either.


Non ha nulla da fare con i thread. JavaScript nel browser viene sempre eseguito in un thread non su più thread. Ma non puoi ancora usare l'eccezione perché non sai quando la tua funzione verrà chiamata in futuro. Asincrono non significa automaticamente un coinvolgimento dei thread. Questo è anche il motivo per cui non puoi lavorare con le eccezioni. È possibile recuperare un'eccezione solo se si chiama una funzione e questa viene immediatamente eseguita. Ma lo scopo di Asincrono è quello di funzionare in futuro, spesso quando qualcosa di finito è finito, e non immediatamente. Ecco perché non puoi usare le eccezioni lì.
David Raab,

1

Il sistema di tipi Haskell richiederà all'utente di riconoscere la possibilità di a Nothing, mentre i linguaggi di programmazione spesso non richiedono che venga rilevata un'eccezione. Ciò significa che sapremo, al momento della compilazione, che l'utente ha verificato la presenza di un errore.


1
Ci sono eccezioni verificate in Java. E la gente spesso li trattava anche male.
Vladimir,

1
@ DairT'arg ButJava richiede controlli per errori IO (come esempio), ma non per NPE. In Haskell è il contrario: viene verificato l'equivalente null (Forse) ma gli errori IO semplicemente generano (non spuntate) eccezioni. Non sono sicuro di quanta differenza faccia, ma non sento Haskeller lamentarsi di Forse e penso che molte persone vorrebbero che gli NPE venissero controllati.

1
@delnan Le persone vogliono che NPE venga controllato? Devono essere pazzi. E intendo quello. Rendere NPE controllato ha senso solo se hai un modo per evitarlo in primo luogo (avendo riferimenti non annullabili, che Java non ha).
Konrad Rudolph,

5
@KonradRudolph Sì, probabilmente le persone non vogliono che venga controllato nel senso che dovranno aggiungere un throws NPEa ogni singola firma e catch(...) {throw ...} a ogni singolo corpo del metodo. Ma credo che ci sia un mercato per il check-in nello stesso senso di Forse: il nullability è facoltativo e monitorato nel sistema dei tipi.

1
@delnan Ah, allora sono d'accordo.
Konrad Rudolph,

0

La monade forse è fondamentalmente la stessa dell'uso della maggior parte del linguaggio tradizionale del controllo "null significa errore" (tranne per il fatto che richiede il controllo null) e presenta in gran parte gli stessi vantaggi e svantaggi.


8
Bene, non ha gli stessi svantaggi poiché può essere controllato staticamente se usato correttamente. Non c'è equivalente di un'eccezione puntatore null quando si usano forse monadi (di nuovo, supponendo che siano usati correttamente).
Konrad Rudolph,

Infatti. Come pensi che dovrebbe essere chiarito?
Telastyn,

@Konrad Questo perché, al fine di utilizzare il valore (possibilmente) in Forse, è necessario verificare la presenza di None (anche se ovviamente c'è la possibilità di automatizzare molto di quel controllo). Altre lingue mantengono quel genere di cose manuali (principalmente per motivi filosofici, credo).
Donal Fellows,

2
@Donal Sì, ma nota che la maggior parte delle lingue che implementano le monadi forniscono lo zucchero sintattico appropriato in modo che questo controllo possa essere spesso nascosto completamente. Ad esempio, puoi semplicemente aggiungere due Maybenumeri scrivendo a + b senza la necessità di verificare Nonee il risultato è ancora una volta un valore opzionale.
Konrad Rudolph,

2
Il confronto con tipi nullable vale per il Maybe tipo , ma l'uso Maybecome monade aggiunge zucchero di sintassi che consente di esprimere la logica null-ish in modo molto più elegante.
tdammers,

0

La gestione delle eccezioni può essere una vera seccatura per il factoring e il testing. So che Python fornisce una bella sintassi "con" che ti permette di intercettare le eccezioni senza il rigido blocco "prova ... cattura". Ma in Java, ad esempio, provare a catturare i blocchi sono grandi, piatti, verbosi o estremamente dettagliati e difficili da spezzare. Inoltre, Java aggiunge tutto il rumore attorno alle eccezioni verificate e non selezionate.

Se, invece, la tua monade rileva le eccezioni e le tratta come una proprietà dello spazio monadico (invece di qualche anomalia di elaborazione), allora sei libero di mescolare e abbinare le funzioni che leghi in quello spazio indipendentemente da ciò che lanciano o catturano.

Se, meglio ancora, la tua monade impedisce condizioni in cui potrebbero verificarsi eccezioni (come spingere un segno di spunta nullo in Forse), allora anche meglio. se ... allora è molto, molto più facile da valutare e testare che provare ... catturare.

Da quello che ho visto Go sta adottando un approccio simile specificando che ogni funzione ritorna (risposta, errore). È un po 'come "sollevare" la funzione in uno spazio monade in cui il tipo di risposta principale è decorato con un'indicazione di errore ed efficacemente lanciare e catturare eccezioni.

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.