This paper appears in
Advances in Exception Handling Techniques,
edited by A. Romanovsky, C. Dony, J.L. Knudsen, and A. Tripathi.
This book, published in 2001, is part of
Lecture Notes in Computer Science, Volume 2022,
published by Springer.
This paper was originally written as an HTML document, exactly as shown
below. Any final reformatting that was done for hardcopy publication
in LaTeX may have been lost in this version.
Any new text that has been added for this annotated version
appears bracketed and in color green; such text is intended to
help clarify the historical context as time passes.
--Kent Pitman, 28-Feb-2002.
Annotated original document follows.
Click here for an index
of other titles by Kent Pitman.
Copyright © 2001, Kent M. Pitman. All Rights Reserved.
[ Web version Copyright © 2002, Kent M. Pitman. All Rights Reserved. ]
The Lisp family of languages has long been a rich source of ideas and inspiration in the area of error handling. Here we will survey some of the abstract concepts and terminology, as well as some specific language constructs that Lisp has contributed.
Although there are numerous dialects of Lisp, several of which offer the modern concepts and capabilities described herein, we will focus specifically on Common Lisp as described in the ANSI standard, X3.226-1994 [X3J13 1994].
The Common Lisp community typically prefers to speak about its condition system rather than its error system to emphasize that there are not just fatal but also non-fatal situations in which the capabilities provided by this system are useful.
Not all exceptional situations
are represented, or sometimes even detected.
A situation that CONDITION
is used to represent such a situation.
A condition is said to be the generalization
of an error. Correspondingly, within the language
the class CONDITION
is a superclass of
another class ERROR
, which represents situations that
would be fatal if not appropriately managed.
So the set of all situations involving conditions includes not only descriptions of outright erroneous situations but also descriptions of situations that are merely unusual or questionable. Even in the case of non-error conditions, the programmer may, as a matter of expressive freedom, choose to use the same capabilities and protocols as would be used for "real" error handling.
To properly understand condition handling, it is critical to understand that it is primarily about protocol, rather than mere computational ability. The establishment of protocols is a sort of before-the-fact hedge against the "prisoner's dilemma"; that is, it creates an obvious way for two people who are not directly communicating to structure independently developed code so that it works in a manner that remains coherent when such code is later combined.
For example, if we want to write a program that searches a list for an object, returning true if the object is present and false otherwise, we could write the following, but would ordinarily not:
(defun search-list (goal-item list-to-search) (handler-case ;; Main body (progn (dolist (item list-to-search) (when (eq item goal-item) (return-from search-list t))) ;; Search has failed, signal an error. (error 'search-failure :item goal-item)) ;; Upon error, just return false. (error () nil)))
The reason not to write this is not that it will not work, but that it involves undue amounts of unneeded mechanism. The language already contains simpler ways of expressing transfer of control from point A to point B in a program where the same programmer, acting in the same role, controls the code at both points. For example, the following would suffice:
(defun search-list (goal-item list-to-search) (dolist (item list-to-search) (when (eq item goal-item) (return-from search-list t))) nil)
(defun search-list (goal-item list-to-search) (dolist (item list-to-search) (when (eq item goal-item) (return-from search-list t))) (error 'search-failure :item goal-item))
Protocol is simply not needed when communicating lexically among parts of a program that were written as a unit and that are not called by other programs that are either logically separated or, at minimum, logically separable. The distinction is subjective, but it is nevertheless important.
Before beginning to look in detail at the features of Common Lisp's condition system, it's useful to observe that computer languages, like human languages, evolve over time both to accommodate current needs and to repair problems observed in past experience. The interesting features of the Common Lisp condition system were not suddenly designed one day as a spontaneous creative act, but rather grew from many years of prior experience in other languages and systems.
The PL/I language, designed at IBM in the early 1960's, included an extensive condition mechanism which had an extensible set of named signals and dynamic handlers running in the dynamic context of the signal. Since PL/I included "downward" lexical closures these handlers had access to the erring environment, and sometimes to the details of the error.
Multics, whose official language was PL/I, adapted and extended
this, including the addition of any_other
,
cleanup
, and unclaimed_signal
,
the passing of machine conditions and other arbitrary data,
cross-protection-domain signals, and the use of this mechanism
to manage multiple suspended environments on one stack.
For security reasons, Multics had an elaborate and rigidly enforced separation of various kinds of code into protected "rings". The Multics operating system relied on their expanded signaling system for several critical system functions, mostly in the user ring, although cross-ring signals were possible in certain cases involving paging or memory errors.
Historically, Multics was an "early" environment, but it was not, by any analysis, a toy. Its condition system was carefully designed, heavily tested, and had many important characteristics that influenced the later design of Lisp:
It separated the notion of condition signaling from condition handling.
It offered the possibility, through program_interrupt
conditions, of resuming an erring computation, presumably after
correcting the offending situation.
It began to deal with the
mediation of signaling and handling through not only a
condition type but also a set of associated data
appropriate to that type: By using a system-defined operator called
"signal_
" instead of the normal PL/I "signal
",
a data block could be associated with the condition being signaled,
a crude precursor to the idea of object-oriented
condition descriptions that followed later in Lisp.
It began to deal with the notion of default handling, through the use of
the unclaimed_signal
pseudocondition.
In the early 1980's, some former users of the Multics system, including Daniel L. Weinreb, Bernard Greenberg and David Moon, harvested the good ideas of the Multics PL/I condition system and recast them into Zetalisp, a dialect running on the Symbolics Lisp Machines. This redesign was called simply the "New Error System" (or sometimes just "NES").
Key elements of the New Error System (NES) in Symbolics Zetalisp were:
NES had an object-oriented nature.
NES clearly separated the treatment of exceptional situations into three logically distinct programming activities:
NES provided for erring programs to be resumed either interactively or non-interactively, separating information about prompting for replacement data from the conduits that would carry such data so that programs wishing to do mechanical recovery could bypass the prompting but use the rest of the recovery pipeline.
NES directly and strongly influenced the design of the Common Lisp condition system. In fact, one initial concern voiced by a number of vendors was that they were fearful that somehow the ideas of the condition system, being taken from the Lisp Machine environment, would not perform well on standard hardware. It took several months of discussion, and the availability of a free public implementation of the ideas, before these fears were calmed and the Common Lisp community was able to adopt them. Even so, numerous small changes and a few major changes were made in the process.
In both Zetalisp and Common Lisp, handlers are functions that are called in the dynamic context of the signaling operation. No stack unwinding has yet occurred when the handlers are called. Potential handlers are tried in order until one decides to handle the condition. Probably the most conspicuous change between NES and the Common Lisp Condition System was the choice of how a handler function communicated its decision to elect a specific mode of recovery for the condition being signaled.
NES used a passive recovery mechanism. That is, in
all cases, the handler would return one or more values. The nature of
the return values would determine which recovery mode (called a
"proceed type" in Zetalisp) was to be used. If NIL
was returned,
the handler had elected no proceed type, and the next handler was tried.
Otherwise, the handler must return at least one value, a keyword designating
the proceed type, and, optionally, additional values which were data
appropriate to that manner of proceeding.
Common Lisp uses an active recovery mechanism. That is, any handler that wishes to designate a recovery mechanism (called a "restart" in Common Lisp) must imperatively transfer control to that restart. If the handler does not transfer control, that is, if the handler returns normally, any returned values are ignored and the handler is said to have "declined" (i.e., elected no restart), and the next handler is tried.
I am commonly credited with the "creation" of the Common Lisp Condition System, although I hope to show through this paper that my role in the design was largely to take the ideas of others and carefully transplant them to Common Lisp. In doing this, I relied on my personal experiences to guide me, and many of my formative experiences came from my work with Maclisp [Pitman 1983], which originated at MIT's Project MAC (later renamed to be the Laboratory for Computer Science), and which ran on the Digital Equipment Corporation (DEC) PDP10, DEC TOPS20 and Honeywell Multics systems.
Maclisp, had a relatively primitive error system, which I had used extensively. At the time I came to the Lisp Machine's NES, I did not know what I was looking for in an error system, but I knew, based on my experience with Maclisp, what I was not looking for. So what impressed me initially about NES was that it had fixed many of the design misfeatures that I had seen in Maclisp.
One important bit of background on Maclisp, at least on the PDP10
implementation that I used, was that it had no STRING
datatype. In almost all cases where one might expect strings to be
used, interned symbols were used instead. Symbols containing characters
that might otherwise confuse the tokenizer were bounded on either end
by a vertical bar (|). Also, since symbols would normally name
variables, they generally had to be quoted with a leading single quote
(') to protect them from the Lisp evaluation mechanism and allow them
to be used as pseudostrings.
'|This is a quoted Maclisp symbol.|
Maclisp had two forms of the function ERROR
. In the simple
and most widely used form, one merely called ERROR
with one argument, a
description of the error. Such errors would stop program execution with
no chance of recovery other than to transfer to the innermost
ERRSET
, the approximate Maclisp equivalent of Common Lisp's
IGNORE-ERRORS
.
(error '|YOU LOSE|)
It was possible, however, in a limited way, to specify the particular kind of error. There were about a dozen predefined kinds of errors that one could identify that did allow recovery. For example,
(error '|VARIABLE HAS NO VALUE| 'A 'UNBND-VRBL)
The "keyword" UNBND-VRBL
was a system-defined name that indicated
to the system that this was an error of kind "unbound variable". A specific
recovery strategy was permitted in this case. One could, either interactively
in a breakpoint or through the dynamic establishment of a handler for such
errors, provide a value for the variable. If that happened, the call to
ERROR
would then return a list of that value and the caller of
ERROR
was expected to pick up that value and use it.
This worked fine for the case where the programmer knew the kind of error
and was prepared to recover from it. But a strange situation occurred when one
knew the kind of error but was not prepared to recover. Sometimes one knew
one had an unbound variable, and wanted to call ERROR
,
but was not prepared
to recover. In this case, the programmer was forced to lie and to say that
it was an error of arbitrary type, using just the short form, to avoid
the misperception on the part of potential handlers that returning a recovery
value would be useful.
(error '|VARIABLE HAS NO VALUE| 'A)
One feature of the NES, which I personally found very attractive, was the notion that I could freely specify the class of error without regard to whether I was prepared to handle it in some particular way. The issue of how to handle the error was specified orthogonally.
In PDP10 Maclisp, error messages were historically all uppercase, since the system's primitive error messages were that way and many users found it aesthetically unpleasant to have some messages in mixed case while others were entirely uppercase. At some point, however, there was pressure to provide mixed case error messages. The decision made by the Maclisp maintainers of the time was not to yield to such pressure.
The problem was that many programs faced with an error message were testing it for object identity. For example:
(eq msg '|UNBOUND VARIABLE|)
Had we changed the case of all of the error messages in the Maclisp system to any other case, lower or mixed, these tests would have immediately begun to fail, breaking a lot of installed code and costing a lot of money to fix. The change would have been seen to be gratuitous.
The lesson from this for all of us in the Maclisp community, which became
magnified later when we confronted the broader community of international
users, was that the identity of an error should not be its name. That is,
had we to do it over again, we would not have used
|unbound variable|
nor |Unbound Variable|
as the identity of the error, but rather would have created objects
whose slots or methods were responsible for yielding the presented string,
but whose identity and nature was controlled orthogonally. This was
another thing that NES offered that drew me immediately to it.
At the time of the Common Lisp design, Scheme did not have an error system, and so its contribution to the dialog on condition systems was not that of contributing an operator or behavior. However, it still did have something to contribute: the useful term continuation. For our purposes here, it is sufficient to see a continuation as an actual or conceptual function that represents, in essence, one of possibly several "future worlds", any of which can be entered by electing to call its associated continuation.
This metaphor was of tremendous value to me socially in my efforts to gain acceptance of the condition system, because it allowed a convenient, terse explanation of what "restarts" were about in Common Lisp. Although Scheme continuations are, by tradition, typically passed by explicit data flow, this is not a requirement. And so I have often found myself thankful for the availability of a concept so that I could talk about the establishment of named restart points as "taking a continuation, labeling it with a tag, and storing it away on a shelf somewhere for possible later use."
Likewise, I found it useful in some circles to refer to some of the
concepts of reflective Lisps, such as Brian Smith's "3Lisp"
[Smith 1982], and later work inspired by it. I feel that the condition
system's ability to introspect (through operators such as
FIND-RESTART
) about what possible actions are pending,
without actually invoking those pending actions, is an important
reflective capability. Even though Common Lisp does not offer
general-purpose reflection, the ability to use this metaphor for
speaking about those aspects of the language that are usefully
described by it simplifies conversations.
Having now hopefully firmly established that the formative ideas in the Common Lisp Condition System did not all spring into existence with the language itself, and are really the legacy of the community using the continuum of languages of which Common Lisp is a part, we can now turn our attention to a survey of some of the important features that Common Lisp provides.
Traditionally, "error handling" has been largely a process of programs stopping and the only real question has been "how much of the program stops?" or "how far out do I throw?" It is against that backdrop that modern condition handling can be best understood.
The proper way to think about condition handling is this:
The process of programming is about saying what to do in every circumstance. In that regard, a computer has been sometimes characterized as a "relentless judge of incompleteness". When a program reaches a place where there are several possible next steps and the program is unwilling or incapable of choosing among them, the program has detected an exceptional situation.
The possible next steps are called restarts. Restarts are, effectively, named continuations.
The process of asking for help in resolving the problem of selecting among the possible next steps is called signaling.
The independently contributed pieces of code which are consulted during
the signaling process are called handlers. In
Common Lisp, these are functions contributed by the dynamic call chain
that are tried in order from innermost
(i.e., most specific) to outermost (i.e., most general).
Each handler is called with an argument
that is a description of the problem situation. The handler will transfer
control (by GO
, RETURN
or THROW
)
if it chooses to handle the problem described by its argument.
In describing condition handling, I tell the following story to help people visualize the need for its various parts:
Think of the process of signaling and handling as analogous to finding a fork in a road that you do not commonly travel. You don't know which way to go, so you make known your dilemma, that is, you signal a condition. Various sources of wisdom (handlers) present themselves, and you consult each, placing your trust in them because you have no special knowledge yourself of what to do. Not all sources of wisdom are experts on every topics, so some may decline to help before you find one that is confident of its advice. When an appropriately confident source of wisdom is found, it will act on your behalf. The situation has been handled.
In the case that the situation is not handled, the next action depends on
which operator was used to signal. The function signal
will
just return normally when a condition goes unhandled. The function
error
is like signal, but rather than return, it enters
the debugger. The Common Lisp debugger might allow
access to low-level debugging features such as examination of individual
storage locations, but it is not required to. Its
Note, too, that in some possible future world, knowledge representation may have advanced enough that handlers could, rather than act unconditionally on behalf of the signaler, merely return a representation of a set of potential actions accompanied by descriptive information respresenting motivations, consequences, and even qualitative representations of the goodness of each. Such information might be combined with, compared to, or confirmed by recommendations from other sources of wisdom in order to produce a better result. This is how consultation of sources of wisdom would probably work in the real world. Consider that even a doctor who is sure of what a patient needs will ask the patient's permission before acting. However, this last step of confirmation, which would allow more flexibility in the reasoning process, is not manifest in Common Lisp as of the time of writing this paper. It is an open area for future research.
Some of these issues are discussed in much greater detail in my 1990 conference paper [Pitman 1990].
It was mentioned earlier that the space of conditions that can be used in the Common Lisp Condition System is more general than the space of mere errors. Here are some examples.
The superclass of error
is serious-condition
.
This kind of condition is a subclass of condition
but is
serious enough that conditions of this kind should generally enter the
debugger if unhandled. Serious conditions, which the Zetalisp NES called
"debugger conditions", exist as a separately named concept
from "error conditions" to accommodate things that are
not semantic errors in a program, but are instead resource limitations and
other incidental accomodations to pragmatics.
Suppose one writes the following:
(ignore-errors (open "some.file"))
This will trap errors during the file open. However, what if a stack overflow occurs, not for reasons of infinite recursion, but merely because the call is nested very deeply in other code? The answer is that a stack overflow is considered a serious condition, but not an error. The above code is equivalent to:
(handler-case (open "some.file") (error (c) (values nil c)))
And since any condition representing a stack overflow is going to be a kind
of SERIOUS-CONDITION
, but not a kind of ERROR
,
the use of IGNORE-ERRORS
will succeed in trapping a file
error but not a stack overflow. If one wanted to catch serious conditions
as well, one would write instead:
(handler-case (open "some.file") (serious-condition (c) (values nil c)))
Some conditions are not at all serious. Such conditions might be handled, but there is an obvious default action in the case of their going unhandled.
Consider a program doing line-at-a-time output to a console. One might assume the screen to have infinite height, and the output might look like:
(defvar *line-number* 0) (defun show-lines (lines) (dolist (line lines) (show-line line)))
However, it might be useful to specify screen line height, and to have the console pause every so many lines for a human reader to confirm that it's ok to proceed. There are, of course, a number of ways such a facility could be programmed, but one possible such way is to use the condition system. For example,
(defvar *line-number* 0) (defvar *page-height* nil) (define-condition end-of-page (condition) ()) (defun show-lines (lines) (dolist (line lines) (show-line line) ; Oops. Omitted this call from original paper! (incf *line-number*) (when (and *page-height* (zerop (mod *line-number* *page-height*))) (restart-case (signal 'end-of-page) (continue ())))))
In the above, there is only one way to proceed. A restart named
CONTINUE
is offered as a way of imperatively selecting
this option (imperatively bypassing any other pending handlers),
but if the handler declines and the condition goes unhandled, the
same result will be achieved.
A similar kind of facility could be used to manage end of line handling. There, it's common to allow various modes, and so a corresponding set of restarts has to be established, which handlers would choose among. If no handler elected to handle the condition, however, no great harm would come. Here's an example of how that might look:
(defvar *line-length* nil) (define-condition end-of-line (condition) ()) (defun show-line (line) (let ((eol (or *line-length* -1)) (hpos 0)) (loop for pos below (length line) for ch = (char line pos) do (write-char ch) when (= hpos eol) do (restart-case (signal 'end-of-line) (wrap () (write-char #\Newline) (setq hpos 0)) (truncate () (return)) (continue () ;; just allow to continue )) else do (incf hpos))))
It has long been the case that Lisp offered the ability to dynamically
make the decision to transfer to a computed return point using the
special operator THROW
. However, without reflective
capabilities, there has not been the ability for a programmer to determine
if there was a pending CATCH
to which control could be thrown
other than relatively clumsy idioms such as the following:
(ignore-errors (throw 'some-tag 'some-value))
The problem with the above idiom is that while it "detects" the presence or absence of a pending tag, it only retains local control and the ability to reason about this knowledge in the case of the tag's non-existence. The price of detecting the tag's existence is transfer to that tag.
The Common Lisp Condition System adds a limited kind of reflective capability in the form of a new kind of catch point, called a restart, the presence or absence of which can be reasoned about without any attempt to actually perform a transfer. A restart can also have associated with it a descriptive textual string that a human user can be shown by the debugger to describe the potential action offered by the restart.
Restart points that require transfer of control but no data can be
established straightforwardly with WITH-SIMPLE-RESTART
.
For example:
(defun lisp-top-level-loop () (with-simple-restart (exit "Exit from Lisp.") (loop (with-simple-restart (continue "Return to Lisp toplevel.") (print (eval (read)))))))
Restarts that require data can also be established using a slightly more
elaborate syntax. This syntax not only accommodates the programmatic
data flow to the restart,
but also enough information for the Common Lisp function
INVOKE-RESTART-INTERACTIVELY
to properly prompt for any appropriate values to be supplied to that restart.
For example:
(defun my-symbol-value (name) (if (boundp name) (symbol-value name) (restart-case (error 'unbound-variable :name name) (use-value (value) :report "Specify a value to use." :interactive (lambda () (format t "~&Value to use: ") (list (eval (read)))) value) (store-value (value) :report "Specify a value to use and store." :interactive (lambda () (format t "~&Value to use and store: ") (list (eval (read)))) (setf (symbol-value name) value) value))))
Code that inquires about such restarts typically makes use of
FIND-RESTART
to test for the availability of a restart,
and then INVOKE-RESTART
to invoke a restart.
For example:
(handler-bind ((unbound-variable #'(lambda (c) ;argument is condition description ;; Try to make unbound variables get value 17 (dolist (tag '(store-value use-value)) (let ((restart (find-restart tag c))) (when restart (invoke-restart restart 17))))))) (+ (my-symbol-value 'this-symbol-has-no-value) (my-symbol-value 'pi))) ;pi DOES have a value! => 20.141592653589793
Absent such a handler, the restart would be offered interactively by the debugger, as in:
(+ (my-symbol-value 'this-symbol-has-no-value) (my-symbol-value 'pi)) Error: The variable THIS-SYMBOL-HAS-NO-VALUE is unbound. Please select a restart option: 1 - Specify a value to use. 2 - Specify a value to use and store. 3 - Return to Lisp toplevel. 4 - Exit from Lisp. Option: 1 Value to use: 19 => 22.141592653589793
A key capability provided by Common Lisp is the fact that, at the most primitive level, handling can be done in the dynamic context of the signaler, while certain very critical dynamic state information is still available that would be lost if a stack unwind happened before running the handler.
This capability is reflected in
the ability of the operator handler-bind
to take
control of a computation handler-case
,
which is more analogous to facilities offered in other languages,
does not allow programmer-supplied code to run until after the
transfer of control; this is useful for some simple situations, but is
less powerful.
Consider a code fragment such as:
(handler-case (main-action) (error (c) (other-action)))
In this example, the expression (other-action)
will run
(main-action)
signaled the error, regardless of how deep into main-action
that signaling occured.
By contrast, handler-bind
takes control SIGNAL
, and so is capable
of accessing restarts that are dynamically between the call to
SIGNAL
and the use of HANDLER-BIND
.
Consider this example:
(with-simple-restart (foo "Outer foo.") (handler-case (with-simple-restart (foo "Inner foo.") (error "Lossage.")) (error (c) (invoke-restart 'foo))))
In the above, the outer FOO
restart will be selected, as contrasted
with the following, where the inner FOO
restart will be selected:
(with-simple-restart (foo "Outer foo.") (handler-bind ((error #'(lambda (c) (invoke-restart 'foo)))) (with-simple-restart (foo "Inner foo.") (error "Lossage."))))
This is important because error handling tends to want to make use of all
available restarts, but HANDLER-BIND
can see but HANDLER-CASE
cannot. Consider another example:
(handler-case (foo) (unbound-variable (c) (let ((r (find-restart 'use-value c))) (if r (invoke-restart r nil)))))
The above example will not achieve its presumed intent, which is to
supply NIL as the default value for any unbound variable encountered
during the call to FOO
. The problem is that any
USE-VALUE
restart that is likely to be found will also be
within the call to FOO
, and will no longer be active
by the time the ERROR
clause of the HANDLER-CASE
expression is executed.
Use of HANDLER-BIND
allows this example to work in a way
that is not possible with HANDLER-CASE
and its analogs in
other programming languages. Consider:
(handler-bind (error #'(lambda (c) (let ((r (find-restart 'use-value c))) (if r (invoke-restart r nil))))) (foo))
Zetalisp contained a facility not only for asserting handlers to be used for conditions, but also an additional facility for asserting handlers that should be provisionally used only if no normal handlers were found. In effect, this meant there were two search lists, a handlers list and a default handlers list, which were searched in order.
In my use of Zetalisp's NES, I became convinced that it was conceptually incorrect
to search the default handlers list in order; I felt it should be searched
in reverse order. I had reported this as a bug, but it was never fixed.
In all honesty, I'm not sure there was enough data then
or perhaps even now to say whether I was right, although I continue to
believe that default handling is something that should proceed from the
outside in, not the inside out.
Nevertheless, whether I was right or not is not so much relevant in this
context as is the fact that it was a point of controversy that ended up
influencing the design of Common Lisp's condition system. I was distrustful
of the operator condition-bind-default
that was offered by NES,
and so I omitted it from the set of offerings that I transplanted to
Common Lisp.
The Common Lisp Condition System does provide a way to implement the concept of a default handler, but it is idiomatic. And, perhaps not entirely coincidentally, it has the net effect of seeking default handlers from the outside in rather than the inside out, as I had always felt was right.
The Zetalisp mechanism looked like this:
(condition-bind-default ((error #'(lambda (c) ...default handling...))) ...body in which handler is in effect...)
The corresponding Common Lisp idiom looks like this:
(handler-bind ((error #'(lambda (c) (signal c) ;resignal ...default handling...))) ...body in which handler is in effect...)
In effect, the Common Lisp idiom continues the signaling process but without explicitly relinquishing control. If the resignaled condition is unhandled, control will return to this handler and the default handling will be done. If, on the other hand, some outer handler does handle the condition, the default handling code will never be reached and so will not be run.
In some systems, such as Unix, "signals" are an asynchronous mechanism primarily used for implementing event-driven programming interfaces, but are not generally used within ordinary, synchronous programming.
While it is beyond the scope of the ANSI Common Lisp standard to address the issue of either interrupts or multitasking, most Common Lisp implementations have a convergent manner of coping with these issues that is sufficiently stable as to be worth some mention. The approach has been to separate the notion of "interrupting" from the notion of "signaling".
That is, in Common Lisp, all signaling is synchronous. However, such synchronous behavior can be usefully coupled with a process interruption to produce interesting effects.
In this separation, process interruptions without signaling might be done for any reason that involved the need to read or modify dynamic state of another process. Here is an example that merely reads the dynamic state of another process:
(defvar *my-dynamic-variable* 1) (let ((temporary-process (mp:process-run-function "temp" '() ;; Launch a temporary process that ;; merely dynamically binds a ;; certain variable and then ;; sleeps for a minute. #'(lambda () (let ((*my-dynamic-variable* 2)) (sleep 60))))) (result-value nil)) ;; Now interrupt our temporary process ;; to see the value of the variable (mp:process-interrupt temporary-process #'(lambda () (setq result-value *my-dynamic-variable*))) ;; Now wait for the interrupt to occur (mp:process-wait "not yet assigned" #'(lambda () result-value)) ;tests for a non-null value ;; If we get this far, the result-value has been assigned ;; and can be returned. result-value) => 2
Note that this merely examines the dynamic state of our temporary process, but does not invoke any signaling mechanism at all. And while the process of interruption is inherently asynchronous, the actions to be done in the interrupted process are synchronous.
If we instead intertwine the notion of process interruption with signaling, we get what some systems might call "asynchronous signaling", but which Common Lisp views as just the composition of two orthogonal facilities. So, for example, a keyboard interrupt to a process might be accomplished by:
(define-condition keyboard-interrupt () ((character :initarg :character :reader kbd-char)) (:report (lambda (condition stream) (format t "The character ~@:C was typed." (kbd-char condition))))) (defun keyboard-interrupt (process character) (mp:process-interrupt process #'(lambda () ;; Offer the process a chance to handle the condition. ;; If the condition is not handled, the call to SIGNAL returns ;; and the interrupt is completed. Normal process execution ;; then continues. (signal 'keyboard-interrupt :character character))))
Using such a facility, a keyboard process (itself a synchronous activity) can asynchronously interrupt another process (presumably, the window selected at the point an interrupt character is seen).
(defvar *selected-window* nil) (defun keyboard-process (raw-keyboard-stream) (loop (let ((char (read-char raw-keyboard-stream))) (when *selected-window* (if (is-interrupt-character? char) (keyboard-interrupt (window-process *selected-window*) char) ;; otherwise... (add-to-input-buffer *selected-window* char))))))
Although the KEYBOARD-PROCESS
shown here will interrupt the
window process, an understanding of what happens at that point does not
require any special knowledge of asynchrony. It merely requires observing
that at the time of interruption, the other process was about to execute
some expression (exp)
and will now execute instead
(progn (funcall interrupt-function) (exp))
This kind of structured approach removes much of the mystery and unpredictability normally associated with asynchronous interrupts in other systems, where the description of the effect is often not linguistic at all but deals in overly concrete terms of bits and registers in a way that only career experts can hope to navigate. The sense in the Common Lisp community is that a correct conceptual treatment of these issues makes these sorts of capabilities something that "mere mortals" can safely and conveniently employ in their programming.
The Dylan language patterned its efforts after the Common Lisp Condition System, but made some interesting changes. I probably lack the appropriate experience and surely the appropriate objectivity to conclude whether their changes are clear improvements over the Common Lisp approaches. But it's plain that by making divergent decisions in some places, the Dylan community has identified certain areas of the Common Lisp design as "controversial".
Common Lisp provides parallel but unrelated operators such as
HANDLER-BIND
and HANDLER-CASE
for dealing with handlers, and
RESTART-BIND
and RESTART-CASE
for dealing with restarts. It was thought
that these were orthogonal operations, requiring unrelated dataflow,
that really didn't belong intermingled. The Dylan community has sought
to coalesce these by making restarts into a kind of condition, and
eliminating special binding forms for them.
Probably the most controversial semantic component of the Common Lisp condition system is what has come to be called the "condition firewall". The idea behind the condition firewall is that a given handler should be executed in an environment that does not "see" intervening handlers that might have been established since its establishment.
So, for example, consider this code:
(handler-case (handler-bind ((error #'(lambda (condition) (declare (ignore condition)) (error 'unbound-variable :name 'fred)))) (handler-case ;; Signal an arbitrary error: (error "Not an UNBOUND-VARIABLE error.") (unbound-variable (c) (list 'inner c)))) (unbound-variable (c) (list 'outer c)))
This sets up two handlers for conditions of class
UNBOUND-VARIABLE
, one outside of the scope of the
general-purpose handler for conditions of class ERROR
and
one inside of its scope. At the time the "arbitrary" error
signaled, both handlers are in effect. This means that if the error
being signaled UNBOUND-VARIABLE
, it would have been caught by the inner
HANDLER-CASE
for UNBOUND-VARIABLE
. However,
as the search for a handler proceeds outward, the handlers that are
tried are executed in a context where the inner handlers are no longer
visible. As such, the above example yields
(OUTER #<ERROR UNBOUND-VARIABLE 12A39B87>)
By contrast, the following code:
(handler-case (handler-bind ((error #'(lambda (condition) (declare (ignore condition)) (error 'unbound-variable :name 'fred)))) (handler-case (error 'unbound-variable :name 'marvin) (unbound-variable (c) (list 'inner c)))) (unbound-variable (c) (list 'outer c)))
yields
(INNER #<ERROR UNBOUND-VARIABLE 12A39B87>)
It is interesting to note as an aside that the "resignaling trick" used earlier in the discussion of default handling relies implicitly on the condition firewall in order to avoid infinite recursion. Without the condition firewall, a different mechanism for implementing default handling is needed.
The designers of the Dylan language chose to eliminate the condition firewall, perhaps out of necessity since the most useful restarts almost always occur in the dynamic space near the point of signal, and the handlers usually occur farther out. If handlers could only see the restarts farther out than where they were established, they would not see the most useful restarts. (I am personally doubtful of this argument, and am more inclined to believe that this is why restarts should not have been turned into a kind of condition in Dylan, but I could be wrong and time will tell.)
The Dylan notation is different in many ways from Common Lisp,
but the approximately equivalent code
to the above two examples would
(INNER #<ERROR UNBOUND-VARIABLE 12A39B87>)
Language features don't originate spontaneously out of nowhere. We have surveyed some of the origins of the Common Lisp Condition System in an effort to demonstrate how prior experiences, both good and bad, have influenced the present design. Nor is this the end of the story. The ideas in Common Lisp have had some influence on other languages and will, I hope, continue to do so, since there are a number of things the Common Lisp makes easy through its condition system that other languages do not.
We have also seen that good terminology is important both to the specification of a programming language and to its community acceptance. Programming is not only a technical endeavor, but a social one. So much of so many lives is spent doing programming, that it is critical that we have good terminology, beyond the terms of the language itself, for talking among each other about what we are doing within the language.
And we have surveyed some of the key features that distinguish Common Lisp's condition system from those offered by other languages, and highlighted some open issues, where Common Lisp's answers to certain problems have already met with challenges.
During the design of Common Lisp, I headed the committee that produced the design of the condition system. At that time, there were many questions and doubts about the design: Were the decisions sound? Were all of the alternatives explored, or were there better ways we might later wish we'd tried? Were there problems lurking under the surface, waiting to bite someone when used under heavier stress?
It wasn't that people doubted our committee's competence, but rather many would-be reviewers lacked the relevant experience to critically analyze our proposals. Yet the design seemed mostly right to me, and my larger concern was that if we didn't at some point release it to a community of users to try, we'd be back at the same design table a few years later with the same questions and the same lack of community experience to answer them. A leap of faith seemed to be required to move ahead. So I and my committee nodded our collective heads and said we stood by the design. Personally, I had some doubts about some details, but I found it counterproductive to raise them because I believed the risk of not trying these things out was higher than the risk of trying them.
In my experience, much of language design is like this. We think we know how it will all come out, but we don't always. Usage patterns are often surprising, as one learns if one is around long enough to design a language or two and then watch how expectations play out in reality over a course of years. So it's a gamble. But the only way not to gamble is not to move ahead.
I once saw an interview on television with a font designer from Bitstream Inc. about how he conceptualized the process of font design. It is not about designing the shape of the letters, he explained, much to my initial surprise. Then he went on to explain that it was really about the shape of words. The font shapes play into that, but they are not, in themselves, the end goal. Programming language design is like that, too. It's not about the semantics of individual operators, but about how those operators fit together to form sentences in programs.
Unlike the situation with fonts, where whole books can be viewed
instantly in a new font to see how the design works, we don't know in
advance what sentences will be made in a programming language. We
have to wait and see what people choose to write. Common Lisp took a
step forward, and while we can quibble endlessly over whether any
given design decision was right, the one design decision I'm most
certain was right was to offer the community a rich set of
capabilities that would empower them not only to write programs, but
also to have a stake in future designs. Never again will I fear
sending out e-mail to a design group asking for advice about what the
semantics of HANDLER-BIND
should be and finding that no one has
an opinion! To me, that kind of progress, the evolution of a whole
community's understanding, is the best kind of progress of all.
I would like to thank Keith Corbett, Christophe Dony, Bernard Greenberg, and Erik Naggum for reviewing drafts of this text. Any lingering errors after they got done looking at it are still my responsibility, but I'm quite sure the editorial, technical, and historical quality of this text was improved measurably through their helpful scrutiny.
Original printed text document
Copyright 2001 by Kent M. Pitman. All Rights Reserved.
HTML hypertext version of document
Copyright 2002, Kent M. Pitman. All rights reserved.
The following limited, non-exclusive,
revokable licenses are granted:
Browsing of this document (that is, transmission and display of a temporary copy of this document for the ordinary purpose of direct viewing by a human being in the usual manner that hypertext browsers permit such viewing) is expressly permitted, provided that no recopying, redistribution, redisplay, or retransmission is made of any such copy.
Bookmarking of this document (that is, recording only the document's title and Uniform Resource Locator, or URL, but not its content, for the purpose of remembering an association between the document's title and the URL, and/or for the purpose of making a subsequent request for a fresh copy of the content named by that URL) is also expressly permitted.
All other uses require negotiated permission.