10 KiB
Exceptions and Control
Racket provides an especially rich set of control operations—not only operations for raising and catching exceptions, but also operations for grabbing and restoring portions of a computation.
1 Exceptions
2 Prompts and Aborts
3 Continuations
1. Exceptions
Whenever a run-time error occurs, an exception is raised. Unless the exception is caught, then it is handled by printing a message associated with the exception, and then escaping from the computation.
> (/ 1 0)
/: division by zero
> (car 17)
car: contract violation
expected: pair?
given: 17
To catch an exception, use the with-handlers
form:
(with-handlers ([predicate-expr handler-expr] ...)
body ...+)
Each predicate-expr
in a handler determines a kind of exception that
is caught by the with-handlers
form, and the value representing the
exception is passed to the handler procedure produced by handler-expr
.
The result of the handler-expr
is the result of the with-handlers
expression.
For example, a divide-by-zero error raises an instance of the
exn:fail:contract:divide-by-zero
structure type:
> (with-handlers ([exn:fail:contract:divide-by-zero?
(lambda (exn) +inf.0)])
(/ 1 0))
+inf.0
> (with-handlers ([exn:fail:contract:divide-by-zero?
(lambda (exn) +inf.0)])
(car 17))
car: contract violation
expected: pair?
given: 17
The error
function is one way to raise your own exception. It packages
an error message and other information into an exn:fail
structure:
> (error "crash!")
crash!
> (with-handlers ([exn:fail? (lambda (exn) 'air-bag)])
(error "crash!"))
'air-bag
The exn:fail:contract:divide-by-zero
and exn:fail
structure types
are sub-types of the exn
structure type. Exceptions raised by core
forms and functions always raise an instance of exn
or one of its
sub-types, but an exception does not have to be represented by a
structure. The raise
function lets you raise any value as an
exception:
> (raise 2)
uncaught exception: 2
> (with-handlers ([(lambda (v) (equal? v 2)) (lambda (v) 'two)])
(raise 2))
'two
> (with-handlers ([(lambda (v) (equal? v 2)) (lambda (v) 'two)])
(/ 1 0))
/: division by zero
Multiple predicate-expr
s in a with-handlers
form let you handle
different kinds of exceptions in different ways. The predicates are
tried in order, and if none of them match, then the exception is
propagated to enclosing contexts.
> (define (always-fail n)
(with-handlers ([even? (lambda (v) 'even)]
[positive? (lambda (v) 'positive)])
(raise n)))
> (always-fail 2)
'even
> (always-fail 3)
'positive
> (always-fail -3)
uncaught exception: -3
> (with-handlers ([negative? (lambda (v) 'negative)])
(always-fail -3))
'negative
Using (lambda (v) #t)
as a predicate captures all exceptions, of
course:
> (with-handlers ([(lambda (v) #t) (lambda (v) 'oops)])
(car 17))
'oops
Capturing all exceptions is usually a bad idea, however. If the user
types Ctl-C in a terminal window or clicks the Stop button in DrRacket
to interrupt a computation, then normally the exn:break
exception
should not be caught. To catch only exceptions that represent errors,
use exn:fail?
as the predicate:
> (with-handlers ([exn:fail? (lambda (v) 'oops)])
(car 17))
'oops
> (with-handlers ([exn:fail? (lambda (v) 'oops)])
(break-thread (current-thread)) ; simulate Ctl-C
(car 17))
user break
2. Prompts and Aborts
When an exception is raised, control escapes out of an arbitrary deep evaluation context to the point where the exception is caught—or all the way out if the exception is never caught:
> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (/ 1 0)))))))
/: division by zero
But if control escapes “all the way out,” why does the REPL keep going
after an error is printed? You might think that it’s because the REPL
wraps every interaction in a with-handlers
form that catches all
exceptions, but that’s not quite the reason.
The actual reason is that the REPL wraps the interaction with a prompt, which effectively marks the evaluation context with an escape point. If an exception is not caught, then information about the exception is printed, and then evaluation aborts to the nearest enclosing prompt. More precisely, each prompt has a prompt tag, and there is a designated default prompt tag that the uncaught-exception handler uses to abort.
The call-with-continuation-prompt
function installs a prompt with a
given prompt tag, and then it evaluates a given thunk under the prompt.
The default-continuation-prompt-tag
function returns the default
prompt tag. The abort-current-continuation
function escapes to the
nearest enclosing prompt that has a given prompt tag.
> (define (escape v)
(abort-current-continuation
(default-continuation-prompt-tag)
(lambda () v)))
> (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0)))))))
0
> (+ 1
(call-with-continuation-prompt
(lambda ()
(+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (+ 1 (escape 0))))))))
(default-continuation-prompt-tag)))
1
In escape
above, the value v
is wrapped in a procedure that is
called after escaping to the enclosing prompt.
Prompts and aborts look very much like exception handling and raising.
Indeed, prompts and aborts are essentially a more primitive form of
exceptions, and with-handlers
and raise
are implemented in terms of
prompts and aborts. The power of the more primitive forms is related to
the word “continuation” in the operator names, as we discuss in the next
section.
3. Continuations
A continuation is a value that encapsulates a piece of an expression’s
evaluation context. The call-with-composable-continuation
function
captures the current continuation starting outside the current
function call and running up to the nearest enclosing prompt. (Keep in
mind that each REPL interaction is implicitly wrapped in a prompt.)
For example, in
(+
1
(+
1
(+
1
0)))
at the point where 0
is evaluated, the expression context includes
three nested addition expressions. We can grab that context by changing
0
to grab the continuation before returning 0:
> (define saved-k #f)
> (define (save-it!)
(call-with-composable-continuation
(lambda (k) ; k is the captured continuation
(set! saved-k k)
0)))
> (+ 1 (+ 1 (+ 1 (save-it!))))
3
The continuation saved in save-k
encapsulates the program context (+ 1 (+ 1 (+ 1 ?)))
, where ?
represents a place to plug in a result
value—because that was the expression context when save-it!
was
called. The continuation is encapsulated so that it behaves like the
function (lambda (v) (+ 1 (+ 1 (+ 1 v))))
:
> (saved-k 0)
3
> (saved-k 10)
13
> (saved-k (saved-k 0))
6
The continuation captured by call-with-composable-continuation
is
determined dynamically, not syntactically. For example, with
> (define (sum n)
(if (zero? n)
(save-it!)
(+ n (sum (sub1 n)))))
> (sum 5)
15
the continuation in saved-k
becomes (lambda (x) (+ 5 (+ 4 (+ 3 (+ 2 (+ 1 x))))))
:
> (saved-k 0)
15
> (saved-k 10)
25
A more traditional continuation operator in Racket or Scheme
is
call-with-current-continuation
, which is usually abbreviated
call/cc
. It is like call-with-composable-continuation
, but applying
the captured continuation first aborts to the current prompt
before
restoring the saved continuation. In addition, Scheme systems
traditionally support a single prompt at the program start, instead of
allowing new prompts via call-with-continuation-prompt
. Continuations
as in Racket are sometimes called delimited continuations, since a
program can introduce new delimiting prompts, and continuations as
captured by call-with-composable-continuation
are sometimes called
composable continuations, because they do not have a built-in abort.
For an example of how continuations are useful, see [missing]. For
specific control operators that have more convenient names than the
primitives described here, see racket/control
.