Równie dobrze mógłbyś napisać - co możemy zrobić z nullem? Nic mądrzejszego tylko propagować go wyżej, aż dojdzie do maina? Nieee, nulle się sprawdza i na nie reaguje.
Tam, gdzie się nulla oczekuje, tam się go sprawdza i nań reaguje. ALe chyba nie powiesz mi, że za każdym razem, gdy cokolwiek czytamy, zawsze obowiązkowo sprawdzamy nulla, nawet tam, gdzie nulla nie ma prawa być? No a jeśli nulla nie ma prawa być, a mimo wszystko jest, to znaczy że mamy bug więc leci wyjątek. Jak inaczej zareagować na sytuację, gdy właśnie wykryliśmy buga podczas działania programu?
Nie wszystkie wyjątki są checked, wiele nie jest: https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html
Eric Lippert dawno temu podzielił wyjątki na 4 kategorie: Fatal, Boneheaded, Vexing i Exogenous. Fatal i Boneheaded zazwyczaj powinny crashować całą aplikację, choć z odmiennych przyczyn. Zdaje mi się, że przy tej klasyfikacji RuntimeException
w Javie zazwyczaj wpadałyby do kategorii Fatal albo Boneheaded właśnie? Czyli nic nie tracimy na tym, że ich nie sprawdzamy statycznie, bo one i tak mają scrashować apkę.
Nawet w przypadku gdy są to bardzo często się te szczegółowe typy zlewa i wstawia throws IOException
czy nawet throws Exception
i wiadomo tyle, że coś co nie dziedziczy po java.lang.RuntimeException
może być rzucone.
No ale to wina programisty, a nie konstrukcji języka.
Dzięki temu, że Either jest zwykłą klasą tak jak wszystkie inne, a nie wyspecjalizowanym mechanizmem możemy tymi Eitherami operować jak innymi klasami. Dla przykładu mogę stworzyć listę Eitherów, albo np stworzyć dwie mapy Map<Key, Either<Problem, Result>>
i je złączyć (na zasadzie, że jeśli klucze się pokrywają to jakoś tam, wedle uznania, łączymy odpowiadające im Eithery).
OK. (chociaż w sumie można by Map<Key, Either<Exception, Result>>
) - czemu wyjątek nie ma mówić o problemie? Nie musimy ograniczać się do jednego mechanizmu
Dodatkowo checked exceptions, nawet z precyzyjnymi typami, są nieprecyzyjne jeśli chodzi o wskazywanie na źródło checked exception. throws SomeCheckedException
daje się na poziomie całej metody i nie widać od razu z którego wywołania pochodzi. Mając Eithera widzimy gdzie jest zwracany, więc dużo łatwiej prześledzić gdzie był stworzony ("rzucony").
Niby można każde wywołanie opatrywać try
/catch
...
Ale to nie jest tego rodzaju sytuacja, w której to zakaz używania wyjątków wydaje mi sie być mniej wygodny.
Tam, gdzie (mi się wydaje) wyjątki są wygodne, to wtedy, gdy trzeba wyskoczyć gdzieś wyżej, nawet jeśli owo "wyżej" może błąd sensownie obsłużyć.
Zalinkowany już przeze mnie Lippert jako przykład złego wyjątku (Vexing) daje parsowanie stringów do intów. No ale właśnie parsowanie inputu usera wydaje mi się być przykładem, gdzie wyjątki są po prostu wygodne...
public MyDataFormat ParseUserInput(string userInput)
{
var (first, middle, last) = BreakInput(userInput);
return new MyDataFormat(ParseFirstPartOfInput(first), Int32.Parse(middle), ParseLastPartOfInput(last));
}
I jak gdziekolwiek coś pójdzie nie tak, to wyjątek. Trzeba tylko pamiętać, by ten wyjątek obsłużyć w kodzie wołającym ParseUserInput
.
public bool TryParseUserInput(string userInput, out MyDataFormat result)
{
if(TryBreakInput(userInput, out (string first, string middle, string last) t))
{
var (first, middle, last) = t;
if(TryParseFirstPartOfInput(first, out var firstParsed) && Int32.TryParse(middle, out int middleParsed) && TryParseLastPartOfInput(last, out var lastParsed))
{
result = new MyDataFormat(firstParsed, middleParsed, lastParsed);
return true;
}
}
return false;
}
A TryBreakInput
, TryParseFirstPartOfInput
, TryParseLastPartOfInput
i ew. metody jeszcze niżej będą wyglądały tak samo, jak ten potworek!!
(aczkolwiek 1. Uznaję, że podejście z wyjątkami łatwo pokoci wyjątki powstałe w wyniku bugu w ParseUserInput
i wyjątki powstałe w wyniku błędnych danych wejściowych - bo sensownie jest uznać, że jeśli leci JAKIKOLWIEK wyjątek, to z pewnością w wyniku błędnych danych; z drugiej strony, rozróżnienie między tymi dwoma źródłami wyjątków wymagałoby upstrzenia kodu mnóstwem try
/catch
, co wyglądąłoby równie brzydko; 2. być może na Eitherach robi się to tak samo wygodnie jak na wyjątkach, szczególnie w języku do tego przeznaczonym, jak Haskell; 3. być może da się uprościć moje TryParse
bez wprowadzania wyjątków)