Ключевое слово X/exception
В 11l присутствует два
[[вида/]типа/]рода исключений:
- Нефатальные/дешёвые исключения. Обязательны к перехвату в исходном коде, компилятор должен проверить, что все такие исключения перехватываются.
Их необходимо указывать в ‘спецификаторах исключений’\‘exception specifications’. Поведение аналогично checked исключениям в Java, а реализация — аналогична ошибкам в Rust и Swift — т.е. просто как дополнительное [скрытое] возвращаемое значение у функции.
- Фатальные/дорогие исключения, отсутствие которых гарантировать невозможно и которые нельзя проверить на этапе компиляции. Поведение аналогично тому, как реализованы исключения в самых популярных языках программирования (C++, Python, C#, unchecked исключения в Java).
Исключения первого рода используют синтаксис
X <объект_исключения>
для порождения и
X.handle <тип_исключения>
для обработки.
Исключения второго рода используют синтаксис
X.throw <объект_исключения>
для порождения и
X.try
/
X.catch
для обработки.
Почему `X.handle`?
Exception Handling for C++:
Similarly, we chose catch
in preference to handle
because handle
is a commonly used C identifier.
В отличие от C++ и большинства других языков программирования, ключевые подслова в 11l могут быть какими угодно.
Пример исключения
первого рода:
T StopIteration {}
T RangeIterator
Int cur, end
F (start, end)
.cur = start
.end = end
F __next__() X(StopIteration)
I .cur >= .end
X StopIteration()
R .cur++
V it = RangeIterator(1, 10)
L
V i = it.__next__()
X.handle StopIteration
L.break
print(i)
Обратите внимание, что код, порождающий исключения первого рода,
не обязательно заключать в блок
X.try
{…при этом обработчик исключения должен располагаться сразу за вызовом функции, потенциально порождающей данное исключение}, т.к. такие исключения гарантированно должны быть обработаны.
Но при необходимости его можно заключить в блок
X.try
:
F circles_from_p1p2r(p1, p2, r) X(Error)
I r == 0.0
X Error(‘radius of zero’)
I p1 == p2
X Error(‘coincident points gives infinite number of Circles’)
...
R (c1, c2)
L(p1, p2, r) [((0.1234, 0.9876), (0.8765, 0.2345), 2.0),
...
((0.1234, 0.9876), (0.1234, 0.9876), 0.0)]
print("Through points:\n #.,\n #.\n and radius #.6\nYou can construct the following circles:".format(p1, p2, r))
X.try
V (c1, c2) = circles_from_p1p2r(p1, p2, r)
print(" #.\n #.\n".format(c1, c2))
X.handle Error v
print(" ERROR: #.\n".format(v.msg))
(
Полный код.)
Исключения первого рода хорошо подходят для возврата из рекурсивных функций:
F walk_maze(m, n, &cell, indx) X(PercolatedException)
cell[n][m] = indx
I n < :nn - 1 & cell[n + 1][m] == :NOT_VISITED
walk_maze(m, n + 1, &cell, indx)
E I n == :nn - 1
X PercolatedException((m, indx))
I m & cell[n][m - 1] == :NOT_VISITED
walk_maze(m - 1, n, &cell, indx)
I m < :M - 1 & cell[n][m + 1] == :NOT_VISITED
walk_maze(m + 1, n, &cell, indx)
I n & cell[n - 1][m] == :NOT_VISITED
walk_maze(m, n - 1, &cell, indx)
F check_from_top(&cell) -> (Int, Int)?
V (n, walk_index) = (0, 1)
L(m) 0 .< :M
I cell[n][m] == :NOT_VISITED
walk_index++
walk_maze(m, n, &cell, walk_index)
X.handle PercolatedException ex
R ex.t
R N
(
Полный код.)
Пример исключения
второго рода:
T Error
String message
F (message)
.message = message
X.try
print(‘1’)
X.throw Error(‘error message’)
print(‘never printed string’)
X.catch Error e
print(‘Error: ’e.message)
Этот код выведет:
1
Error: error message
Обоснование разделения исключений в 11l на два рода
Разделение исключений на checked и unchecked в Java — идея, признанная неудачной:
Don't Use Checked Exceptions:
Java is the only (mainstream) programming language to implement the concept of checked exceptions. Ever since, checked exceptions have been the subject of controversy. Considered an innovative concept at the time (Java was introduced in 1996), nowadays they are commonly considered bad practice.
The Trouble with Checked Exceptions A Conversation with Anders Hejlsberg, Part II:
I completely agree that checked exceptions are a wonderful feature. It's just that particular implementations can be problematic. By implementing checked exceptions the way it's done in Java, for example, I think you just take one set of problems and trade them for another set of problems.
Почему тогда в 11l сделано так же?
Нет, не так же.
Checked и unchecked исключения в Java имеют неудачный синтаксис и реализацию:
- синтаксически перехват и возбуждение checked и unchecked исключений никак не отличаются (
throw
и catch
работают для них в Java одинаково, в 11l это устранено — checked исключения [исключения первого рода] имеют другой синтаксис);
- возбуждение checked исключений такое же медленное как и unchecked, что подавляет мотивацию использовать checked исключения в Java в качестве альтернативного возвращаемого значения.
В 11l исключения первого рода — это просто дополнительное возвращаемое значение, просто синтаксический сахар, аналог ошибок в
Rust и
Swift.
И в дополнение, 11l поддерживает полноценные исключения, как в Python, C++, C#, Java и многих других языках. Это — исключения второго рода.
Почему исключений первого рода не достаточно?
Следование по пути Rust приводит к значительному усложнению кода, если хочется иметь возможность обрабатывать любые ошибки. Т.к. все возможные ошибки в коде функции должны быть
совместимы с типом возвращаемого значения.
Следование же пути Python с традиционными исключениями, с одной стороны, значительно упрощает программирование
{…можно вообще не думать об обработке ошибок, если это не критично для данного приложения, и полагаться целиком на понятные сообщения об ошибках при возникновении необработанных исключений}, а с другой — даёт возможность обработки любых исключений на любом уровне
[внутри функции и снаружи].
Конкретный пример: в symasm вместо того, чтобы вставлять проверку в
каждом вызове
eoc()
, была
добавлена обработка исключения
IndexError
, которое возникает при попытке обращения к несуществующему операнду.
[Хотя данный пример не очень показателен, т.к. в Rust «проверка в каждом вызове eoc()
» сводится к добавлению одного символа ?
после eoc()
.]
Замечание про термин «исключение»
Конечно, использовать термин «исключение» применительно к исключениям первого рода — это не очень корректно. Более правильно было бы называть их «ошибками», как в Rust или Swift. Но ключевое слово используется
exception
, а добавить ключевое слово
error
в 11l, во-первых,
невозможно, и, во-вторых, это часто используемое имя переменной.