Jaka jest korzyść z braku „żadnych wyjątków czasu wykonywania”, jak twierdzi Elm?

16

Niektóre języki twierdzą, że „nie ma wyjątków czasu wykonywania”, co stanowi wyraźną przewagę nad innymi językami, które je mają.

Jestem zdezorientowany w tej sprawie.

O ile wiem wyjątek czasu wykonywania jest tylko narzędziem, a jeśli jest dobrze używany:

  • możesz komunikować „brudne” stany (wyrzucając nieoczekiwane dane)
  • dodając stos możesz wskazać łańcuch błędów
  • możesz rozróżnić bałagan (np. zwracanie pustej wartości przy nieprawidłowych danych wejściowych) i niebezpieczne użycie, które wymaga uwagi programisty (np. zgłaszanie wyjątku w przypadku nieprawidłowych danych wejściowych)
  • możesz dodać szczegóły do ​​swojego błędu za pomocą komunikatu wyjątku zawierającego dalsze pomocne szczegóły pomagające w debugowaniu (teoretycznie)

Z drugiej strony bardzo trudno mi debugować oprogramowanie, które „połyka” wyjątki. Na przykład

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

Pytanie zatem brzmi: jaka jest silna, teoretyczna zaleta braku „wyjątków czasu wykonywania”?


Przykład

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

W praktyce nie występują błędy w czasie wykonywania. Brak wartości null. Żadna niezdefiniowana nie jest funkcją.

atoth
źródło
Myślę, że pomocne byłoby podanie przykładu lub dwóch języków, do których się odwołujesz i / lub linków do takich roszczeń. Można to interpretować na wiele sposobów.
JimmyJames,
Najnowszy przykład, jaki znalazłem, to wiąz w porównaniu do C, C ++, C #, Java, ECMAScript itp. Zaktualizowałem moje pytanie @JimmyJames
atoth
2
To pierwszy raz o tym słyszałem. Moją pierwszą reakcją jest zadzwonienie do BS. Zwróć uwagę na słowa łasicy: w praktyce
JimmyJames
@atoth Mam zamiar edytować tytuł pytania, aby było jaśniejsze, ponieważ istnieje wiele niepowiązanych pytań, które wyglądają podobnie do niego (np. „RuntimeException” vs „Exception” w Javie). Jeśli nie podoba ci się nowy tytuł, możesz go edytować ponownie.
Andres F.
OK, chciałem, żeby było to dość ogólne, ale mogę się zgodzić, że jeśli to pomoże, dziękuję za Twój wkład @AndresF.!
atoth

Odpowiedzi:

28

Wyjątki mają niezwykle ograniczającą semantykę. Muszą być obsługiwane dokładnie tam, gdzie są rzucane, lub w stosie wywołań bezpośrednich w górę, i nie ma dla programisty wskazania w czasie kompilacji, jeśli zapomnisz to zrobić.

Porównaj to z Wiązem, w którym błędy są kodowane jako Wyniki lub Maybes , które są wartościami . Oznacza to, że otrzymasz błąd kompilatora, jeśli nie obsłużysz tego błędu. Możesz przechowywać je w zmiennej, a nawet w kolekcji, aby odłożyć ich obsługę na dogodny czas. Możesz utworzyć funkcję do obsługi błędów w sposób specyficzny dla aplikacji zamiast powtarzania bardzo podobnych bloków try-catch w całym miejscu. Możesz połączyć je w obliczenia, które zakończą się sukcesem tylko wtedy, gdy wszystkie jego części się powiodą, i nie trzeba ich wcisnąć w jeden blok próbny. Nie jesteś ograniczony przez wbudowaną składnię.

To nie przypomina „przełykania wyjątków”. Wyraźnie określa warunki błędów w systemie typów i zapewnia znacznie bardziej elastyczną alternatywną semantykę do ich obsługi.

Rozważ następujący przykład. Możesz wkleić to na http://elm-lang.org/try, jeśli chcesz zobaczyć to w akcji.

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")
    ]

Zauważ, że String.toIntw calculatefunkcji ma możliwość awarii. W Javie może to spowodować zgłoszenie wyjątku czasu wykonywania. Gdy czyta dane wejściowe od użytkownika, ma dość dużą szansę. Wiąz zmusza mnie do poradzenia sobie z tym, zwracając Result, ale zauważ, że nie muszę się z tym natychmiast mierzyć. Mogę podwoić dane wejściowe i przekonwertować je na ciąg, a następnie sprawdzić , czy dane wejściowe w getDefaultfunkcji są nieprawidłowe . To miejsce jest znacznie lepiej dostosowane do sprawdzania niż albo punkt, w którym wystąpił błąd, albo w górę w stosie wywołań.

Sposób, w jaki kompilator zmusza naszą rękę, jest również znacznie bardziej szczegółowy niż sprawdzone wyjątki Javy. Musisz użyć bardzo specyficznej funkcji, takiej jak Result.withDefaultwyodrębnienie żądanej wartości. Chociaż technicznie możesz nadużywać tego rodzaju mechanizmów, nie ma to większego sensu. Ponieważ możesz odroczyć decyzję, dopóki nie zobaczysz dobrego komunikatu o błędzie domyślnym / błędzie, nie ma powodu, aby go nie używać.

Karl Bielefeldt
źródło
8
That means you get a compiler error if you don't handle the error.- Cóż, takie było uzasadnienie sprawdzonych wyjątków w Javie, ale wszyscy wiemy, jak dobrze to zadziałało.
Robert Harvey,
4
@RobertHarvey W pewnym sensie sprawdzonymi wyjątkami Javy były wersje tego biedaka. Niestety mogą zostać „połknięte” (jak w przykładzie PO). Nie były to także typy rzeczywiste, co czyniło z nich dodatkową ścieżkę do przepływu kodu. Języki z lepszych systemów typu pozwalają na błędy kodują jako wartości najwyższej klasy, która jest, co zrobić w (powiedzmy) z Haskell Maybe, Eitheritp Elm wygląda jakby biorąc stronę z języków takich jak ML, SML lub Haskell.
Andres F.
3
@JimmyJames Nie, nie zmuszają cię. Musisz „obsłużyć” błąd tylko wtedy, gdy chcesz użyć wartości. Jeśli tak x = some_func(), nie muszę nic robić, chyba że chcę zbadać wartość x, w którym to przypadku mogę sprawdzić, czy mam błąd lub „prawidłową” wartość; ponadto próba użycia jednego zamiast drugiego jest błędem typu statycznego, więc nie mogę tego zrobić. Jeśli typy Wiązów działają podobnie jak inne języki funkcjonalne, mogę faktycznie tworzyć takie rzeczy, jak komponować wartości z różnych funkcji, zanim nawet będę wiedział, czy są to błędy, czy nie! Jest to typowe dla języków FP.
Andres F.
6
@atoth Ale należy mieć znaczące zyski i nie jest bardzo dobry powód (jak zostało wyjaśnione w wielu odpowiedzi na pytanie). Naprawdę zachęcam do nauki języka ze składnią podobną do ML, a zobaczysz, jak wyzwalające jest pozbycie się cruftu składni podobnego do C (nawiasem mówiąc, ML został opracowany na początku lat 70. XX wieku, co czyni go z grubsza współczesny C). Ludzie, którzy zaprojektowali tego rodzaju systemy typu, uważają, że ten typ składni jest normalny, w przeciwieństwie do C :) Gdy już to robisz, nie zaszkodzi nauczyć się Lisp :)
Andres F.
6
@atoth Jeśli chcesz wziąć jedną rzecz z tego wszystkiego, weź to: zawsze upewnij się, że nie padniesz ofiarą Paradoksu Blub . Nie denerwuj się nową składnią. Może jest tam ze względu na potężne funkcje, których nie znasz :)
Andres F.
10

Aby zrozumieć to stwierdzenie, musimy najpierw zrozumieć, co kupuje system typu statycznego. Zasadniczo to, co daje nam system typów statycznych, jest gwarancją: jeśli sprawdzimy typ programu, nie będzie mogła wystąpić pewna klasa zachowań uruchomieniowych.

To brzmi złowieszczo. Sprawdzanie typów jest podobne do sprawdzania twierdzeń. (W rzeczywistości, zgodnie z izomorfizmem Curry'ego-Howarda, są one tym samym.) Jedną z osobliwości twierdzeń jest to, że gdy udowodnisz twierdzenie, udowodnisz dokładnie to, co mówi to twierdzenie, nie więcej. (To na przykład dlatego, gdy ktoś mówi „Udowodniłem, że ten program jest poprawny”, zawsze powinieneś zapytać „proszę zdefiniować„ poprawny ””.) To samo dotyczy systemów typu. Kiedy mówimy „program jest typu bezpieczne”, co mamy na myśli, to nie , że nie może dojść do ewentualnego błędu. Możemy tylko powiedzieć, że błędy, które system typów obiecuje nam zapobiec, nie mogą wystąpić.

Programy mogą mieć nieskończenie wiele różnych zachowań w środowisku wykonawczym. Spośród nich nieskończenie wiele jest przydatnych, ale także nieskończenie wiele z nich jest „niepoprawnych” (dla różnych definicji „poprawności”). System typu statycznego pozwala nam udowodnić, że nie może wystąpić określony skończony, ustalony zestaw tych nieskończenie wielu niepoprawnych zachowań środowiska uruchomieniowego.

Różnica między różnymi systemami typów polega zasadniczo na tym, w ilu i jak złożonym zachowaniu środowiska wykonawczego mogą się one nie pojawić. Słabe systemy typu, takie jak Java, mogą udowodnić tylko bardzo podstawowe rzeczy. Na przykład Java może udowodnić, że metoda, która jest typowana jako zwracająca a, Stringnie może zwrócić a List. Ale, na przykład, może nie okazać się, że metoda ta nie będzie powrotu. Nie może również udowodnić, że metoda nie zgłosi wyjątku. I nie może udowodnić, że nie zwróci zła String  - każdy Stringspełni wymagania sprawdzania typu. (I oczywiście nawetnull będzie go zadowolić, jak również.) Istnieją nawet bardzo proste rzeczy, że Java nie może udowodnić, dlatego mamy wyjątki, takie jak ArrayStoreException, ClassCastExceptionczy wszyscy ulubione The NullPointerException.

Potężniejsze systemy typu, takie jak Agda, mogą również udowodnić, że „zwróci sumę dwóch argumentów” lub „zwraca posortowaną wersję listy przekazaną jako argument”.

Teraz projektanci Elm mają na myśli stwierdzenie, że nie mają wyjątków w czasie wykonywania, że ​​system typów Elma może udowodnić brak (znacznej części) zachowań w czasie wykonywania, których w innych językach nie można udowodnić, a zatem mogą prowadzić do błędnego zachowania w czasie wykonywania (co w najlepszym przypadku oznacza wyjątek, w gorszym przypadku oznacza awarię, aw najgorszym ze wszystkich oznacza brak awarii, wyjątek i po prostu cicho zły wynik).

Tak, są one nie mówią „nie realizować wyjątki”. Mówią „rzeczy, które byłyby wyjątkami w czasie wykonywania w typowych językach, z którymi typowi programiści przybywający do Elm mieliby do czynienia, są przechwytywani przez system typów”. Oczywiście, ktoś pochodzący z Idris, Agdy, Guru, Epigram, Isabelle / HOL, Coq lub podobnych języków, zobaczy Elma jako dość słabego w porównaniu. Instrukcja jest bardziej skierowana do typowych programistów Java, C♯, C ++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl,…

Jörg W Mittag
źródło
5
Uwaga dla potencjalnych redaktorów: Przykro mi z powodu używania podwójnych, a nawet potrójnych negatywów. Zostawiłem je jednak celowo: systemy typów gwarantują brak określonych rodzajów zachowań w środowisku wykonawczym, tj. Gwarantują, że pewne rzeczy nie wystąpią. I chciałem zachować to sformułowanie „udowodnić, że nie występuje” nienaruszone, co niestety prowadzi do konstrukcji takich jak „nie można udowodnić, że metoda nie powróci”. Jeśli możesz znaleźć sposób na ich poprawę, śmiało, ale pamiętaj o powyższym. Dziękuję Ci!
Jörg W Mittag,
2
Dobra odpowiedź ogólnie, ale jeden mały nitpick: „sprawdzanie typu jest podobne do twierdzenia twierdzenia”. W rzeczywistości sprawdzanie typów jest bardziej podobne do sprawdzania twierdzeń: oba dokonują weryfikacji , a nie dedukcji .
ogrodnik
4

Wiąz nie może zagwarantować żadnego wyjątku czasu wykonywania z tego samego powodu C nie może zagwarantować żadnego wyjątku czasu działania: język nie obsługuje pojęcia wyjątków.

Wiąz ma sposób sygnalizowania warunków błędów w czasie wykonywania, ale ten system nie jest wyjątkiem, lecz „wynikami”. Funkcja, która może zawieść, zwraca „Wynik”, który zawiera wartość regularną lub błąd. Wiąz jest silnie wpisany, więc jest to wyraźnie zaznaczone w systemie typów. Jeśli funkcja zawsze zwraca liczbę całkowitą, ma typ Int. Ale jeśli zwróci liczbę całkowitą lub nie powiedzie się, typem zwracanym jest Result Error Int. (Ciąg jest komunikatem o błędzie.) Zmusza to użytkownika do jawnego obsługiwania obu przypadków w witrynie wywoływania.

Oto przykład ze wstępu (nieco uproszczony):

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

    Ok age ->
        text "OK!"

Funkcja toIntmoże się nie powieść, jeśli dane wejściowe nie są analizowalne, więc typem zwracanym jest Result String int. Aby uzyskać rzeczywistą wartość całkowitą, musisz „rozpakować” poprzez dopasowanie wzorca, co z kolei zmusza cię do obsługi obu przypadków.

Wyniki i wyjątki zasadniczo robią to samo, istotną różnicą są „domyślne”. Wyjątki będą domyślnie zawieszane i kończą działanie programu, a jeśli chcesz je obsłużyć, musisz je jawnie złapać. Rezultat jest inny - domyślnie jesteś zmuszony je obsłużyć, więc musisz zręcznie przekazać je aż do szczytu, jeśli chcesz, aby zakończyły program. Łatwo jest zobaczyć, jak to zachowanie może prowadzić do bardziej niezawodnego kodu.

JacquesB
źródło
2
@atoth Oto przykład. Wyobraź sobie, że język A dopuszcza wyjątki. Następnie masz funkcję doSomeStuff(x: Int): Int. Zwykle spodziewasz się, że zwróci Int, ale czy może również rzucić wyjątek? Nie patrząc na jego kod źródłowy, nie możesz wiedzieć. Natomiast język B, który koduje błędy za pomocą typów, może mieć taką samą funkcję zadeklarowaną w ten sposób: doSomeStuff(x: Int): ErrorOrResultOfType<Int>(W Elm ten typ jest faktycznie nazwany Result). W odróżnieniu od pierwszego przypadku, od razu wiadomo, czy funkcja może zawieść, i należy ją jawnie obsłużyć.
Andres F.,
1
Jak @RobertHarvey sugeruje w komentarzu do innej odpowiedzi, wydaje się to zasadniczo przypominać sprawdzone wyjątki w Javie. Nauczyłem się, pracując z Javą we wczesnych dniach, kiedy sprawdzono większość wyjątków, że naprawdę nie chcesz być zmuszany do pisania kodu, aby zawsze pojawiał się błąd.
JimmyJames,
2
@JimmyJames To nie jest tak, jak sprawdzone wyjątki, ponieważ wyjątki się nie komponują, mogą być ignorowane („połykane”) i nie są pierwszorzędnymi wartościami :) Naprawdę zalecam naukę statycznie funkcjonalnego języka, aby naprawdę to zrozumieć. To nie jest jakaś nowomodna rzecz, którą wymyślił Elm - tak programujesz w językach takich jak ML lub Haskell i różni się ona od Javy.
Andres F.
2
@AndresF. this is how you program in languages such as ML or HaskellW Haskell tak; ML, nr Robert Harper, główny współpracownik Standard ML i badacz języka programowania, uważa wyjątki za przydatne . Typy błędów mogą przeszkadzać w tworzeniu funkcji w przypadkach, w których można zagwarantować, że błąd nie wystąpi. Wyjątki mają również inną wydajność. Nie płacisz za wyjątki, które nie są zgłaszane, ale za każdym razem płacisz za sprawdzanie wartości błędów, a wyjątki są naturalnym sposobem wyrażania wstecznego śledzenia w niektórych algorytmach
Doval
2
@JimmyJames Mam nadzieję, że teraz widzisz, że sprawdzone wyjątki i rzeczywiste typy błędów są tylko pozornie podobne. Sprawdzone wyjątki nie łączą się z wdziękiem, są nieporęczne w użyciu i nie są zorientowane na ekspresję (dlatego możesz je po prostu „połknąć”, tak jak dzieje się to w Javie). Niesprawdzone wyjątki są mniej uciążliwe, dlatego są wszędzie normą oprócz Javy, ale są bardziej prawdopodobne, że Cię potkną, a nie możesz stwierdzić, patrząc na deklarację funkcji, czy to rzuca, czy nie, co utrudnia program rozumieć.
Andres F.
2

Po pierwsze, pamiętaj, że twój przykład „połykania” wyjątków jest ogólnie okropną praktyką i całkowicie niezwiązaną z brakiem wyjątków w czasie wykonywania; gdy się nad tym zastanowił, wystąpił błąd czasu wykonywania, ale postanowiono go ukryć i nic z tym nie zrobić. To często powoduje błędy, które są trudne do zrozumienia.

To pytanie można interpretować na wiele sposobów, ale ponieważ w komentarzach wspomniano o Elm, kontekst jest jaśniejszy.

Wiąz jest między innymi statycznym językiem programowania. Jedną z zalet tego rodzaju systemów typów jest to, że kompilator przechwytuje wiele klas błędów (choć nie wszystkie), zanim program zostanie faktycznie użyty. Niektóre rodzaje błędów mogą być zakodowane w typach (takich jak Elm Resulti Task), zamiast być zgłaszane jako wyjątki. Oto, co mają na myśli projektanci Elma: wiele błędów zostanie wychwyconych w czasie kompilacji zamiast w „czasie wykonywania”, a kompilator zmusi cię do radzenia sobie z nimi zamiast ignorowania ich i liczenia na najlepsze. Jest jasne, dlaczego jest to zaleta: lepiej, aby programista zdał sobie sprawę z problemu, zanim zrobi to użytkownik.

Pamiętaj, że gdy nie używasz wyjątków, błędy są kodowane na inne, mniej zaskakujące sposoby. Z dokumentacji wiązu :

Jedną z gwarancji Elm jest to, że w praktyce nie zobaczysz błędów w czasie wykonywania. NoRedInk używa Elm do produkcji od około roku i nadal go nie ma! Jak wszystkie gwarancje Elm, sprowadza się to do podstawowych wyborów językowych. W tym przypadku pomaga nam fakt, że Elm traktuje błędy jako dane. (Czy zauważyłeś, że tworzymy tutaj dużo danych?)

Projektanci wiązów śmiało twierdzą, że „nie ma wyjątków w czasie wykonywania” , choć określają to mianem „w praktyce”. To, co prawdopodobnie oznaczają, to „mniej nieoczekiwanych błędów niż w przypadku kodowania w javascript”.

Andres F.
źródło
Czy to źle rozumiem, czy po prostu grają w grę semantyczną? Zakazują nazwy „wyjątek czasu wykonywania”, ale po prostu zastępują ją innym mechanizmem, który przekazuje informacje o błędach w górę stosu. To brzmi jak zmiana implementacji wyjątku na podobną koncepcję lub obiekt, który inaczej implementuje komunikat o błędzie. To nie jest wstrząsające ziemią. Jest jak każdy statycznie wpisany język. Porównaj przejście z COM HRESULT na wyjątek .NET. Inny mechanizm, ale nadal jest wyjątkiem w czasie wykonywania, bez względu na to, jak go nazwiesz.
Mike wspiera Monikę
@Mike Szczerze mówiąc, nie przyjrzałem się szczegółowo Elmowi. Sądząc docs, mają typy Resulti Taskktóre wyglądają bardzo podobnie do bardziej zaznajomieni Eitheroraz Futurez innych języków. W przeciwieństwie do wyjątków, wartości tego typu można łączyć iw pewnym momencie należy je jawnie obsłużyć: czy reprezentują one prawidłową wartość lub błąd? Nie czytam w myślach, ale ten brak zaskoczenia programisty jest prawdopodobnie tym, co projektanci Elm rozumieli przez „brak wyjątków czasu wykonywania” :)
Andres F.
@Mike Zgadzam się, że to nie jest wstrząsające ziemią. Różnica w stosunku do wyjątków środowiska wykonawczego polega na tym, że nie są one jednoznaczne w typach (tzn. Nie można wiedzieć, nie patrząc na jego kod źródłowy, czy fragment kodu może zostać wygenerowany); błędy kodowania typów są bardzo wyraźne i zapobiegają ich przeoczeniu przez programistę, co prowadzi do bezpieczniejszego kodu. Odbywa się to w wielu językach FP i tak naprawdę nie jest niczym nowym.
Andres F.
1
Zgodnie z twoimi komentarzami myślę, że jest w tym coś więcej niż „statyczna kontrola typu” . Możesz dodać go do JS za pomocą Typescript, który jest znacznie mniej restrykcyjny niż nowy ekosystem „make-or-break”.
atoth
1
@AndresF .: Z technicznego punktu widzenia najnowszą cechą systemu typów Java jest parametryczny polimorfizm, który pochodzi z późnych lat 60. Mówiąc w pewnym sensie, powiedzenie „nowoczesny”, gdy masz na myśli „nie Java”, jest nieco poprawne.
Jörg W Mittag,
0

Wiąz twierdzi:

W praktyce nie występują błędy w czasie wykonywania. Brak wartości null. Żadna niezdefiniowana nie jest funkcją.

Ale pytasz o wyjątki czasu wykonywania . Jest różnica.

W Elm nic nie zwraca nieoczekiwanego wyniku. NIE możesz napisać poprawnego programu w Elm, który powoduje błędy w czasie wykonywania. Dlatego nie potrzebujesz wyjątków.

Tak więc pytanie powinno brzmieć:

Jaka jest korzyść z braku „błędów w czasie wykonywania”?

Jeśli możesz napisać kod, który nigdy nie zawiera błędów w czasie wykonywania, twoje programy nigdy się nie zawieszą.

Zabijaka
źródło