Come gestire la divisione per zero in una lingua che non supporta le eccezioni?

61

Sono nel mezzo dello sviluppo di un nuovo linguaggio di programmazione per risolvere alcuni requisiti aziendali e questo linguaggio è rivolto agli utenti inesperti. Quindi non esiste alcun supporto per la gestione delle eccezioni nella lingua e non mi aspetto che li usino anche se l'ho aggiunto.

Ho raggiunto il punto in cui devo implementare l'operatore divide e mi chiedo come gestire al meglio un errore di divisione per zero?

Sembra che abbia solo tre possibili modi per gestire questo caso.

  1. Ignora l'errore e produce 0 come risultato. Registrazione di un avviso, se possibile.
  2. Aggiungi NaN come valore possibile per i numeri, ma ciò solleva domande su come gestire i valori di NaN in altre aree della lingua.
  3. Termina l'esecuzione del programma e segnala all'utente un grave errore.

L'opzione n. 1 sembra l'unica soluzione ragionevole. L'opzione n. 3 non è pratica in quanto questa lingua verrà utilizzata per eseguire la logica come cron notturno.

Quali sono le mie alternative alla gestione di un errore di divisione per zero e quali sono i rischi associati all'opzione n. 1.

    
posta cgTag 18.08.2013 - 13:45
fonte

16 risposte

98

Consiglio vivamente contro il n. 1, perché ignorare gli errori è un anti-modello pericoloso. Può portare a bug difficili da analizzare. Impostare il risultato di una divisione da zero a 0 non ha alcun senso, e l'esecuzione continua del programma con un valore assurdo causerà problemi. Specialmente quando il programma è in esecuzione senza sorveglianza. Quando l'interprete del programma nota che c'è un errore nel programma (e una divisione per zero è quasi sempre un errore di progettazione), abortire e mantenere tutto così com'è di solito è preferibile a riempire il tuo database di spazzatura.

Inoltre, difficilmente riuscirai a seguire con attenzione questo schema. Prima o poi ti imbatterai in situazioni di errore che non possono essere ignorate (come l'esaurimento della memoria o l'overflow dello stack) e dovrai comunque implementare un modo per terminare il programma.

L'opzione # 2 (usando NaN) sarebbe un po 'di lavoro, ma non tanto quanto si potrebbe pensare. Come gestire NaN in diversi calcoli è ben documentato nello standard IEEE 754, quindi è probabile che tu faccia solo ciò che fa il linguaggio in cui è scritto il tuo interprete.

A proposito: creare un linguaggio di programmazione utilizzabile da non programmatori è qualcosa che abbiamo cercato di fare dal 1964 (Dartmouth BASIC). Finora, non ci siamo riusciti. Ma buona fortuna comunque.

    
risposta data 18.08.2013 - 14:48
fonte
33

1 - Ignore the error and produce 0 as the result. Logging a warning if possible.

Questa non è una buona idea. Affatto. Le persone inizieranno a dipendere da esso e se dovessi risolverlo, spezzerai molto codice.

2 - Add NaN as a possible value for numbers, but that raises questions about how to handle NaN values in other areas of the language.

Dovresti gestire il NaN come fanno i runtime di altri linguaggi: qualsiasi ulteriore calcolo produce anche NaN e ogni confronto (anche NaN == NaN) produce false.

Penso che sia accettabile, ma non necessariamente nuovo amichevole.

3 - Terminate the execution of the program and report to the user a severe error occurred.

Questa è la soluzione migliore, credo. Con queste informazioni in mano gli utenti dovrebbero essere in grado di gestire 0. Dovresti fornire un ambiente di test, specialmente se è destinato a essere eseguito una volta a notte.

C'è anche una quarta opzione. Rendi la divisione un'operazione ternaria. Ognuno di questi due funzionerà:

  • div (numeratore, denumeratore, alternative_result)
  • div (numeratore, denumeratore, alternative_denumerator)
risposta data 18.08.2013 - 15:43
fonte
20

Terminare l'applicazione in esecuzione con estremo pregiudizio. (Fornendo informazioni debug adeguate)

Quindi istruisci i tuoi utenti a identificare e gestire le condizioni in cui il divisore potrebbe essere zero (valori inseriti dall'utente, ecc.)

    
risposta data 18.08.2013 - 15:23
fonte
12

In Haskell (e simili in Scala), invece di lanciare eccezioni (o restituire riferimenti null) è possibile utilizzare i tipi di wrapper Maybe e Either . Con Maybe l'utente ha la possibilità di verificare se il valore che ha ottenuto è "vuoto", o potrebbe fornire un valore predefinito quando "unwrapping". Either è simile, ma può essere utilizzato restituisce un oggetto (ad esempio una stringa di errore) che descrive il problema se ce n'è uno.

    
risposta data 18.08.2013 - 18:34
fonte
12

Altre risposte hanno già considerato i meriti relativi delle tue idee. Ne propongo un altro: usa l'analisi di base del flusso per determinare se una variabile può essere zero. Quindi puoi semplicemente disabilitare la divisione per variabili potenzialmente zero.

x = ...
y = ...

if y ≠ 0:
  return x / y    // In this block, y is known to be nonzero.
else:
  return x / y    // This, however, is a compile-time error.

In alternativa, disponi di una funzione di assert intelligente che stabilisce gli invarianti:

x = ...
require x ≠ 0, "Unexpected zero in calculation"
// For the remainder of this scope, x is known to be nonzero.

Ciò equivale a lanciare un errore di runtime, che supera le operazioni non definite interamente, ma ha il vantaggio che il percorso del codice non deve essere nemmeno colpito per il potenziale errore da esporre. Può essere molto simile al normale controllo ortografico, valutando tutti i rami di un programma con ambienti di digitazione annidati per il tracciamento e la verifica degli invarianti:

x = ...           // env1 = { x :: int }
y = ...           // env2 = env1 + { y :: int }
if y ≠ 0:         // env3 = env2 + { y ≠ 0 }
  return x / y    // (/) :: (int, int ≠ 0) → int
else:             // env4 = env2 + { y = 0 }
  ...
...               // env5 = env2

Inoltre, si estende in modo naturale all'intervallo e al controllo null , se la tua lingua ha tali caratteristiche.

    
risposta data 18.08.2013 - 23:23
fonte
10

Il numero 1 (inserire zero non discutibile) è sempre negativo. La scelta tra # 2 (propagare NaN) e # 3 (uccidere il processo) dipende dal contesto e idealmente dovrebbe essere un'impostazione globale, come in Numpy.

Se stai facendo un grande calcolo integrato, propagare NaN è una cattiva idea perché alla fine si diffonderà e infetterà il tuo intero calcolo --- quando guarderai i risultati al mattino e vedrai che sono tutti NaN , dovresti buttare via i risultati e ricominciare comunque. Sarebbe stato meglio se il programma fosse terminato, hai ricevuto una chiamata nel bel mezzo della notte e l'hai risolto --- in termini di numero di ore sprecate, almeno.

Se stai facendo molti piccoli calcoli, in gran parte indipendenti (come il ridimensionamento della mappa o calcoli imbarazzanti paralleli), e puoi tollerare che una percentuale di essi sia inutilizzabile a causa dei NaN, probabilmente è l'opzione migliore. Terminare il programma e non fare il 99% che sarebbe buono e utile a causa dell'1% che è malformato e dividere per zero potrebbe essere un errore.

Un'altra opzione, relativa ai NaN: la stessa specifica in virgola mobile IEEE definisce Inf e -Inf, e questi sono propagati in modo diverso rispetto a NaN. Ad esempio, sono abbastanza sicuro che Inf > qualsiasi numero e -Inf < qualsiasi numero, che sarebbe quello che volevi se la tua divisione per zero fosse successo perché lo zero doveva essere un numero limitato. Se gli input sono arrotondati e soffrono di errori di misurazione (come le misurazioni fisiche eseguite a mano), la differenza tra due grandi quantità può risultare zero. Senza la divisione per zero, avresti ottenuto un grande numero e forse non ti importa di quanto sia grande. In tal caso, In e -Inf sono risultati validamente validi.

Può anche essere formalmente corretto --- solo dire che stai lavorando nei real realistici.

    
risposta data 18.08.2013 - 22:33
fonte
8

3. Terminate the execution of the program and report to the user a severe error occurred.

[This option] is not practical…

Ovviamente è pratico: è responsabilità dei programmatori scrivere un programma che abbia davvero un senso. Dividere per 0 non ha alcun senso. Pertanto, se il programmatore esegue una divisione, è anche sua responsabilità verificare in anticipo che il divisore non sia uguale a 0. Se il programmatore non riesce a eseguire il controllo di convalida, allora lui / lui dovresti realizzare questo errore il prima possibile, e i risultati di calcolo denormalizzati (NaN) o errati (0) semplicemente non ti aiuteranno in questo senso.

L'opzione 3 sembra essere quella che ti avrei raccomandato, btw, per essere la più semplice, onesta e matematicamente corretta.

    
risposta data 19.08.2013 - 11:11
fonte
4

Mi sembra una cattiva idea eseguire compiti importanti (es. "cron notturno") in un ambiente in cui gli errori vengono ignorati. È una pessima idea farlo una caratteristica. Questo esclude le opzioni 1 e 2.

L'opzione 3 è l'unica soluzione accettabile. Le eccezioni non devono essere parte della lingua, ma fanno parte della realtà. Il tuo messaggio di terminazione dovrebbe essere il più specifico e informativo possibile sull'errore.

    
risposta data 21.08.2013 - 21:08
fonte
3

IEEE 754 ha in realtà una soluzione ben definita per il tuo problema. Gestione delle eccezioni senza utilizzare exceptions link

1/0  = Inf
-1/0 = -Inf
0/0  = NaN

in questo modo tutte le tue operazioni hanno un significato matematico.

\ lim_ {x \ a 0} 1 / x = Inf

Secondo me, IEEE 754 ha più senso dal momento che garantisce che i calcoli siano corretti come se fossero su un computer e che tu sia anche coerente con il comportamento di altri linguaggi di programmazione.

L'unico problema che si presenta è che Inf e NaN contamineranno i risultati e gli utenti non sapranno esattamente da dove proviene il problema. Dai un'occhiata a una lingua come Julia che funziona abbastanza bene.

julia> 1/0
Inf

julia> -1/0
-Inf

julia> 0/0
NaN

julia> a = [1,1,1] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 Inf

julia> sum(a)
Inf

julia> a = [1,1,0] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 NaN

julia> sum(a)
NaN

L'errore di divisione viene propagato correttamente attraverso le operazioni matematiche ma alla fine l'utente non sa necessariamente da quale operazione deriva l'errore.

edit: Non ho visto la seconda parte della risposta di Jim Pivarski che è fondamentalmente ciò che sto dicendo sopra. Il mio male.

    
risposta data 13.07.2014 - 09:29
fonte
2

Penso che il problema sia "rivolto agli utenti inesperti. - > Quindi non c'è supporto per ..."

Perché pensi che la gestione delle eccezioni sia problematica per gli utenti inesperti?

Cosa c'è di peggio? Hai una funzione "difficile" o non hai idea del perché sia successo qualcosa? Cosa potrebbe confondere di più? Un crash con un core dump o "Fatal error: Divide by Zero"?

Invece, ritengo che sia preferibile cercare di ottenere GRANDI errori nei messaggi. Quindi, invece: "Calcolo errato, Dividi 0/0" (es .: Mostra sempre i DATI che causano il problema, non solo il tipo di problema). Guarda come PostgreSql commette gli errori dei messaggi, che sono ottimi IMHO.

Tuttavia, puoi esaminare altri modi per lavorare con eccezioni come:

link

Ho anche sognato di creare una lingua, e in questo caso penso che mix a Maybe / Optional con le normali Exceptions potrebbe essere il migliore:

def openFile(fileName): File | Exception
    if not(File.Exist(fileName)):
        raise FileNotExist(fileName)
    else:
        return File.Open()

#This cause a exception:

theFile = openFile('not exist')

# But this, not:

theFile | err = openFile('not exist')
    
risposta data 13.07.2014 - 06:45
fonte
1

A mio avviso, la tua lingua dovrebbe fornire un meccanismo generico per rilevare e gestire gli errori. Gli errori di programmazione dovrebbero essere rilevati al momento della compilazione (o il prima possibile) e dovrebbero normalmente portare alla conclusione del programma. Gli errori derivanti da dati imprevisti o errati o da condizioni esterne impreviste devono essere rilevati e resi disponibili per l'azione appropriata, ma consentire al programma di continuare quando possibile.

Le azioni plausibili includono (a) terminare (b) richiedere all'utente un'azione (c) registrare l'errore (d) sostituire un valore corretto (e) impostare un indicatore da testare nel codice (f) richiamare una gestione degli errori routine. Quali di questi rendi disponibili e in che modo sono le scelte che devi fare.

Dalla mia esperienza, errori di dati comuni come conversioni errate, divisione per zero, overflow e valore fuori intervallo sono benigni e dovrebbero essere gestiti per default sostituendo un valore diverso e impostando un flag di errore. Il (non programmatore) che utilizza questa lingua vedrà i dati difettosi e capirà rapidamente la necessità di controllare gli errori e gestirli.

[Per un esempio, considera un foglio di calcolo di Excel. Excel non termina il tuo foglio di calcolo perché un numero è in overflow o quant'altro. La cella assume uno strano valore e vai a scoprire perché e risolvilo.]

Quindi, per rispondere alla tua domanda: non dovresti certo terminare. Potresti sostituire NaN ma non dovresti renderlo visibile, ma assicurati che il calcolo sia completo e generi uno strano valore alto. E imposta un flag di errore in modo che gli utenti che ne hanno bisogno possano determinare che si è verificato un errore.

Divulgazione: ho creato proprio questa implementazione linguistica (Powerflex) e ho affrontato esattamente questo problema (e molti altri) negli anni '80. Negli ultimi 20 anni c'è stato poco o nessun progresso nelle lingue per i non programmatori, e attirerai un sacco di critiche per aver provato, ma spero davvero che tu ci riesca.

    
risposta data 12.07.2014 - 13:18
fonte
1

SQL, facilmente il linguaggio più utilizzato dai non programmatori, fa # 3, per quello che vale. Nella mia esperienza osservando e assistendo i non programmatori che scrivono SQL, questo comportamento è generalmente ben compreso e facilmente compensato (con una dichiarazione di un caso o simili). È utile che il messaggio di errore che si tende a essere abbastanza diretto, ad es. in Postgres 9 ottieni "ERRORE: divisione per zero".

    
risposta data 13.07.2014 - 06:28
fonte
1

Mi è piaciuto l'operatore ternario in cui fornisci un valore alternativo nel caso in cui il denumeratore sia 0.

Un'altra idea che non ho visto è quella di produrre un valore "non valido" generale. Un generale "questa variabile non ha un valore perché il programma ha fatto qualcosa di male", che porta una traccia di stack completa con se stessa. Quindi, se si utilizza mai quel valore, il risultato è nuovamente non valido, con la nuova operazione tentata in cima (cioè se il valore non valido appare mai in un'espressione, l'intera espressione diventa non valida e non viene tentata alcuna chiamata di funzione; essere operatori booleani - vero o non valido è vero e falso e non valido è falso - potrebbero esserci anche altre eccezioni). Una volta che quel valore non è più referenziato da nessuna parte, si registra una descrizione lunga e bella dell'intera catena in cui le cose sono andate storte e si continua come al solito. Forse invia la traccia via email al lead del progetto o qualcosa del genere.

In pratica qualcosa come la monade Forse. Funzionerà con qualsiasi altra cosa che possa fallire, e puoi consentire alle persone di costruire i propri invalidi. E il programma continuerà a funzionare fintanto che l'errore non è troppo profondo, che è ciò che è veramente voluto qui, penso.

    
risposta data 13.07.2014 - 09:05
fonte
1

Ci sono due ragioni fondamentali per una divisione per zero.

  1. In un modello preciso (come gli interi), si ottiene una divisione per zero di DBZ perché l'input è sbagliato. Questo è il tipo di DBZ a cui la maggior parte di noi pensa.
  2. In un modello non preciso (come il pt mobile), potresti ottenere un DBZ a causa dell'arrotondamento, anche se l'input è valido. Questo è ciò a cui normalmente non pensiamo.

Per 1. devi comunicare agli utenti che hanno commesso un errore perché sono l'unico responsabile e sono quelli che sanno meglio come rimediare alla situazione.

Per 2. Questo non è un errore dell'utente, puoi puntare il dito all'algoritmo, all'implementazione hardware, ecc. ma questo non è un errore dell'utente quindi non dovresti chiudere il programma o lanciare un'eccezione (se permesso che non è in questo caso ). Quindi una soluzione ragionevole è continuare le operazioni in un modo ragionevole.

Posso vedere la persona che fa questa domanda per il caso 1. Quindi devi comunicare all'utente. Utilizzando qualsiasi standard in virgola mobile, Inf, -Inf, Nan, IEEE non si adatta a questa situazione. Strategia fondamentalmente sbagliata.

    
risposta data 13.07.2014 - 10:11
fonte
0

Non consentire nella lingua. Vale a dire, non consentire la divisione per un numero fino a quando non è provabilmente zero, di solito provandolo prima. Vale a dire.

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero
    
risposta data 15.11.2013 - 17:52
fonte
0

Mentre stai scrivendo un linguaggio di programmazione, dovresti approfittare del fatto e rendere obbligatorio includere un'azione per lo stato di zero.  a < = n / c: 0 div-per-zero-action

So che quello che ho appena suggerito è essenzialmente l'aggiunta di un "goto" al tuo PL.

    
risposta data 13.07.2014 - 21:02
fonte

Leggi altre domande sui tag