Qual è il vantaggio di non avere "eccezioni di runtime", come afferma Elm?


16

Alcune lingue affermano di non avere "eccezioni di runtime" come un chiaro vantaggio rispetto ad altre lingue che li hanno.

Sono confuso sulla questione.

L'eccezione di runtime è solo uno strumento, per quanto ne so, e se usato bene:

  • puoi comunicare stati "sporchi" (lanciando dati imprevisti)
  • aggiungendo stack puoi puntare alla catena di errori
  • puoi distinguere tra disordine (ad es. restituire un valore vuoto su input non valido) e un utilizzo non sicuro che richiede attenzione da parte di uno sviluppatore (ad esempio gettando un'eccezione su input non valido)
  • puoi aggiungere dettagli al tuo errore con il messaggio di eccezione che fornisce ulteriori dettagli utili per aiutare gli sforzi di debug (teoricamente)

D'altra parte trovo davvero difficile eseguire il debug di un software che "ingoia" le eccezioni. Per esempio

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

Quindi la domanda è: qual è il forte vantaggio teorico di non avere "eccezioni di runtime"?


Esempio

https://guide.elm-lang.org/

Nessun errore di runtime in pratica. Nullo Nessun indefinito non è una funzione.


Penso che sarebbe utile fornire un esempio o due delle lingue a cui ti riferisci e / o collegamenti a tali affermazioni. Questo potrebbe essere interpretato in vari modi.
JimmyJames,

L'ultimo esempio che ho trovato è stato elm rispetto a C, C ++, C #, Java, ECMAScript, ecc. Ho aggiornato la mia domanda @JimmyJames
atoth

2
Questa è la prima volta che ne ho sentito parlare. La mia prima reazione è di chiamare BS. Nota le parole della donnola: in pratica
JimmyJames

@atot Ho intenzione di modificare il titolo della domanda per renderlo più chiaro, perché ci sono più domande non correlate che sembrano simili ad essa (come "RuntimeException" vs "Exception" in Java). Se non ti piace il nuovo titolo, sentiti libero di modificarlo di nuovo.
Andres F.

OK, volevo che fosse abbastanza generale, ma posso essere d'accordo su questo se questo aiuta, grazie per il tuo contributo @AndresF.!
atoth

Risposte:


28

Le eccezioni hanno una semantica estremamente limitante. Devono essere gestiti esattamente dove vengono lanciati, o nello stack di chiamate dirette verso l'alto, e non ci sono indicazioni per il programmatore al momento della compilazione se si dimentica di farlo.

In contrasto con Elm, dove gli errori sono codificati come risultati o Maybes , che sono entrambi valori . Ciò significa che ricevi un errore del compilatore se non gestisci l'errore. Puoi archiviarli in una variabile o anche in una raccolta per rinviarne la gestione a un momento opportuno. È possibile creare una funzione per gestire gli errori in un modo specifico dell'applicazione invece di ripetere blocchi try-catch molto simili ovunque. Puoi incatenarli in un calcolo che ha successo solo se tutte le sue parti hanno esito positivo e non devono essere stipate in un blocco try. Non sei limitato dalla sintassi integrata.

Non è niente come "ingoiare le eccezioni". Sta rendendo esplicite le condizioni di errore nel sistema dei tipi e fornendo una semantica alternativa molto più flessibile per gestirle.

Considera il seguente esempio. Puoi incollarlo in http://elm-lang.org/try se vuoi vederlo in azione.

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

Notare che String.toIntnella calculatefunzione ha la possibilità di fallire. In Java, questo ha il potenziale per generare un'eccezione di runtime. Mentre legge l'input dell'utente, ha buone possibilità di farlo. Elm invece mi costringe a gestirlo restituendo un Result, ma nota che non devo affrontarlo subito. Posso raddoppiare l'input e convertirlo in una stringa, quindi verificare l'input errato nella getDefaultfunzione. Questo posto è molto più adatto per il controllo rispetto al punto in cui si è verificato l'errore o verso l'alto nello stack di chiamate.

Il modo in cui il compilatore forza la nostra mano è anche molto più preciso delle eccezioni verificate di Java. È necessario utilizzare una funzione molto specifica come Result.withDefaultestrarre il valore desiderato. Mentre tecnicamente potresti abusare di quel tipo di meccanismo, non ha molto senso. Dal momento che puoi rinviare la decisione fino a quando non conosci un buon messaggio di default / errore da mettere, non c'è motivo di non usarlo.


8
That means you get a compiler error if you don't handle the error.- Beh, questo era il ragionamento alla base di Checked Exceptions in Java, ma sappiamo tutti quanto bene sia andato a buon fine.
Robert Harvey,

4
@RobertHarvey In un certo senso, le eccezioni verificate di Java erano la versione di questo povero. Sfortunatamente potrebbero essere "ingoiati" (come nell'esempio del PO). Inoltre non erano tipi reali, rendendoli un percorso aggiuntivo estraneo al flusso di codice. Lingue con sistemi di tipo migliori consentono di errori di codifica come valori di prima classe, che è ciò che si fa a (diciamo) Haskell con Maybe, Either, ecc sembra Elm come sta prendendo una pagina da linguaggi come ML, OCaml o Haskell.
Andres F.

3
@JimmyJames No, non ti costringono. Devi solo "gestire" l'errore quando vuoi usare il valore. Se lo faccio x = some_func(), non devo fare nulla a meno che non voglia esaminare il valore di x, nel qual caso posso verificare se ho un errore o un valore "valido"; inoltre, è un errore di tipo statico tentare di usarne uno al posto dell'altro, quindi non posso farlo. Se i tipi di Elm funzionano in modo simile ad altri linguaggi funzionali, posso effettivamente fare cose come comporre valori da funzioni diverse prima ancora di sapere se sono errori o no! Questo è tipico dei linguaggi FP.
Andres F.

6
@atoth Ma tu non hai guadagni significativi e non v'è una buona ragione (come è stato spiegato in più risposte alla tua domanda). Ti incoraggio davvero ad imparare una lingua con una sintassi simile a ML e vedrai quanto è liberatorio sbarazzarsi della sintassi simile a C (ML, tra l'altro, è stata sviluppata nei primi anni '70, il che la rende approssimativamente un contemporaneo di C). Le persone che hanno progettato questo tipo di sistemi di tipo considerano questo tipo di sintassi normale, a differenza di C :) Mentre ci sei, non farebbe male anche imparare un Lisp :)
Andres F.

6
@atoth Se vuoi prendere una cosa da tutto questo, prendi questa: assicurati sempre di non cadere in preda al Blub Paradox . Non essere irritato dalla nuova sintassi. Forse è lì a causa delle potenti funzionalità di cui non hai familiarità :)
Andres F.

10

Per comprendere questa affermazione, dobbiamo prima capire cosa ci compra un sistema di tipo statico. In sostanza, ciò che ci offre un sistema di tipo statico è una garanzia: se il tipo di programma verifica, una determinata classe di comportamenti di runtime non può verificarsi.

Sembra inquietante. Bene, un controllo di tipo è simile a un controllo di teorema. (In realtà, secondo il Curry-Howard-Isomorfismo, sono la stessa cosa.) Una cosa che è molto peculiare dei teoremi è che quando dimostrate un teorema, dimostrate esattamente ciò che dice il teorema, non di più. (Questo è ad esempio il motivo per cui, quando qualcuno dice "Ho dimostrato che questo programma è corretto", dovresti sempre chiedere "per favore, definisci" corretto ".) Lo stesso vale per i sistemi di tipi. Quando diciamo "un programma è sicuro per i tipi", ciò che intendiamo non è che non possa verificarsi alcun errore. Possiamo solo dire che gli errori che il sistema di tipi ci promette di prevenire non possono verificarsi.

Quindi, i programmi possono avere infiniti comportamenti di runtime infiniti. Di questi, infinitamente molti sono utili, ma anche infinitamente molti sono "errati" (per varie definizioni di "correttezza"). Un sistema di tipo statico ci consente di dimostrare che non può verificarsi un certo insieme finito finito di quegli infiniti comportamenti di runtime errati.

La differenza tra i diversi sistemi di tipi sta fondamentalmente in quali, quanti e comportamenti di runtime complessi possono dimostrare di non verificarsi. I sistemi di tipo debole come Java possono solo dimostrare cose molto basilari. Ad esempio, Java può dimostrare che un metodo che viene digitato come Stringreturn a non può restituire a List. Ma, per esempio, può non dimostrare che il metodo non non tornare. Inoltre, non può dimostrare che il metodo non genererà un'eccezione. E non può provare che non restituirà il torto String  - nessuno Stringsoddisferà il controllo del tipo. (E, naturalmente, anche nullin grado di soddisfare pure.) Ci sono anche cose molto semplici che Java non può provare, ed è per questo che abbiamo eccezioni come ArrayStoreException, ClassCastExceptiono ognuno di preferito, il NullPointerException.

Sistemi di tipo più potenti come quelli di Agda possono anche dimostrare cose come "restituirà la somma dei due argomenti" o "restituisce la versione ordinata della lista passata come argomento".

Ora, ciò che i progettisti di Elm intendono con l'affermazione che non hanno eccezioni di runtime è che il sistema di tipi di Elm può dimostrare l'assenza di (una parte significativa di) comportamenti di runtime che in altre lingue non possono essere dimostrati non si verificano e quindi potrebbero condurre a un comportamento errato in fase di esecuzione (che nel migliore dei casi significa un'eccezione, nel peggiore dei casi significa un arresto anomalo e, nel peggiore dei casi, nessun arresto anomalo, nessuna eccezione e solo un risultato silenziosamente sbagliato).

Quindi, essi sono non dicendo "non si procede eccezioni". Stanno dicendo "cose ​​che sarebbero eccezioni di runtime in linguaggi tipici con cui i programmatori tipici che arrivano a Elm avrebbero esperienza, sono catturati dal sistema di tipi". Ovviamente, qualcuno che viene da Idris, Agda, Guru, Epigram, Isabelle / HOL, Coq o lingue simili vedrà Elm come piuttosto debole in confronto. L'affermazione è più rivolta ai programmatori Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl, ....


5
Nota per i potenziali editori: mi dispiace molto per l'uso di doppi e anche tripli negativi. Tuttavia, li ho lasciati intenzionalmente: i sistemi di tipo garantiscono l'assenza di determinati tipi di comportamenti di runtime, ovvero garantiscono che determinate cose non si verifichino. E volevo mantenere intatta quella formulazione "dimostrare di non verificarsi", il che sfortunatamente porta a costruzioni come "non può dimostrare che un metodo non ritorni". Se riesci a trovare un modo per migliorarli, vai avanti, ma tieni a mente quanto sopra. Grazie!
Jörg W Mittag,

2
Buona risposta in generale, ma un piccolo pignolo: "un controllo di tipo è simile a un prover di teorema". In realtà, un controllo di tipo è più simile a un controllo di teorema: entrambi fanno la verifica , non la detrazione .
gardenhead

4

Elm non può garantire alcuna eccezione di runtime per lo stesso motivo per cui C non può garantire alcuna eccezione di runtime: il linguaggio non supporta il concetto di eccezioni.

Elm ha un modo per segnalare condizioni di errore in fase di esecuzione, ma questo sistema non fa eccezione, è "Risultati". Una funzione che potrebbe non riuscire restituisce un "Risultato" che contiene un valore normale o un errore. Elms è fortemente tipizzato, quindi questo è esplicito nel sistema dei tipi. Se una funzione restituisce sempre un numero intero, ha il tipo Int. Ma se restituisce un numero intero o ha esito negativo, il tipo restituito è Result Error Int. (La stringa è il messaggio di errore.) Questo ti costringe a gestire esplicitamente entrambi i casi nel sito di chiamata.

Ecco un esempio dall'introduzione (un po 'semplificato):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

La funzione toIntpotrebbe non riuscire se l'input non è analizzabile, quindi lo è il tipo restituito Result String int. Per ottenere il valore intero effettivo, è necessario "decomprimere" tramite la corrispondenza del modello, che a sua volta ti costringe a gestire entrambi i casi.

I risultati e le eccezioni fanno sostanzialmente la stessa cosa, la differenza importante sono le "impostazioni predefinite". Le eccezioni generano bolle e terminano il programma per impostazione predefinita e devi catturarle esplicitamente se vuoi gestirle. Il risultato è dall'altra parte: per impostazione predefinita, sei costretto a gestirli, quindi devi passarli rapidamente verso l'alto se vuoi che terminino il programma. È facile vedere come questo comportamento possa portare a un codice più solido.


2
@atoth Ecco un esempio. Immagina che la lingua A consenta eccezioni. Quindi ti viene data la funzione doSomeStuff(x: Int): Int. Normalmente ti aspetti che restituisca un Int, ma può anche generare un'eccezione? Senza guardare il suo codice sorgente, non puoi saperlo. Al contrario, una lingua B che codifica gli errori tramite i tipi può avere la stessa funzione dichiarata in questo modo: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(in Elm questo tipo è effettivamente chiamato Result). A differenza del primo caso, ora è immediatamente ovvio se la funzione potrebbe non funzionare e devi gestirla in modo esplicito.
Andres F.

1
Come suggerisce @RobertHarvey in un commento su un'altra risposta, ciò sembra fondamentalmente come un'eccezione verificata in Java. Quello che ho imparato lavorando con Java nei primi giorni in cui sono state verificate la maggior parte delle eccezioni è che non vuoi davvero essere costretto a scrivere codice per sempre errori nel momento in cui si verificano.
JimmyJames,

2
@JimmyJames Non è come le eccezioni controllate perché le eccezioni non si compongono, possono essere ignorate ("ingoiate") e non sono valori di prima classe :) Consiglio vivamente di imparare un linguaggio funzionale tipicamente statico per capirlo veramente. Questa non è una novità inventata da Elm: è così che programmi in lingue come ML o Haskell, ed è diversa da Java.
Andres F.

2
@AndresF. this is how you program in languages such as ML or HaskellIn Haskell, sì; ML, no. Robert Harper, un importante collaboratore di Standard ML e ricercatore nel linguaggio di programmazione, considera utili le eccezioni . I tipi di errore possono ostacolare la composizione delle funzioni nei casi in cui è possibile garantire che non si verifichi un errore. Le eccezioni hanno anche prestazioni diverse. Non paghi per le eccezioni che non vengono generate, ma paghi per controllare i valori di errore ogni volta, e le eccezioni sono un modo naturale per esprimere il backtracking in alcuni algoritmi
Doval

2
@JimmyJames Speriamo ora di vedere che le eccezioni verificate e i tipi di errore effettivi sono solo superficialmente simili. Le eccezioni controllate non si combinano con grazia, sono ingombranti da usare e non sono orientate all'espressione (e quindi puoi semplicemente "ingoiarle", come accade con Java). Le eccezioni non controllate sono meno ingombranti, motivo per cui sono la norma ovunque tranne che in Java, ma hanno maggiori probabilità di farti inciampare, e non puoi dire guardando una dichiarazione di funzione se lancia o no, rendendo il tuo programma più difficile capire.
Andres F.

2

Innanzitutto, tieni presente che il tuo esempio di "deglutizione" delle eccezioni è in generale una pratica terribile e completamente estraneo al fatto di non avere eccezioni di runtime; quando ci pensi, hai avuto un errore di runtime, ma hai scelto di nasconderlo e non fare nulla al riguardo. Ciò comporterà spesso bug difficili da capire.

Questa domanda potrebbe essere interpretata in vari modi, ma poiché hai citato Elm nei commenti, il contesto è più chiaro.

Elm è, tra le altre cose, un linguaggio di programmazione tipicamente statico . Uno dei vantaggi di questo tipo di sistemi di tipo è che molte classi di errori (sebbene non tutti) vengano rilevate dal compilatore, prima che il programma venga effettivamente utilizzato. Alcuni tipi di errori possono essere codificati in tipi (come Elm's Resulte Task), anziché essere generati come eccezioni. Questo è ciò che significano i progettisti di Elm: molti errori verranno colti al momento della compilazione anziché in "fase di esecuzione", e il compilatore ti costringerà a gestirli invece di ignorarli e sperare nel meglio. È chiaro perché questo è un vantaggio: meglio che il programmatore venga a conoscenza di un problema prima che l'utente lo faccia.

Si noti che quando non si utilizzano le eccezioni, gli errori vengono codificati in altri modi meno sorprendenti. Dalla documentazione di Elm :

Una delle garanzie di Elm è che in pratica non vedrai errori di runtime. NoRedInk utilizza Elm in produzione da circa un anno ormai, e non ne hanno ancora avuto uno! Come tutte le garanzie in Elm, questo si riduce alle scelte fondamentali di progettazione del linguaggio. In questo caso, siamo aiutati dal fatto che Elm tratta gli errori come dati. (Hai notato che qui facciamo molti dati sulle cose?)

I progettisti di Elm sono un po 'audaci nel rivendicare "nessuna eccezione al tempo di esecuzione" , anche se lo qualificano con "in pratica". Ciò che probabilmente significano è "errori meno imprevisti rispetto a quando si codificava in JavaScript".


Sto leggendo male, o stanno solo giocando a un gioco semantico? Dichiarano fuorilegge il nome "eccezione di runtime", ma poi lo sostituiscono con un meccanismo diverso che trasmette le informazioni di errore nello stack. Sembra solo cambiare l'implementazione di un'eccezione in un concetto o oggetto simile che implementa il messaggio di errore in modo diverso. Non è assolutamente sconvolgente. È come qualsiasi linguaggio tipicamente statico. Confronta il passaggio da una COM HRESULT a un'eccezione .NET. Meccanismo diverso, ma rimane comunque un'eccezione di runtime, indipendentemente da come lo chiami.
Mike Supporta Monica il

@Mike Ad essere sincero, non ho esaminato Elm in dettaglio. A giudicare dai documenti, hanno tipi Resulte Taskche sembrano molto simili a quelli più familiari Eithere Futuredi altre lingue. A differenza delle eccezioni, i valori di questi tipi possono essere combinati e ad un certo punto è necessario gestirli esplicitamente: rappresentano un valore valido o un errore? Non leggo le menti, ma questa mancanza di sorpresa per il programmatore è probabilmente ciò che i progettisti di Elm intendevano per "nessuna eccezione al tempo di esecuzione" :)
Andres F.

@Mike Sono d'accordo che non è sconvolgente. La differenza con le eccezioni di runtime è che non sono esplicite nei tipi (cioè non si può sapere, senza guardare il suo codice sorgente, se un pezzo di codice può lanciare); la codifica degli errori nei tipi è molto esplicita e impedisce al programmatore di trascurarli, portando a un codice più sicuro. Questo è fatto da molte lingue del PQ e non è in effetti nulla di nuovo.
Andres F.

1
Secondo i tuoi commenti, penso che ci sia qualcosa di più che "controlli di tipo statico" . Puoi aggiungerlo a JS usando Typescript, che è molto meno vincolante di un nuovo ecosistema "make-or-break".
atoth

1
@AndresF .: Tecnicamente parlando, la più recente funzionalità del sistema di tipi di Java è il polimorfismo parametrico, che proviene dalla fine degli anni '60. Quindi, in un certo senso, dire "moderno" quando intendi "non Java" è in qualche modo corretto.
Jörg W Mittag,

0

Elm afferma:

Nessun errore di runtime in pratica. Nullo Nessun indefinito non è una funzione.

Ma chiedi delle eccezioni di runtime . C'è una differenza

In Elm, nulla restituisce un risultato inaspettato. NON è possibile scrivere un programma valido in Elm che produca errori di runtime. Pertanto, non sono necessarie eccezioni.

Quindi, la domanda dovrebbe essere:

Qual è il vantaggio di non avere "errori di runtime"?

Se riesci a scrivere codice che non presenta mai errori di runtime, i tuoi programmi non si arresteranno mai.

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.