Now, just a bit more exposition before we get to the novel exception behaviour in Lisp. Our example can now read in a file, print out the appropriate ratios, and give useful diagnostics in the event that the file is unreadable. It also exits cleanly when it reaches the end of the input file. But it’s still brittle. The input file might not consist only of numbers, in which case the math operation will fail.
Knowing that a handler might be able to correct bad input, we write our changes accounting for that possibility. We’ll create a new condition type, one that allows manipulation of its content. Here’s the next version of the code:
;; 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) (format t "Starting to analyse file ~A~%" pathname) (handler-case (with-open-file (s pathname :direction :input) (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))) (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))) (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))
Now, if we run the condition-demo function on this file:
1 2
4.1 9
zero one
we get the following output:
Starting to analyse file condition-input.txt
The ratio of 1 to 2 is 1/2
The ratio of 4.1 to 9 is 0.45555556
A non-file error was encountered while trying to analyse condition-input.txt.
The error returned was:
The value “ZERO” could not be interpreted as a number.
As we see, the value ‘zero’ was not interpreted as a number. The condition was built and signaled, and the toplevel handler caught it, printed out its diagnostic information, and the program exited.
But maybe the programmer wants ‘zero’ to be recognised in some contexts. Or maybe somebody, when creating the input file, put the numbers in double-quotes, so that it looked like this:
“1” “2”
“4.1” “9”
“zero” “one”
Well, we can set up a restart for this. Maybe it is only valid on certain code paths, for certain filenames, but we can decide at function invocation time whether to try to fix up these values, or allow them to signal. The code to do that will require some lengthy explanation, so it will come in the next post.