Exception handling in Lisp, as seen from C++, Part 6

Now, we’ll streamline the code a bit.

Recall that if a handler function returns, rather than making a non-local exit through a restart or by signaling another condition, then the condition that caused the handler to be called in the first place is deemed not to have been handled, and we continue up the call stack looking for handlers to handle the condition.  This means that we can make our number-fixing function simply exit if it fails to find a valid fix, and invoke the restart if it succeeds.  In that case, we’ll continue up the call stack to the general-purpose “continue” handler at the top.

If we do this, then our unreadable number condition no longer needs a field to indicate whether or not it has been successfully corrected.  The only way code can proceed past the signal function call is if the restart is invoked, which will only happen if there is a corrected value available.

The result is that we have an analysis function that can correct bad numbers if necessary.  All that is required is that an enclosing context binds a handler function to the not-readable-number condition, and that that handler function, if it successfully supplies a corrected number, fills in the slot in the condition and calls the number-was-fixed restart.  There might be several different functions available for this, and the caller might choose how to do it based on some enclosing context.  For instance, it might be that another function uses a recognized words list from French, or Chinese.  The analyse-stream function just knows that somebody might fix the number, and indicate that by calling the number-was-fixed restart.

Finally, what about arithmetic errors?  The ratio of two numbers can give an arithmetic error in at least two ways under Lisp.  The denominator of the fraction could be zero, or the result of the division could lead to a floating-point overflow or underflow.  If this happens, we want our code to produce a message, but then continue running with the next pair of numbers, rather than unwinding the stack to the toplevel and exiting.  We do this by wrapping the arithmetic operation in its own handler-case, so that conditions of type arithmetic-error (or classes derived from it) are handled locally, and not passed back up the call stack.

Here’s how the code looks now:
 

;; 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)
   (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))
            (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))
            (setf num2 (get-corrected-value msg))))

        (handler-case
            (format t 
                    "The ratio of ~A to ~A is ~A~%" 
                    num1 num2 (/ num1 num2))
          (arithmetic-error (c)
            (format t "An arithmetic error occured:~%~A~%" c)
            (format t "Continuing forward.~%"))))))

;; 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-corrected-value c) candidate)
          (invoke-restart 'number-was-fixed)))

      ;; 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-corrected-value c) (cdr match))
          (invoke-restart 'number-was-fixed))))))

Before leaving the subject of exceptions, there are a few more things to point out.

Whenever programming with exceptions, the programmer should be aware that certain function calls may invoke an exception, that could unwind through the function you’re writing to reach an exception handler above.  In C++, if you designed a function to write out data to disc, it might first open the file, then call some functions to generate the output, then finally close the file.  If one of those output-generating functions throws an exception which is caught at a higher level, the result will be a dangling open file with no way to recover its handle to close it.  Problems of this sort are generally handled in one of two ways:

  1. The programmer ensures that any cleanup work that might be necessary is located inside the destructor of an auto (stack-allocated) object.  When the stack is unwound, auto objects have their destructors called, and cleanup occurs there.
  2. The programmer surrounds the block that might throw an exception in a try/catch block with a default catch, which does the appropriate cleanups and then rethrows.

An example of the second case might be this:
 

{
    FILE *ofile = ::fopen(pathname, "w");

    try {
        write_out_some_data(ofile);
    } catch (...) {
        ::fclose(ofile);
        throw;
    }

    ::fclose(ofile);
}

Most C++ programmers, though, will probably prefer the first approach, to clean up in destructors.  Instead of using the fopen() and fclose() functions, they would use iostream classes that close their files in the destructor, so that the cleanup happens automatically even if an exception is thrown from a called function and caught from a calling function.

In Lisp, though, objects don’t have destructors.  Lisp objects also don’t have the properties of auto variables, they aren’t built on the stack of the function and destroyed when the function exits.  Lisp objects are destroyed by garbage collection once all references to them are known to be gone, so they can persist long after the function that allocated them has exited, or they can vanish partway through execution of the function.  How, then, do Lisp programmers use exceptions safely?  In Lisp, the usual technique is a variation of method 2 above.  A form called unwind-protect is used to clean up.  Note, however, that unwind-protect is not exactly like the C++ model above.  The unwind-protect form is always executed, whether an exception is thrown or not.  What it does is to say that, no matter how the protected form is exited, the cleanup code runs before the stack is allowed to unwind past that point.  In the tiny C++ sample above, we have two invocations of ::fclose().  One in the exception handler, and one in the normal execution case.  In Lisp, the code would appear only once, as the unwind-protect cleanup forms execute in both cases.  An example might be:
 

(defun write-out (pathname)
  (let ((output-stream (open-output-file pathname)))
    (unwind-protect
         (progn
           (write-output-1 output-stream)
           (write-output-2 output-stream))
      (close-output-file output-stream))))

It is a common Lisp construct to create functions or macros with a name like (with-*) to indicate that there is an unwind-protect on a resource within.  For instance, in the code I’ve been presenting in this series on exceptions, I’ve been using a method (with-open-file).  This can be implemented as a macro using unwind-protect and the (open) and (close) functions of lisp.  The with-open-file method ensures that the file is always cleanly closed before the form exits, whether it exits by normal flow or by a non-local return through a catch or restart.

2 thoughts on “Exception handling in Lisp, as seen from C++, Part 6

  1. You said “The only way code can proceed past the signal function call is if the restart is invoked, which will only happen if there is a corrected value available.”

    But in your previous article, you said that signal will have no effect if there is no bound handler.

    So which is it?

    1. Stuart,
      Maybe my description could have been clearer. As you point out, in the previous article I assert that, in the absence of a bound handler for a signal-ed condition, execution continues as if the signal did not do anything. However, there is a handler bound to the generic ‘condition object, so the signal will always do something.
      The change that I’ve put in this code is to move the restart invocation so that it only occurs when the number was successfully corrected. If the function failed to correct the number, it does not invoke the restart, so execution runs through to the end of the function. When that happens, because there was no non-local exit, the signaled condition is deemed not to have been handled, and the condition continues up the stack looking for another handler. It finds such a handler, in the general non-file-error ‘condition handler in condition-demo, so execution resumes there, and the rest of the input stream is ignored. So, execution, in this specific case, cannot continue after the signal call unless it was handled, because of this enclosing condition handler.
      To put it in C++ terminology, the signal call is ignored (barring side-effects in its invocation), only when there is no enclosing handler that can catch it. The handler on ‘condition is a catch-all that will always match that signaled object. Since this general exception handler exits the program, execution cannot continue past the signal call unless it was resumed with the non-local exit in the ‘not-readable-number handler.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

反垃圾邮件 / Anti-spam question * Time limit is exhausted. Please reload CAPTCHA.