Обработка ошибок в LISP
Система условий в Лиспе — одна из его лучших особенностей. Он выполняет ту же задачу, что и механизмы обработки исключений в Java, Python и C++, но более гибок. На самом деле его адаптируемость выходит за рамки обработки ошибок, поскольку условия, более гибкие, чем исключения, могут представлять любое событие, происходящее во время работы программы, и могут представлять интерес для кода на разных уровнях стека вызовов. Система условий более гибкая, чем системы исключений, потому что она делит обязанности на три части: сигнализация условия, его обработка и перезапуск. Системы исключений делят обязанности на две части: код, который обрабатывает проблему, и код, который сигнализирует об условии.
Условие — это объект, класс которого описывает его общие характеристики, а данные экземпляра которого содержат информацию о специфике событий, приведших к указанному условию. За исключением того факта, что надклассом по умолчанию для классов, определенных с помощью DEFINE-CONDITION, является CONDITION, а не STANDARD-OBJECT, классы условий определяются с помощью макроса DEFINE-CONDITION.
Чтобы определить условие:
Синтаксис:
(define-condition condition-name (error)
((text :initarg :text :reader text))
)
Отлов любого условия (обработчик-случай):
Код, используемый для обработки ситуации, о которой сообщается, называется обработчиком условий. Функция ошибки обычно вызывается из одной из функций более высокого уровня, где она обычно определена. Механизм сигнализации ищет подходящий обработчик при уведомлении о состоянии на основе класса условия. Каждый обработчик имеет спецификатор типа, который указывает ситуации, которые он может обрабатывать, а также функцию, которая принимает только условие в качестве своего единственного аргумента.
Механизм сигнализации находит самый последний обработчик, совместимый с типом условия, и вызывает его функцию всякий раз, когда сигнализируется условие. Обработчик условия создается макросом Handler-case.
Синтаксис:
(handler-case (code that errors out)
(condition-type (the-condition);; <– optional argument
(code));;<– code of error clause
( another-condition (the-condition)
…))
Случай-обработчик возвращает свое значение, если код, обнаруживший ошибку, возвращается нормально. Случай Handler-body должен состоять из одного выражения; PROGN можно использовать для объединения множества выражений в одну форму. Код в соответствующем предложении ошибки выполняется, и его значение возвращается случаем обработчика, если код, который не удается выполнить, сигнализирует об условии, которое является экземпляром любого из типов условий, перечисленных в любом предложении ошибки.
Отлов определенного состояния:
Написав тип условия, мы можем сообщить обработчикам условий, какое условие обрабатывать. Этот процесс сравним с try/catch в других языках программирования, но мы можем добиться большего.
Пример 1:
Lisp
;; LISP code for error handling (handler - case ( / 3 0 ) (division - by - zero (c) ( format t "Caught division by zero: ~a~%" c))) |
Выход:
Вы часто будете сталкиваться со следующим предупреждением компилятора, если сохраните объект условия в качестве аргумента, но не получите к нему доступ в своих обработчиках:
; caught STYLE-WARNING:
; The variable C is defined but never used.
Используйте вызов declare, как показано ниже, чтобы удалить его:
(handler-case (/ 3 0)
(division-by-zero (c)
(declare (ignore c))(format t “Caught division by zero~%”)))
;; we don’t print “c” here and don’t get the warning.
Условия обработки (Handler-bind):
Обработчики условий могут реагировать на условие, активируя соответствующий перезапуск на этапе перезапуска, который является кодом, который действительно восстанавливает ваше приложение после проблем. Код перезапуска обычно вставляется в функции низкого или среднего уровня, а обработчики условий располагаются на более высоких уровнях приложения. Используя макрос Handler-bind, вы можете продолжить работу с функциями более низкого уровня, не раскручивая стек вызовов функций и не предоставляя функцию перезапуска. Другими словами, функция нижнего уровня будет по-прежнему контролировать процесс. Синтаксис Handler-bind следующий:
(handler-bind ((a-condition #’function-to-handle-it)
;;<– binding
(another-one #’another-function))
(code that can…)
(…error out))
Каждая привязка имеет функцию-обработчик с одним аргументом и типом условия. Макрос invoke-restart ищет и вызывает самую последнюю связанную функцию перезапуска, передавая указанное имя в качестве аргумента.
Пример 2:
Lisp
;Lisp program to demonstrate defining ; a condition and then handling it using handler - bind ;If the divisor argument is zero, ; the program will produce an error situation. ;there are three anonymous functions(cases) ; offer three different strategies to overcome it. ; ( return 0 , recalculate using divisor 3 ; and continue without returning) (define - condition dividing - by - zero (error) ((message :initarg :message :reader message)) ) ( defun div - zero - handle () (restart - case ( let ((result 0 )) ( setf result (div - func 24 0 )) ( format t "The value returned is: ~a~%" result) ) (continue () nil ) ) ) ( defun div - func (val1 val2) (restart - case ( if ( / = val2 0 ) ( / val1 val2) (error "dividing - by - zero :message "denominator is zero" ) ) (return - zero () 0 ) (return - val (x) x) (recalculate - with (s) (div - func val1 s)) ) ) ( defun high - level - code () (handler - bind ( (dividing - by - zero #"(lambda (i) ( format t "Error is: ~a~%" (message i)) (invoke - restart "return - zero) ) ) (div - zero - handle) ) ) ) (handler - bind ( (dividing - by - zero #"(lambda (i) ( format t "Error is: ~a~%" (message i)) (invoke - restart "return - val 0 ) ) ) ) (div - zero - handle) ) (handler - bind ( (dividing - by - zero #"(lambda (i) ( format t "Error is: ~a~%" (message i)) (invoke - restart "recalculate - with 3 ) ) ) ) (div - zero - handle) ) (handler - bind ( (dividing - by - zero #"(lambda (i) ( format t "Error is: ~a~%" (message i)) (invoke - restart "continue) ) ) ) (div - zero - handle) ) ( format t "Finished executing all cases!" )) |
Выход:
Обработчик-случай VS Обработчик-привязка:
Формы try/catch, используемые в других языках, сравнимы с регистром-обработчиком. Когда нам нужен полный контроль над тем, что происходит, когда возникает сигнал, мы должны использовать привязку обработчика. Он перезапускается интерактивно или программно и позволяет нам использовать отладчик.
Тот факт, что функция-обработчик, связанная с помощью Handler-bind, будет вызываться без раскручивания стека, сохраняя управление в вызове parse-log-entry при вызове этой функции, является более существенным различием между Handler-bind и Handler-case. Самый последний связанный перезапуск с указанным именем будет найден и вызван командой invoke-restart. Мы можем наблюдать перезапуски (созданные перезапуском-кейсом) где угодно глубоко в стеке, в том числе перезапуски, установленные другими библиотеками, функции которых вызывала эта библиотека, если какая-то библиотека не улавливает все ситуации и позволяет некоторым выскочить к нам. И мы можем видеть трассировку стека, которая включает в себя каждый вызванный фрейм, а также локальные переменные и другие вещи в некоторых лиспах. Все раскручивается, как только мы забываем об этом после рассмотрения дела. Стек не перематывается обработчиком-привязкой.
Условия сигнализации (броска):
В дополнение к «системе условий» в общем LISP также доступен ряд функций, которые можно использовать для сигнализации об ошибках. Однако способ обработки ошибки после сообщения о ней зависит от реализации.
Сообщение об ошибке задается программой пользователя. Функции анализируют это сообщение и могут показывать его пользователю, а могут и не показывать. Поскольку система LISP будет обрабатывать их в соответствии со своим предпочтительным стилем, сообщения об ошибках не должны содержать символ новой строки ни в начале, ни в конце, или для обозначения ошибки. Вместо этого их следует создавать с помощью функции форматирования.
Ниже приведены несколько часто используемых подпрограмм для предупреждений, прерываний и фатальных и нефатальных ошибок:
error format-string &rest args
- Это указывает на фатальную ошибку. Такие ошибки не могут быть устранены, поэтому они никогда не возвращаются к тому, кто их вызвал.
Мы можем использовать ошибку двумя способами:
- (Ошибка «какой-то текст»): указывает на состояние простой ошибки.
- (Сообщение об ошибке: «Мы попробовали то и это, но это не сработало».)
cerror continue-format-string error-format-string &rest args
- Заходит в отладчик и выдает ошибку. Однако после устранения проблемы он позволяет продолжить программу из отладчика.
- cerror возвращает nil, если программа возобновляется после возникновения ошибки. За вызовом cerror следует выполнение следующего кода. Этот код должен исправить проблему, возможно, запросив у пользователя новое значение, если переменная была неверной.
- Аргумент continue-format-string предоставляется в качестве управляющей строки для форматирования вместе с аргументами для создания строки сообщения, подобно аргументу error-format-string.
warn format-string &rest args
- Обычно он не входит в отладчик, а вместо этого выводит сообщение об ошибке.
- Реализация предупреждения должна позаботиться о перемещении сообщения об ошибке на новую строку до и после него, а также, возможно, указать имя функции, вызвавшей предупреждение.
break &optional format-string &rest args
- Без какой-либо возможности быть перехваченным запрограммированными инструментами обработки ошибок, он выводит сообщение и сразу же входит в отладчик.
- Break возвращает ноль, если продолжается. При использовании break параметры не требуются, и будет доставлено подходящее сообщение по умолчанию.
- Предполагается, что использование команды break для введения в программу временных «точек останова» отладки, а не для указания проблем, предотвратит любые непредвиденные действия по восстановлению. По этой причине Break не принимает дополнительный аргумент строки управления форматом, который принимает cerror.
Пример 3:
Lisp
; Lisp program to show signaling of ; condition using error function. ; Program calculates square root of a number ; and signals error if the number is negative. ( defun Square - root (i) ( cond ((or ( not (typep i "integer)) (minusp i)) (error "~S is a negative number!" i)) ((zerop i) 1 ) ( t (sqrt i)) ) ) (write(Square - root 25 )) (terpri) (write(Square - root - 3 )) |
Выход:
Пользовательские сообщения об ошибках:
До сих пор всякий раз, когда возникает ошибка или сообщается об ошибке, мы видели текст по умолчанию в отладчике, который отображал тип условия. Это сообщение об ошибке по умолчанию в отладчике может быть настроено по желанию программиста с помощью функции :report .
Сообщение по умолчанию в отладчике:
Condition COMMON-LISP-USER::MY-DIVISION-BY-ZERO was signaled.
[Condition of type MY-DIVISION-BY-ZERO]
мы можем написать функцию :report в объявлении условия, чтобы указать сообщение, которое будет отображаться в отладчике при возникновении условия.
Пример:
(define-condition my-division-by-zero (error)
((dividend :initarg :dividend
:initform nil
:accessor dividend));; the :report is the message into the debugger:
(:report (lambda (condition stream)
(format stream “You were going to divide ~a by zero.~&” (dividend condition)))))
Сообщение в отладчике:
You were going to divide 3 by zero.
[Condition of type MY-DIVISION-BY-ZERO]
Иерархия условий:
Иерархия различных условий изображена ниже:
Ниже приведен список приоритетов класса простых ошибок:
- простая ошибка
- простое условие
- ошибка
- тяжелое состояние
- условие
- т
Ниже приведен список приоритетов классов простых предупреждений:
- простое предупреждение
- простое условие
- предупреждение
- условие
- т