Qual è il vantaggio di avere "nessuna eccezione di runtime", come sostiene Elm?

14

Alcune lingue sostengono di avere "nessuna eccezione di runtime" come un chiaro vantaggio rispetto ad altre lingue che le hanno.

Sono confuso sull'argomento.

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

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

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

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

Quindi la domanda è: qual è il strong vantaggio teorico di avere "nessuna eccezione di runtime"?

Esempio

link

No runtime errors in practice. No null. No undefined is not a function.

    
posta atoth 01.12.2016 - 17:43
fonte

5 risposte

26

Le eccezioni hanno una semantica estremamente limitante. Devono essere gestiti esattamente nel punto in cui vengono lanciati, o nella pila di chiamate dirette verso l'alto, e non c'è alcuna indicazione al programmatore in fase di compilazione se ti dimentichi di farlo.

Confrontalo con Elm dove gli errori sono codificati come Risultati o Maybes , che sono entrambi valori . Ciò significa che si ottiene un errore del compilatore se non si gestisce l'errore. È possibile memorizzarli in una variabile o anche in una raccolta per rinviare la loro gestione a un orario conveniente. È possibile creare una funzione per gestire gli errori in un modo specifico dell'applicazione invece di ripetere blocchi try-catch molto simili dappertutto. Puoi incatenarli in un calcolo che riesce solo se tutte le sue parti hanno successo, e non devono essere stipati in un blocco try. Non sei limitato dalla sintassi integrata.

Questo non è niente come "deglutire le eccezioni". Sta rendendo esplicite le condizioni di errore nel sistema di tipi e fornendo semantica alternativa molto più flessibile per gestirle.

Considera il seguente esempio. Puoi incollarlo nel link se desideri 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")
    ]

Nota che String.toInt nella funzione calculate ha la possibilità di fallire. In Java, questo ha il potenziale per lanciare 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 noto che non devo occuparmene subito. Posso raddoppiare l'input e convertirlo in una stringa, quindi controllare l'input errato nella funzione getDefault . 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 impone la nostra mano è anche molto più fine delle eccezioni controllate di Java. È necessario utilizzare una funzione molto specifica come Result.withDefault per estrarre il valore desiderato. Mentre tecnicamente potresti abusare di quel tipo di meccanismo, non ha molto senso. Dal momento che è possibile rinviare la decisione fino a quando non si conosce un buon messaggio predefinito / errore da inserire, non c'è motivo di non usarlo.

    
risposta data 01.12.2016 - 20:45
fonte
10

Per comprendere questa affermazione, dobbiamo prima capire che cosa un sistema di tipo statico ci compra. In sostanza, ciò che un sistema di tipo statico ci dà è una garanzia: iff i controlli del tipo di programma, una certa classe di comportamenti di runtime non può verificarsi.

Sembra strano. Bene, un controllo di tipo è simile a un correttore di teoremi. (In realtà, secondo l'Isomorphism di Curry-Howard, sono la stessa cosa.) Una cosa che è molto peculiare dei teoremi è che quando si dimostra un teorema, si dimostra esattamente quello che dice il teorema, non di più. (Questo è per esempio, perché, quando qualcuno dice "Ho provato questo programma corretto", dovresti sempre chiedere "per favore definisci 'corretto'".) Lo stesso vale per i sistemi di tipi. Quando diciamo "un programma è sicuro dal tipo", ciò che intendiamo è non che non si può verificare alcun errore possibile. Possiamo solo dire che gli errori che il sistema di tipi ci promette di impedire non possono verificarsi.

Quindi, i programmi possono avere infiniti comportamenti di runtime diversi. Di quelli, 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 determinato insieme limitato di infiniti comportamenti runtime non corretti.

La differenza tra i diversi sistemi di tipi è fondamentalmente in cui, quanti e quanto complessi comportamenti di runtime possono dimostrare di non verificarsi. I sistemi di tipo debole come quelli di Java possono solo provare cose molto semplici. Ad esempio, Java può dimostrare che un metodo digitato come restituire String non può restituire List . Ad esempio, può non dimostrare che il metodo non verrà restituito. Inoltre, non può dimostrare che il metodo non genererà un'eccezione. E non può dimostrare che non restituirà sbagliato String - qualsiasi String soddisferà il controllo di tipo. (E, naturalmente, anche null lo soddisferà.) Ci sono anche cose molto semplici che Java non può dimostrare, ed è per questo che abbiamo eccezioni come ArrayStoreException , ClassCastException , o il preferito da tutti, la% % co_de.

I sistemi di tipi più potenti come Agda possono anche provare 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 di non avere eccezioni di runtime è che il sistema di tipi di Elm può provare l'assenza di (una porzione significativa di) comportamenti di runtime che in altre lingue possono non essere provato che non si verifichi e quindi potrebbe portare a comportamenti errati 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 significa nessun arresto anomalo, nessuna eccezione, e solo un risultato silenziosamente sbagliato).

Quindi, non dicono "non implementiamo eccezioni". Stanno dicendo "cose che sarebbero eccezioni di runtime in linguaggi tipici che i programmatori tipici di Elm dovrebbero avere, vengono catturati dal sistema di tipi". Ovviamente, qualcuno proveniente da Idris, Agda, Guru, Epigram, Isabelle / HOL, Coq o lingue simili vedrà Elm come piuttosto debole in confronto. L'istruzione è più indirizzata ai tipici programmatori Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl, ...

    
risposta data 02.12.2016 - 02:43
fonte
4

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

Elm ha un modo di segnalare le condizioni di errore in fase di esecuzione, ma questo sistema non è un'eccezione, è "Risultati". Una funzione che può fallire restituisce un "Risultato" che contiene un valore normale o un errore. Elms è strongmente digitato, quindi questo è esplicito nel sistema di tipi. Se una funzione restituisce sempre un numero intero, ha il tipo Int . Ma se restituisce un intero o fallisce, il tipo restituito è Result Error Int . (La stringa è il messaggio di errore.) Questo ti obbliga a gestire entrambi i casi in modo esplicito 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 toInt potrebbe non riuscire se l'input non è analizzabile, quindi il suo tipo di ritorno è Result String int . Per ottenere il valore intero effettivo, devi "scompattare" tramite la corrispondenza del modello, che a sua volta ti costringe a gestire entrambi i casi.

Risultati ed eccezioni fondamentalmente fa la stessa cosa, l'importante differenza sono i "default". Le eccezioni aumenteranno e termineranno il programma per impostazione predefinita, e dovrai prenderle esplicitamente se vuoi gestirle. Il risultato è l'altro modo: sei obbligato a gestirli per impostazione predefinita, quindi devi passare in modo imprevisto fino alla cima se vuoi che termini il programma. È facile vedere come questo comportamento possa portare a un codice più robusto.

    
risposta data 01.12.2016 - 18:27
fonte
3

Per prima cosa, tieni presente che il tuo esempio di eccezioni di "deglutizione" è in generale una pratica terribile e del tutto estraneo al non avere eccezioni del tempo di esecuzione; quando ci pensi, hai avuto un errore in fase di esecuzione, ma hai scelto di nasconderlo e non fare nulla al riguardo. Ciò si tradurrà spesso in bug che sono difficili da capire.

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

Elm è, tra le altre cose, un linguaggio di programmazione staticamente tipizzato . Uno dei vantaggi di questo tipo di sistemi di tipi è che molte classi di errori (anche se non tutte) vengono catturate dal compilatore, prima che il programma venga effettivamente utilizzato. Alcuni tipi di errori possono essere codificati in tipi (come Elm Result e Task ), anziché essere generati come eccezioni. Questo è ciò che i progettisti di Elm intendono dire: molti errori verranno catturati in fase di compilazione invece che in "tempo di esecuzione", e il compilatore ti costringerà ad affrontarli invece di ignorarli e sperare per il meglio. È chiaro perché questo è un vantaggio: meglio che il programmatore si accorga di un problema prima che l'utente lo faccia.

Nota che quando non usi le eccezioni, gli errori sono codificati in altri modi, meno sorprendenti. Dalla documentazione di Elm :

One of the guarantees of Elm is that you will not see runtime errors in practice. NoRedInk has been using Elm in production for about a year now, and they still have not had one! Like all guarantees in Elm, this comes down to fundamental language design choices. In this case, we are helped by the fact that Elm treats errors as data. (Have you noticed we make things data a lot here?)

I progettisti di Elm sono un po 'audaci nel sostenere "senza eccezioni di run time" , anche se lo qualificano con "in pratica". Quello che probabilmente significano è "meno errori imprevisti che se si stesse codificando in javascript".

    
risposta data 01.12.2016 - 18:01
fonte
0

Reclami di Elm:

No runtime errors in practice. No null. No undefined is not a function.

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 produce errori di runtime. Pertanto, non hai bisogno di eccezioni.

Quindi la domanda dovrebbe essere:

What is the benefit of having "no runtime errors"?

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

    
risposta data 04.03.2017 - 15:54
fonte