190 likes | 313 Views
Error Handling. Common Lisp has the throw/catch statements so that you can simulate exception handling as seen in languages like Java But CL goes far beyond this by providing a number of alternative mechanisms
E N D
Error Handling • Common Lisp has the throw/catch statements so that you can simulate exception handling as seen in languages like Java • But CL goes far beyond this by providing a number of alternative mechanisms • you can use throw/catch or return-from but include an unwind-protect so that specific operations happen no matter if you branch or not • you can specify an error that terminates the execution of the program • you can specify an error that drops you in the debugger • you can specify an error that drops you in the debugger and includes a specialized restart • you can define conditions (same as Exceptions in Java) and then there are several ways to handle conditions including specialized restarts • Why all the different approaches? • in part, the CL philosophy is one of giving you as many tools as possible • however, CL differs from most languages in that you have the interactive environment, so using the debugger becomes another weapon in your programming arsenal • the idea of branching to an exception handler, as performed in Java, is both limiting and may lead to continuation problems, CL tries to do better
Catch/Throw vs. Return-from • Throw can be used to transfer control to the most recent catch statement that has a matching symbol • this allows you to escape from a potentially erroneous situation but has two problems • throw unwinds the stack to the point where the catch is found • finding the proper catch is done dynamically, so there is no way to necessarily know where control will transfer when writing the code • return-from is preferred because it can be used • to terminate the function and return control to the calling unit • In either case, any remaining instructions in the function or block are ignored • we may want to force those instructions to execute (such as a close statement) and so we would use unwind-protect to surround the code with the error and the clean-up code
Unwind Protect • Unwind-protect executes the next instruction and then guarantees that code that follows the next instruction inside the unwind-protect will execute no matter if the instruction tries to leave the block through some branch • such as through a throw or return-from • typically the next instruction will be a function call or a progn statement that permits multiple operations • This will be helpful when some concluding statement(s) should be assured of running, such as closing a file • Form: (unwind-protect (progn ;; statements go here ) (;; statements here will automatically be executed) (;; and more statements will be executed) (;; and so on))
(defun fn-a ( ) (catch 'fn-a (print 'before-fn-b-call) (fn-b) (print 'after-fn-b-call))) (defun fn-b ( ) (print 'before-fn-c-call) (fn-c) (print 'after-fn-c-call)) (defun fn-c ( ) (print 'before-throw) (throw 'fn-a 'done) (print 'after-throw)) If we call fn-a, we get: BEFORE-FN-B-CALL BEFORE-FN-C-CALL BEFORE-THROW DONE Example If we change fn-b to be (defun fn-b ( ) (unwind-protect (progn (print 'before-fn-c-call) (fn-c)) (print 'after-fn-c-call))) We get BEFORE-FN-B-CALL BEFORE-FN-C-CALL BEFORE-THROW AFTER-FN-C-CALL DONE
Another Example We will exit this loop prematurely if either there is an error reading the file or the file is not found Status gets temp, whatever the last thing was that was read in – it should be nil Status is then used in the protect portion (defun compute-average (filename) (let (infile (count 0) (sum 0) avg (status t)) (unwind-protect (progn (setf infile (open filename)) (setf status (do ((temp (read infile nil nil) (read infile nil nil))) ((null temp) temp) (setf count (+ count 1)) (setf sum (+ sum temp))))) (if (null status) (if (> count 0) (setf avg (/ sum count)) (setf avg (format nil "File ~A empty" filename))) (setf avg (format nil "Error in File ~A occurred" filename))) (close infile)) avg))
Error • The error instruction causes a program to terminate at that point and return an error message • you can provide a tailored error message for the given circumstance • your output statement is like a format in that it can contain various ~ arguments followed by variables • since this instruction terminates the program, a continuation/restart is not possible – you are dropped in the debugger with abort options only • Examples: (reciprocal 0) Error: Cannot divide by 0 1 (abort) Return to level 0. 2 Return to top loop level 0. (defun reciprocal (x) (if (= x 0) (error "Cannot divide by 0") (/ 1 x))) (defun count0 (lis) (let ((num 0)) (dolist (a lis) (if (not (numberp a)) (error "List contains non-numeric item ~A" a) (if (= a 0) (setf num (+ num 1))))) num))
cerror is a superior version which also drops you in the debugger, but unlike error (or break), cerror gives you the ability to specify your own continuation message (if desired) so that you can restart the program from this point note that the restart just goes on to the next instruction Form: (cerror “restart message” “error message” params) the parameters refer to any ~var items in either message continuation must be part of the code, that is, you cannot provide a list of possible restarts, instead it just continues Cerror CL-USER 86 > (count0 '(1 0 a 3 0 4)) Error: List contains non-numeric item A 1 (continue) Skip A and continue 2 (abort) Return to level 0. 3 Return to top loop level 0. CL-USER 87 : 1 > :c 1 2 (defun count0 (lis) (let ((num 0)) (dolist (a lis) (if (not (numberp a)) (cerror "Skip ~A and continue" "List contains non-numeric item ~A" a) (if (= a 0) (setf num (+ num 1))))) num))
Assertions • An assertion is similar to cerror except that you can permit the user to submit replacement values • Form: (assert test (value(s)) &optional “message”) • the message is like what we had in error or cerror, without it, the message is not necessarily useful for a non-programmer • If the test fails, then CL drops into the debugger and the user has a chance to enter a new value (values) as specified in the list • examples: (defun reciprocal (x) (assert (not (= 0 x)) (x) “Cannot divide by 0, replace x”) (/ 1 x)) (defun reciprocal (x) (assert (not (= 0 x)) (x)) (/ 1 x)) CL-USER 51 > (reciprocal 0) Error: The assertion (NOT (= 0 X)) failed. 1 (continue) Retry assertion with new value for X. 2 (abort) Return to level 0. 3 Return to top loop level 0. The list can contain multiple variables in which case the user is asked to input new values for all of them
Assertion Example • Consider as an example that we want to take two values and ensure that they are non-negative numbers • Here is a function to accomplish this: (defun test-values (a b) (assert (and (numberp a) (numberp b)) (a b)) (assert (and (>= a 0) (>= b 0)) (a b)) (list a b)) (test-values -5 'a) Error: The assertion (AND (NUMBERP A) (NUMBERP B)) failed. 1 (continue) Retry assertion with new values for A, B. 2 (abort) Return to level 0. 3 Return to top loop level 0. CL-USER 3 : 1 > :c 1 Enter a form to be evaluated: [pop-up window asks to replace a] -5 Enter a form to be evaluated: [pop-up window asks to replace b] -4 Error: The assertion (AND (>= A 0) (>= B 0)) failed. 1 (continue) Retry assertion with new values for A, B. 2 (abort) Return to level 0. 3 Return to top loop level 0. CL-USER 4 : 1 > :c 1 Enter a form to be evaluated: [pop-up window for a] 5 Enter a form to be evaluated: [pop-up window for b] 4 (5 4)
Defining Conditions • As in Java, conditions in CL are objects • You define a class for a specific type of condition • Condition objects include slots like with objects • slots can include arguments like initarg, initform, accessor, etc • Conditions can inherit from other conditions • Aside from slots, a condition can also include a • :report clause which will be used as output when the condition is raised • :documentation clause (define-condition my-error (condition) ((error-type :initarg :error-type :initform "Unknown" :accessor error-type) (error-location :initarg :error-location :initform "Unknown" :accessor error-location)) (:report (lambda (condition stream) (format stream "Condition ~A arose in ~A location" (error-type condition) (error-location condition))))) (error 'foo :error-type "Mistake") would generate the message Condition Mistake arose in Unknown location
Handling the Condition • As seen with the previous example, the condition can be raised explicitly through an error message • this drops the program into the debugger, but the user’s only restart choice is to abort the program • The condition can also be raised through cerror, where the programmer can specify what restarting will do • here is a revised reciprocal function that will raise the my-error condition and still let the user continue (defun reciprocal (x) (when (= x 0) (cerror "Use 1 in place of 0 for input" 'my-error :error-type "Division by zero" :error-location "in function reciprocal") (setf x 1)) (/ 1 x)) CL-USER 89 > (reciprocal 0) Condition Division by zero arose in in function reciprocal location 1 (continue) Use 1 in place of 0 for input 2 (abort) Return to level 0. 3 Return to top loop level 0. CL-USER 90 : 1 > :c 1 1
More Elaborate Continuation • The previous example forced the user to accept x = 1 if they wanted to continue • Can we improve on this? • The cerror command only allows for 1 continuation, but we can be more elaborate in what happens with that continuation code (defun reciprocal (x) (when (= x 0) (cerror "Input a new value in place of 0" 'my-error :error-type "Division by zero" :error-location "in function reciprocal") (format t "~%Enter new value: ") (setf x (read))) (/ 1 x)) CL-USER 57: > (reciprocal 0) Condition Division by zero arose in in function reciprocal location 1 (continue) Input a new value in place of 0 2 (abort) Return to level 0. 3 Return to top loop level 0. CL-USER 58 : 1 > :c 1 Enter new value: 5 1/5
Condition Handlers • The standard form of a condition handler is through the handler-bind macro • we are used to seeing try-catch blocks in something of this form in Java (C++ is similar): • { … try {…} catch(ExceptionType e) {…} • in Common Lisp, the catch is made through handler-bind, and the try block is embedded into this block of code • (handler-bind ((condition-name #’(lambda (x) ;; code goes here to handle the condition))) ;; code goes here equivalent to the try block) • the code after the ))) will can contain signal statements that can signal the given condition, in which case control is transferred to that chunk of code • to signal a condition: (signal ’condition-name) • the signal instruction can also include accessor or initarg arguments such as (signal ’my-error :error-type “Help I’m lossed” :error-location “my-function”)
(defun my-divider (a b) (if (and (= a 0) (= b 0)) (signal 'my-error :error-type "0 / 0 Does not exist" :error-location "my-divider") (if (= b 0) (signal 'my-error :error-type "Division by zero" :error-location "my-divider") (/ a b)))) (defun safe-divider (a b) (handler-bind ((my-error #'(lambda (x) (format t "~A. How should we proceed?~%1. Return 0.~%2. Input new value for denominator.~%3. Input new values for numerator and denominator.~%Select: " x) (let ((temp (read))) (cond ((= temp 1) (return-from safe-divider 0)) ((= temp 2) (format t "Enter new denominator: ") (setf b (read))(return-from safe-divider (my-divider a b))) ((= temp 3) (format t "Enter new numerator: ") (setf a (read)) (format t "Enter new denominator: ") (setf b (read)) (return-from safe-divider (my-divider a b))) (t (return-from safe-divider nil))))))) (my-divider a b))) Example
How This Differs From Java • In a language like Java, an exception handler • contains the mechanism for handling the exception • its location dictates continuation • for instance, consider that method handle1 has a try/catch block that can catch Exception Foo • handle1 calls method handle2, which throws Foo • handle2 calls error3 which throws Foo • if a Foo exception arises in handle2, handle2 transfers control back to handle1 to solve the problem, handle1 then continues – this is what we might expect • if a Foo exception arises in error3 then error3 throws to handle2 which throws to handle1 • should handle2 have been aborted because of an exception in error3? • in Common Lisp, since the handler was part of the handler-bind, restart will (or can) occur in the instruction after the call that produced the signal • in our previous example, this was not the case because of the use of return-from statements
Handler-Case • Handler-bind allows you to handle a situation without removing items from the stack • But unfortunately, it is somewhat complex to use • Handler-case is a simpler condition handler • Form: • (handler-case (;; code here that might cause a condition) (condition1 (param) ;;condition1 handler) (condition2 (param) ;; condition2 handler) …) • Example: Here assume that a, b and c are variables equal to different values If a or c are 0, then control transfers to the error handler (handler-case (progn (print (/ b a)) (print (/ b c))) (division-by-zero (x) (print 'oops))) Notice that conditions can be built-in or user-defined
Restart Handlers • Handler-case unwinds the stack to the point where the handler-case is found • so this is of less use than handler-bind • Restart-case is a version of handler-case does not try to handle the condition but instead • drops you in the debugger • however, the restart-case allows you to specify restart options that the user can select between • since we generally want to shelter a user from the debugger, we may not want to handle conditions in this way • A variation is invoke-restart • when provided with a function name as a symbol, it will find the most recently bound version of that restart (that is, the one nearest on the run-time stack) and resume execution • we are going to skip the details of the restart forms since they get more and more complicated!
And Finally… • Using signal outside of a handler macro will result in the condition being raised but not handled, so you will just get a nil return value • Error and Cerror call signal for you by the way • Warn is like error or cerror in that it signals the condition, but rather than being dropped in the debugger, if signal returns then execution continues from that point • this allows you to perform the operation of a condition handler without having to worry about restarts • A built-in restart is called continue, which merely resumes (this would be equivalent to using warn instead of error or cerror) • the Practical Common Lisp on-line text has more detail, read it if you are interested!