One way for this feature of the virtual machine to be used is to
intercept and translate error messages to a more meaningful form. An
application guarded as shown below causes messages of invalid deconstruction
to be changed to 'syntax error'
.
main = guard(
application,
conditional(
bu(compare,('invalid deconstruction',nil)),
(constant ('syntax error',nil),identity)))
The conditional compares its argument to the error message for an
invalid deconstruction, and if it matches, the syntax error message is
returned, but otherwise the original message is returned. Note that an
error message must be in the form of a list of character strings, so
that it can be printed. Although the message of 'syntax error'
might not be very informative, at least it looks less like a crash.
A real application should of course strive to do better than that.
Exception handling features of the virtual machine can also be adapted by applications to raise their own exceptions with customized messages.
error_messenger = guard(compose(compare,constant nil),constant ('syntax error',nil))
This code fragment implements a function that causes a message of
'syntax error'
to be reported for any possible input. This code
works by first causing an invalid comparison and then substituting its
own error message. A function that always causes an error is not useful
in itself, but might be used as part of an application in the following
form.
main = conditional(validation,(application,error_messenger))
In this case, the application checks the validity of the input with a predicate, and invokes the error messenger if it is invalid.
Although the previous examples return a fixed error message for each possible kind of error, it is also possible to have error messages that depend on the input data, as the next example shows.
main = (hired apply)( compose( bu(guard,some_application), (hired constant)(constant 'invalid input was:',identity)), identity)
If the application causes an exception for any reason, the error message
returned will include a complete listing of the input, prefaced by the
words 'invalid input was:'
. This particular example works only if
the input is a list of character strings, but could be adapted for other
types of data by substituting an appropriate formatting function for the
first identity. The formatting function would take the relevant data
type to a list of character strings. Another possible variation would be to
concatenate the invalid input listing with the error message that was
generated, rather than just replacing it.
As the last example may suggest, exception handlers turn out to be an essential debugging tool for functional programs, making them as easy to debug as imperative programs if not more so. This example forms the basis for a higher order function that wraps any given function with an exception handler that prints the argument causing it to crash. For arguments not causing a crash, the behavior is unchanged. Alternatively, code implementing a function that unconditionally reports its argument in an error message can be inserted at a strategic point in the application code similarly to a print statement. Finally, inspired use of exception handlers that concatenate their messages with previously generated messages can show something like a parameter stack dump when a recursively defined function crashes. These are all matters for a language designer and are not pursued further in this document.