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
W praktyce nie występują błędy w czasie wykonywania. Brak wartości null. Żadna niezdefiniowana nie jest funkcją.
Odpowiedzi:
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.
Zauważ, że
String.toInt
wcalculate
funkcji 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ącResult
, 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 wgetDefault
funkcji 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.withDefault
wyodrę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ć.źródło
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.Maybe
,Either
itp Elm wygląda jakby biorąc stronę z języków takich jak ML, SML lub Haskell.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.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,
String
nie może zwrócić aList
. 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łaString
- każdyString
speł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 jakArrayStoreException
,ClassCastException
czy wszyscy ulubione TheNullPointerException
.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,…
źródło
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 jestResult 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):
Funkcja
toInt
może się nie powieść, jeśli dane wejściowe nie są analizowalne, więc typem zwracanym jestResult 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.
źródło
doSomeStuff(x: Int): Int
. Zwykle spodziewasz się, że zwróciInt
, 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 nazwanyResult
). W odróżnieniu od pierwszego przypadku, od razu wiadomo, czy funkcja może zawieść, i należy ją jawnie obsłużyć.this is how you program in languages such as ML or Haskell
W 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 algorytmachPo 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
Result
iTask
), 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 :
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”.
źródło
Result
iTask
które wyglądają bardzo podobnie do bardziej zaznajomieniEither
orazFuture
z 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” :)Wiąz twierdzi:
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ć:
Jeśli możesz napisać kod, który nigdy nie zawiera błędów w czasie wykonywania, twoje programy nigdy się nie zawieszą.
źródło