Il modo più pulito per segnalare errori in Haskell


22

Sto lavorando per imparare Haskell e ho scoperto tre diversi modi di gestire gli errori nelle funzioni che scrivo:

  1. Posso semplicemente scrivere error "Some error message.", il che genera un'eccezione.
  2. Posso avere la mia funzione di ritorno Maybe SomeType, dove posso o meno essere in grado di restituire ciò che vorrei restituire.
  3. Posso avere la mia funzione di ritorno Either String SomeType, dove posso restituire un messaggio di errore o ciò che mi è stato chiesto di restituire in primo luogo.

La mia domanda è: quale metodo di gestione degli errori dovrei usare e perché? Forse dovrei metodi diversi, a seconda del contesto?

La mia attuale comprensione è:

  • È "difficile" trattare le eccezioni nel codice puramente funzionale, e in Haskell si vuole mantenere le cose il più puramente funzionali possibile.
  • Il ritorno Maybe SomeTypeè la cosa giusta da fare se la funzione fallisce o ha esito positivo (cioè, non ci sono modi diversi in cui può fallire ).
  • Il ritorno Either String SomeTypeè la cosa giusta da fare se una funzione può fallire in uno dei vari modi.

Risposte:


32

Bene, prima regola della gestione degli errori in Haskell: non usare maierror .

È semplicemente terribile in ogni modo. Esiste puramente come un atto della storia e il fatto che Prelude lo usi è terribile. Non usarlo.

L'unico momento immaginabile che potresti usare è quando qualcosa è così internamente terribile che qualcosa deve essere sbagliato nel tessuto stesso della realtà, rendendo così discutibile il risultato del tuo programma.

Ora la questione diventa Maybevs Either. Maybeè adatto a qualcosa del genere head, che può o non può restituire un valore ma c'è solo una possibile ragione per fallire. Nothingsta dicendo qualcosa del tipo "si è rotto e sai già perché". Alcuni direbbero che indica una funzione parziale.

La forma più efficace di gestione degli errori è Either+ un errore ADT.

Ad esempio in uno dei miei compilatori di hobby, ho qualcosa di simile

data CompilerError = ParserError ParserError
                   | TCError TCError
                   ...
                   | ImpossibleError String

data ParserError = ParserError (Int, Int) String
data TCError = CouldntUnify Ty Ty
             | MissingDefinition Name
             | InfiniteType Ty
             ...

type ErrorM m = ExceptT CompilerError m -- from MTL

Ora definisco un gruppo di tipi di errore, annidandoli in modo da avere un glorioso errore di livello superiore. Questo può essere un errore da qualsiasi fase della compilazione o un ImpossibleError, che indica un bug del compilatore.

Ognuno di questi tipi di errore cerca di conservare il maggior numero di informazioni il più a lungo possibile per una stampa carina o altre analisi. Ancora più importante, non avendo una stringa posso verificare che l'esecuzione di un programma non tipizzato tramite il controllo del tipo genera effettivamente un errore di unificazione! Una volta che qualcosa è a String, è andato per sempre e tutte le informazioni in esso contenute sono opache per il compilatore / test, quindi Either Stringnon è neanche eccezionale.

Alla fine ho inserito questo tipo in ExceptTun nuovo trasformatore di monade di MTL. Questo è essenzialmente EitherTe viene fornito con una bella serie di funzioni per lanciare e catturare errori in modo puro e piacevole.

Infine, vale la pena ricordare che Haskell ha i meccanismi per supportare la gestione delle eccezioni come fanno altre lingue, tranne per il fatto che la cattura di un'eccezione vive IO. Conosco alcune persone che amano usarle per IOapplicazioni pesanti in cui tutto potrebbe potenzialmente fallire, ma così raramente che non gli piace pensarci. Se usi queste impure eccezioni o semplicemente ExceptT Error IOè una questione di gusti. Personalmente scelgo ExceptTperché mi piace essere ricordato della possibilità di fallimento.


In sintesi,

  • Maybe - Posso fallire in un modo ovvio
  • Either CustomType - Posso fallire e ti dirò cosa è successo
  • IO+ eccezioni - a volte fallisco. Controlla i miei documenti per vedere cosa lancio quando lo faccio
  • error - Ti odio anche tu utente

Questa risposta mi chiarisce molto. Mi stavo ripensando in base al fatto che funzioni integrate sembrano heado lastsembrano usare error, quindi mi chiedevo se quello fosse effettivamente un buon modo di fare le cose e mi mancava qualcosa. Questo risponde a questa domanda. :)
CmdrMoozy l'

5
@CmdrMoozy Sono felice di aiutare :) È davvero un peccato che il preludio abbia delle cattive pratiche. Tale è il modo di eredità: /
Daniel Gratzer l'

3
+1 Il tuo riepilogo è fantastico
recursion.ninja

2

Non separerei 2 e 3 in base a quanti modi in cui qualcosa può fallire. Non appena pensi che ci sia un solo modo possibile, un altro mostrerà la sua faccia. La metrica dovrebbe invece essere "i miei chiamanti si preoccupano perché qualcosa non è riuscito? Possono davvero fare qualcosa al riguardo?".

Oltre a ciò, non mi è immediatamente chiaro che Either String SomeTypepuò produrre una condizione di errore. Vorrei creare un semplice tipo di dati algebrico con le condizioni con un nome più descrittivo.

Il tuo utilizzo dipende dalla natura del problema che affronti e dai modi di dire del pacchetto software con cui stai lavorando. Anche se tenderei ad evitare il n. 1.


In realtà, l'utilizzo Eitherper errori è un modello molto noto in Haskell.
Rufflewind,

1
@rufflewind - Eithernon è la parte poco chiara, l'uso di Stringcome un errore piuttosto che una stringa effettiva.
Telastyn,

Ah, ho letto male le tue intenzioni.
Rufflewind
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.