Skąd się wzięły wyjątki?

Zdecydowana większość współczesnych języków programowania posiada wbudowaną obsługę wyjątków. Związana z nią składnia zazwyczaj jest we wszystkich tych językach dosyć podobna – wyjątki rzuca się (throw) lub podnosi (raise), a następnie łapie (catch), jeśli wystąpiły wewnątrz blokutry. Istotną sprawą jest tu możliwość przekazywania kontroli sterowania nie tylko o jeden poziom wyżej – wyjątki propagowane są w górę tak długo, dopóki gdzieś nie zostaną złapane lub, jeśli programista nie próbował nigdzie przechwycić wyjątku, zostanie użyty domyślny handler, który zakończy działanie programu.

Wyjątki potrafią niekiedy bardzo ułatwić życie i zwiększyć przejrzystość kodu, o czym doskonale wiedzą o tym ci, którzy mieli okazję stosować tradycyjną metodę obsługi błędów, polegającą na weryfikacji wartości zwracanych przez funkcje. Czy jednak zastanawialiście się kiedykolwiek nad tym kto i kiedy wymyślił obsługę sytuacji wyjątkowych w formie, którą najczęściej posługujemy się obecnie? Jaki język programowania był prekursorem takiego rozwiązania i kto był jego twórcą? Okazuje się, że z perspektywy historii programowania temat ten jest niezwykle ciekawy!

Wyjątkowe początki

Za pierwszy język, który implementował obsługę wyjątków podaje się najczęściej PL/I (Programming Language One) – stworzony w 1964 roku język programowania firmowany przez IBM. Wprawdzie pierwsze działające kompilatory dla PL/I pojawiły się dopiero kilka lat później, ale wciąż mówimy o latach 60-tych, co pod względem chronologicznym jednoznacznie stawia ten język na pierwszym miejscu. Chociaż sama idea obsługi błędów stanowiła już zaczątek wyjątków, to stosowana terminologia była daleka od współczesnej.

Procedury przechwytujące wyjątki są w PL/I nazywane ON-units. Pozwalają one zdefiniować zachowanie programu w przypadku wystąpienia określonych warunków (błędów), takich jak FIXEDOVERFLOW, ZERODIVIDEczyUNDERFLOW. Na przykład funkcja obsługująca błąd dzielenia przez zero musi być zdefiniowana jako:

Częstą praktyką było przypisanie globalnym zmiennym odpowiednich wartości, a następnie wykonanie skoku do miejsca znajdującego się już poza procedurą, w której powstał wyjątek. Alternatywną ścieżką wykonania był brak go to, co skutkowało powrotem do instrukcji następującej bezpośrednio po tej, która spowodowała błąd.

Metody ON-unit mogą być nadpisywane, co oznacza, że zdefiniowanie on zerodivide po raz kolejny w dalszej części kodu sprawi, iż od tego miejsca dzielenie przez zero spowoduje wywołanie nowej procedury obsługi błędu. Oczywiście programista wcale nie musiał pisać funkcji ON-unit do wszystkich możliwych błędów – brak handlerów powodował po prostu wykonanie domyślnych akcji systemu, którymi często było zamienienie się warunku w błąd (promocjęCONDITIONdoERROR) i w rezultacie zakończenie działania aplikacji.

Język PL/I pozwala nie tylko na obsługę wyjątków systemowych, ale też umożliwia użytkownikowi definiowanie własnych warunków oraz ich rzucanie w dowolnym momencie. Służy do tego słowo kluczowe SIGNAL, np.:

Istnieje możliwość dezaktywacji procedury ON-unit np. w wybranym bloku kodu. Realizowane jest to przy użyciu instrukcji REVERT. Obszerniejszy przykład mechanizmu wyjątków (wraz z komentarzem) w języku PL/I można znaleźć na stronie IBM [1].

Innowacyjny CLU

Znaczne ulepszenie wyjątków przyniósł język CLU, zaprojektowany w 1975 roku przez Barbarę Liskov (tak, to właśnie od jej nazwiska pochodzi jedna z literek w regułach SOLID) na uniwersytecie MIT. Język ten był innowacyjny na kilku polach – wprowadził pojęcie klastrów (ang. clusters), znaczeniowo odpowiadających znanym z programowania obiektowego klasom, a także był pionierem w kwestii użycia iteratorów.

Dużym krokiem naprzód w kwestii wyjątków było odejście od dynamicznego (jak miało to miejsce w PL/I) łączenia handlerów z funkcjami na rzecz statycznego (co oznacza, że można dowiedzieć się jak przechwytywane są konkretne wyjątki z samej tylko statycznej analizy kodu). Zwiększyło to czytelność programów i ułatwiło tworzenie wydajniejszych implementacji języka. Co więcej, zamiast w sposób globalny związywać warunki z procedurami je obsługującymi, Liskov postawiła na rozwiązanie, które zakłada przechwytywanie wyjątków bezpośrednio pod blokami kodu, w których mogły one zostać rzucone. Jest to więc podejście dość ucywilizowane i znane nam ze współcześnie używanych języków. Dla zobrazowania technikaliów poniżej przedstawiam fragment kodu w języku CLU, zaczerpnięty z publikacji „Exceptions Handling in CLU” autorstwa B. Liskov i A. Snydera [2]:

Trudno zaprzeczyć, że składnia wyjątków w CLU jest rzeczywiście czytelna i przejrzysta. Nareszcie spotykamy się ze słowem kluczowym except, a użycie when do rozróżniania typów również wydaje się być jasne. Widać, że dzięki wyjątkom w końcu możemy przekazywać jakieś dane, np. przechwytując bad_format możemy wprost odnieść się stringa, który zapewne powie nieco więcej o rzuconym błędzie. Powyższy przykład ujawnia jeszcze jedną cechę wyjątków w tym języku – są one sygnalizowane tylko o jeden poziom wyżej. W związku z tym, jeśli chcemy obsłużyć wyjątek jeszcze wyżej, należy go resygnalizować samodzielnie. Spójrzmy jak zaprezentowana funkcja napisana w CLU wyglądałaby w Pythonie:

Jest to oczywiście translacja bardzo dosłowna, mająca jedynie zilustrować działanie przykładu CLU, bowiem ze względu na fakt, iż w Pythonie wyjątków nie trzeba rzucać ponownie, moglibyśmy trzy ostatnie excepty po prostu usunąć.  Moglibyśmy też wyrazić je jeszcze bardziej dosłownie, podkreślając, że przechwytywane wyjątki niosą ze sobą jakieś dodatkowe informacje:

Chociaż Python dzieli z CLU słowo kluczowe except oraz brak średnioków, to może jednak lepszym językiem, na który można by przetłumaczyć pierwotnie przedstawiony kod jest C++. Dlaczego? Chodzi o miejsce tuż za sygnaturą funkcji:

Oczywiście szczegółowe znaczenie deklarowania w nagłówku funkcji wyjątków, które ona rzuca, jest inne w tych językach – w C++ ma charakter jedynie informacyjny oraz całkiem sporą grupę przeciwników, natomiast w CLU informacja o rzucanych wyjątkach jest obowiązkowa. Wiąże się to z tym, że w zaprojektowanym przez Liskov języku wyjątki są po prostu jednym z dwóch sposobów wyjścia z funkcji. Skoro zatem programista musi deklarować typ zwracany przez return, to ta sama obowiązuje jeśli procedura ma zwracać jakiś typ (wyjątku) przez signal.

Podsumowanie

W internetowych dyskusjach dotyczących historii mechanizmu wyjątków nierzadko pada też nazwa Lispa. Jako język, który wywarł wpływ na wiele innych późniejszych technologii, z pewnością był inspiracją dla nich również w kwestii wyjątków. Jednak według większości źródeł Lisp nie dzierży tu palmy pierwszeństwa, dlatego pozwoliłem sobie nie omawiać w tym wpisie obsługi błędów zaimplementowanej w lispowych dialektach. Warto natomiast podkreślić, że język, który wyjątki – nomen omen – rozpropagował, czyli C++ zaczerpnął ich model właśnie z CLU, o czym otwarcie pisze twórca C++, Bjarne Stroustrup, w artykule Exception Handling for C++ [3].

Można zatem stwierdzić, że współcześnie dominujący sposób obsługi wyjątków ukształtował się w latach 70-tych.  W świecie programowania miała wtedy miejsce dyskusja, w której padały najrozmaitsze pomysły. Niektóre z nich opisuje John B. Goodenough w publikacji Exception Handling: Issues and a Proposed Notation z roku 1975 [4], jak również sam przedstawia swój model. W ramach tego modelu autor proponuje używanie trzech rodzajów wyjątków – NOTIFY, ESCAPE oraz SIGNAL – które różnią się wymaganiem odnośnie tego, co ma się stać po zakończeniu procedury obsługi. W zależności od wybranego rodzaju wyjątku wykonanie kodu musi powrócić do miejsca, skąd został rzucony wyjątek, nie może do niego powrócić, lub jest to opcjonalne. Jak widać, współcześnie przyjęło się nieco inne podejście. Ale otwarte pozostaje pytanie – na jak długo?

Źródła

  1. PL/I condition handling example, IBM.
  2. Exception Handling in CLU [w:] „IEEE Transactions on Software Engineering”, B. H. Liskov, A. Snyder, listopad 1979.
  3. Exception Handling for C++, A. Koenig, B. Stroustrup, 1989.
  4. Exception Handling: Issues and a Proposed Notation, John B. Goodenough, 1975.
  5. StackOverflow: What language was the first to implement exception handling?