Now, we’re ready to set up the code to correct and continue from certain errors. This isn’t going to be the final form of this example, I’m putting in unnecessary extra code in hopes that it aids in clarity. I’ll make some final changes to streamline it a bit in a later post.
;; Some examples of using conditions in Lisp code. ;; (declaim (optimize (debug 3) (safety 3))) (define-condition not-readable-number (error) ((object-seen :initarg :object-seen :initform nil :reader get-object-seen) (was-corrected :initform nil :accessor get-was-corrected) (corrected-value :initform nil :accessor get-corrected-value)) (:documentation "The condition that will be used to communicate exceptions when trying to read numbers.") (:report (lambda (c s) (format s "The value \"~A\" could not be interpreted as a number." (get-object-seen c))))) (defun condition-demo (pathname &key fix-bad-numbers) (format t "Starting to analyse file ~A~%" pathname) (handler-case (with-open-file (s pathname :direction :input) (if fix-bad-numbers (handler-bind ((not-readable-number #'fix-number-technique-1)) (analyse-stream s)) (analyse-stream s))) (file-error (c) (format t "A file error was encountered while trying to analyse ~A.~%The error returned was:~%~A~%" pathname c)) (end-of-file () (format t "Successfully reached the end of the file.~%")) (condition (c) (format t "A non-file error was encountered while trying to analyse ~A.~%The error returned was:~%~A~%" pathname c)))) ;; Goes through the stream, dividing entries one by the next (defun analyse-stream (stream) (do () (nil) (let ((num1 (get-next-number stream)) (num2 (get-next-number stream))) (unless (numberp num1) (let ((msg (make-condition 'not-readable-number :object-seen num1))) (with-simple-restart (number-was-fixed "") (signal msg)) (unless (get-was-corrected msg) (error msg)) (setf num1 (get-corrected-value msg)))) (unless (numberp num2) (let ((msg (make-condition 'not-readable-number :object-seen num2))) (with-simple-restart (number-was-fixed "") (signal msg)) (unless (get-was-corrected msg) (error msg)) (setf num2 (get-corrected-value msg)))) (format t "The ratio of ~A to ~A is ~A~%" num1 num2 (/ num1 num2))))) ;; Returns the next object in the stream (defun get-next-number (stream) (read stream)) (defparameter *recognized-words* '(("zero" . 0) ("one" . 1) ("two" . 2) ("three" . 3) ("four" . 4) ("five" . 5) ("six" . 6) ("seven" . 7) ("eight" . 8) ("nine" . 9) ("ten" . 10) ("eleven" . 11) ("twelve" . 12) ("thirteen" . 13))) ;; Try to take a string or symbol representation and convert it to a ;; number (defun fix-number-technique-1 (c) (let ((bad-object (get-object-seen c)) string-rep) (when (symbolp bad-object) (setf string-rep (symbol-name bad-object))) (when (stringp bad-object) (setf string-rep (copy-seq bad-object))) (when string-rep ;; Maybe it's just a number in double-quotes. Let's ask if we ;; can read a number from that. (let ((candidate (read-from-string string-rep nil nil))) (when (numberp candidate) (setf (get-was-corrected c) t) (setf (get-corrected-value c) candidate) (invoke-restart 'number-was-fixed) (return-from fix-number-technique-1))) ;; If we get here, it wasn't that. Maybe it's one of our ;; recognized words. (let ((match (assoc string-rep *recognized-words* :test 'string-equal))) (when match (setf (get-was-corrected c) t) (setf (get-corrected-value c) (cdr match)))))) (invoke-restart 'number-was-fixed))
Let’s look over this code in order and see what it does. We’ve defined a condition, ‘not-readable-number’, that we will signal (i.e. throw in C++ parlance) when we encounter an unreadable number. In the previous post, we saw the output generated when that condition was signaled and caught in the generic catch-all in the condition-demo function.
Starting with condition-demo. We print an informational message, then set up the outer condition handlers with handler-case. The handler-case function sets up for mandatory stack unwinding before the conditions are handled, so it looks like the C++ catch statement. There, we’ve set up three handlers. They are file-error, end-of-file, and condition. As mentioned earlier, all conditions must ultimately derive from the condition class. Note that end-of-file is not derived from the file-error class, so the file-error condition handler, which appears earlier in the list than end-of-file, will not itself collect end-of-file conditions thus depriving the end-of-file handler of its conditions. The final entry, condition, catches any conditions not caught by earlier entries in the list of handlers.
Underneath the handler-case, we check to see if the invocation has asked to attempt to fix bad numbers. If so, we use handler-bind to attach the not-readable-number condition to a function, fix-number-technique-1. In C++ terms, the handler-bind function sets up something similar to an interrupt handler, but one that runs in the context of the location where the interrupt was signaled, rather than unwinding the stack as happens in a throw or handler-case. Then, we continue as before, calling analyse-stream.
In analyse-stream, we read the number, but then check to see if the thing we read in was really a number, and not some other lisp object. If it was not a number, we construct a not-readable-number condition and signal it underneath a ‘number-was-fixed’ restart. If we did not bind a handler to that restart, because the caller did not ask to fix bad numbers, then this signaled condition is caught by the final “condition” handler in the top handler-case. The stack unwinds, a message is printed, and the function exits. However, if the handler was bound to the condition, a function is called with a single argument, the condition object that was signaled. If this handler function invokes a restart, flow continues after the restart form. If it does not invoke a restart, but instead returns normally, then it is deemed not to have handled the condition, and the next outer condition handler gets to attempt to handle it. In order for the condition to be deemed handled, it must make a non-local transfer of control, either by invoking a restart or by signaling a new condition.
The fix-number-technique-1 function is there, for clarity. This particular one can interpret as numbers the bare words zero, ZERO, one, One, TwO, etc. It also recognizes those as strings, enclosed in double-quotes. Finally, it can recognize numeric representation of numbers that have been accidentally enclosed in double-quotes causing them to look like strings to the reader. If the function can find a replacement number, it modifies the condition object by filling in the corrected number, and setting a field to notify the user that a corrected number is available. It then always calls the restart, which makes flow continue just after the with-simple-restart form. There, we examine the condition object, and if it contains a corrected value, we use it, otherwise we throw the condition again, this time as an error. The significant difference between signal and error is that, when there is no handler available for the condition in question, execution continues after a signal function, but does not continue through an error function.
This last point is important to emphasize. In C++, you expect that if you throw an exception without a handler available for it, the program will ABEND with a message about an unhandled exception. In Lisp, if you use signal to raise a condition, and there is no enclosing handler available for it, execution continues after the signal. This can surprise those who aren’t expecting it. If the aim is to ensure that execution will not continue past the form, the condition should be raised with error rather than with signal.
In the next post, we’ll clean this up a bit, making use of some subtler behaviours we’ve skipped for now.