Conditions in TOM are modeled after Common Lisp conditions (see Common
Lisp the Language, 2nd edition), with some simplifications.
Conditions are not like exceptions in languages like C++ or Java,
the most important difference being that the stack is not unwound
while the condition is being handled (see also the TOM highlight
Why methods do not declare the
exceptions they raise).
Creation and issuing of conditions is functionality of the TOM runtime
library. Handling conditions is part of the TOM language. We'll
start the discussion of these two intertwined subjects with
catching things that are thrown.
Non-local gotos
TOM provides exactly one way to perform a non-local goto:
throwing something at an object, by invoking the
throw method that is defined by the All
instance, as in:
Here, void is not the void type, but the
single possible value of that type, which bears the same name.
Thus basically, we throw nothing special at the object we know as
my_object .
Of course, throwing something is not really interesting if you can not
catch it. To setup a catch for a value thrown at an object, use a
catch expression:
|
catch (my_object)
[foo do_difficult_with bar];
|
catch is followed by the tag expression in
parentheses, followed by a body expression. While the body of the
catch is evaluated, anything thrown at the tag will be caught by
this catch. In the example, we're catching values for the object
we know as my_object .
The tag expression must be an object. The tag can be nil ,
but that is not very useful, since you will not be able to throw
anything at nil .
A catch is an expression and like every expression, it has
a type and it produces a value after evaluation. The value of the
catch expression is either the value of the body expression or the
value thrown at the tag object. For example, the following is an
elaborate way to assign to the boolean a
whether a1 is larger than a2 .
|
boolean a = catch (self)
({
if (a1 > a2)
[self throw TRUE];
FALSE;
});
|
When executing, if a1 > a2 , the value
TRUE will be thrown at the catch: execution of the
body expression is terminated, the stack is unwound up to the
stack frame of the catch (think longjmp if you like),
and the value TRUE is returned as the value of the
catch expression. In the other case, if
a1 is not larger than a2 , the body of
the if is not executed, and the result of the catch
is the value of the last expression, FALSE .
Incidentally, note the parentheses around the body compound of
catch : the body of a catch must be an expression, and
a compound can only be an expression as an element of a tuple.
(The tuple has only one element in this case.)
It is an error if the type of value thrown at an object does not match
the type that the catch expects. The one exception to this rule
is when the value void is thrown, in which case the
value returned by the catch is the default value of the type to be
returned (0 for numbers, nil for objects, etc).
Issuing conditions
A condition is an object, an instance of the
Condition class. A condition is normally created by
invoking the following method of the Condition class:
|
instance (id)
for All object
class ConditionClass condition_class
message String message;
|
The three arguments provide the values of the three instance
variables with the same name, of the Condition object
that will be created and returned. The object is the
object to which the condition applies, for example the
File object for a condition applying to some failed
operation on that file. The message is meant to provide a
description of the condition, to be read by a human and not to be
interpreted by a program. An example of a useful message is the
string returned by the C library function perror .
The condition_class is an instance of the
ConditionClass class. Each condition-class object
has a name, and through their super-condition-class instance
variable, the condition class objects describe a single
inheritance hierarchy of condition classes.
The Conditions class contains static class variables for
the predefined condition classes. For example, one of them is the
nil-receiver ; it is a runtime-condition ,
which in turn is a serious-condition , which is a
condition . The condition is the root of
the condition-class hierarchy; it is the supercondition-class of
all other condition classes.
As an example, when a message is sent to nil , the
following condition is created:
|
Condition c = [Condition for nil class nil-receiver
message "nil receiver"];
|
The object to which the condition applies is of course
nil , since the fact that it is nil was
the reason for creating the condition.
Conditions can be issued in two different ways: raised or signaled.
When a condition is raised, as in
|
Condition c = ...;
[c raise];
|
execution of the program is guaranteed to not return from the
raise method, exiting the program if that is the only
way to achieve the goal. A condition is signaled by invoking its
signal method:
An invocation of the signal method may or may not
return, depending on the behavior of the installed condition
handlers: if one of them performs a non-local goto, the
signal invocation will not return.
Condition handlers
To start with an example, the following is a main
method that I used frequently to observe unhandled condition
signals (until the library option :rt-signals was
provided that does the same):
|
int (retcode)
main Array arguments
{
ConditionClass cc = condition;
bind ((cc,
{
[[[stdio err]
print ("unhandled condition: ",
condition)] nl];
condition;
}))
retcode = [self real_main arguments];
}
|
The bind sets up a condition handler, which will be
in place while the body of the bind is active, in
this case during the invocation of real_main . When a
condition is raised or signaled, all active handlers are
considered in reverse order from their creation.
Each handler in a bind is a two-expression tuple; multiple
handlers are separated by semicolons. The type of a handler is
The first element is a ConditionClass , indicating to
which kind of conditions the handler applies. In the example, the
condition class is condition , but a variable with a
different name is used to denote it. The reason for this is the
fact that within a handler, condition is the name of
the condition that is being passed. In the example, a handler is
installed that matches any condition with condition class
condition , or any subcondition-class thereof.
The second element of a handler is an expression that will be executed
to handle a condition. The condition is available in the
implicitly present `argument' to the handler:
A condition handler can do one of three things:
- Let the condition pass: in this case the handler decides that the
condition is not interesting after all, and that the condition
must continue to search for a handler that is willing to handle
it. Searching continues with outstanding handlers further on the
stack.
A handler shows its desinterest by returning the condition
object, as is done in the above example.
- Handle the condition: the handler returns any object, just not the
condition being handled. What the value that is
returned means depends on the kind of condition. In this
case, signaling the condition will finish and the invocation of
the signal of the condition will return with the
value returned by the handler. See below for an example of this
usage.
- Perform a non-local goto, by throwing some value at some
catch tag.
The above description is valid for a condition that is signaled.
If a condition is raised, cases 1 and 2 are not discerned (and
handled like case 1): if a condition is raised, some handler
somewhere along the line must perform a non-local goto, or the
program will exit when it has run out of possible handlers. A
handler can ask a condition whether it is being signaled or raised
by invoking its raised method, which returns a
boolean .
unwind
The unwind expression guarantees that some protection
code is executed, either when the body expression has been
evaluated, or when the stack frame is cleared because of a
non-local goto. In the following example, the unwind
ensures that the file is closed no matter what
happens:
|
File file = [File open ...];
unwind ([file close])
({
/* Do something with the FILE. */
...;
});
|
The value of the unwind expression is the value of
the body.
signal example
This section presents an example in which conditions are used to
give the user control over what happens when a file can not be
opened. This setup is only possible because the stack is not
unwound while a condition is handled, i.e., because conditions can
not only be raised but also signaled.
Suppose a (fictuous) ReadOnlyFile object is opened and
created using this method:
|
ReadOnlyFile (the_file)
open String filename
{
the_file = [[self alloc] initWithFilename filename];
if (![the_file open])
return nil;
}
|
Thus, opening a file first creates a new object for the given
filename, and then lets the file open itself. The implementation
of the open method could look like this; hopefully
the methods being invoked have descriptive enough names for their
missing implementation to not be a problem:
|
boolean (success)
open
{
for (;;)
{
/* Try to open, and return TRUE upon success. */
[self attempt_to_open];
if ([self is_open])
return TRUE;
/* See if we can get an alternative filename. */
Condition c = [Condition for self class file-open-problem
message [self strerror]];
String alternative_name = [c signal];
if (!alternative_name)
{
/* The condition was not handled. */
return FALSE;
}
/* Make the ALTERNATIVE_NAME our new name. */
[self set_name alternative_name];
}
}
|
This method will try to open the file, getting alternative names
as long as they are supplied, and finally return TRUE
upon success, or FALSE upon failure.
We could use the mechanism provided by the ReadOnlyFile
class in a program to offer the user the possibility of specifying
an alternative filename, as shown in the following code:
|
/* Setup a handler for FILE-OPEN-PROBLEM conditions. */
bind ((file-open-problem,
{
/* Retrieve the file object to which the condition applies. */
ReadOnlyFile file = [condition object];
/* Describe the problem to the user and prompt for input. */
[[[stdio err] print ("trouble opening: ", [file name])] nl];
[[[stdio err] print ("reason: ", [condition message])] nl];
[[[stdio err] print "alternative (RET for original)? "] nl];
/* Read a line of input. */
String input = [[stdio err] readLine];
/* If the input is an empty line, return NIL, indicating that
we did handle this condition, but that an alternative
filename was not entered. Otherwise, return the filename
entered by the user. As long as we do not return the
CONDITION object, we will have handled this condition. */
[input length] > 0 ? input : nil;
}))
({
file = [ReadOnlyFile open "/foo/bar"];
})
|
This code is available as a complete example with source and GNUmakefile (the latter should
strongly resemble the GNUmakefile from the `hello world' example
in the TOM distribution, apart from the UNIT= and
TOM_SRC= lines).
Below are two example runs; the program attempts to open the file
and prints the value that is returned before exiting:
|
$ ./rofile foo
trouble opening: foo
reason: No such file or directory
alternative (RET for original)?
bar
trouble opening: bar
reason: No such file or directory
alternative (RET for original)?
*nil*
$ ./rofile foo
trouble opening: foo
reason: No such file or directory
alternative (RET for original)?
rofile
#<ReadOnlyFile 00201700 name=rofile>
$
|
Up: Highlights
|