Replace Exceptions With Defaults

December 1, 2017

There are several ways to approach this task, so our first priority is to define precisely what we want:

Write syntax try that takes zero or more expressions and returns the value computed by the left-most expression that is non-#f. If all the expressions are #f, return #f. An expression that raises an exception is considered to have returned #f and does not cause program execution to halt.

For instance, we can search an association-list like this:

> (define a-list '((1 . 1) (3 . 3) (5 . 5)))
> (assoc 4 a-list)
#f

But in many cases it’s better to return a default value:

> (define (lookup x) (try (cdr (assoc x a-list)) x))
> (lookup 4)
4

We can almost use or for assigning a default value:

> (define (lookup x) (or (cdr (assoc x a-list)) x))

But that doesn’t work; the assoc returns #f if x is not found, causing cdr to throw an exception. Some programming languages make use of something like that; here’s pseudo-perl, which I’ve always found to be a little bit ugly:

lookup 4 || die

It’s almost always better to prevent an exception from allowing an expression to fail, because the program can continue execution without halting:

> (define xs '())
> (car xs)
Exception in car: () is not a pair
> (try (car xs) #f)
#f

Here is our version of try:

(define-syntax try
  (syntax-rules (trying)
    ((try trying expr default)
      (call-with-current-continuation
        (lambda (return)
          (with-exception-handler
            (lambda (x) (return default))
            (lambda () expr)))))
    ((try) #f)
    ((try expr) (try trying expr #f))
    ((try expr0 expr1 ...)
      (let ((t (try trying expr0 #f)))
        (if t t (try expr1 ...))))))

Here, try trying is a low-level rule that evaluates a single expression, returning either the value of the expression in the continuation or the default value if the expression throws an exception; the magic is in with-exception-handler, which has been part of Scheme since R6RS. The other three rules, without trying, walk through a series of expressions, returning the first that neither causes an exception nor evaluates #f. Any of the expressions can be arbitrarily complex:

> (try (car '()) (+ 2 2)
4

By the way, try must be a macro, not a function, because some of the expressions may not be evaluated. The beauty of Scheme’s hygienic macros is that trying, as it appears literally in the macro above, doesn’t capture an appearance of trying in client code; here, trying is unbound, and throws an exception:

> trying
Exception: variable trying is not bound
> (try trying 3)
3

Try provides a clean way to prevent exceptions from causing a program to halt. Liberal use of try leads to a different mind-set when programming, because every expression successfully produces a value and you don’t have to worry about unhandled exceptions. You can run the program at https://ideone.com/ejdllr.

Advertisement

Pages: 1 2

5 Responses to “Replace Exceptions With Defaults”

  1. chaw said

    Below are my procedural and syntactic interpretations of the idea.
    The syntactic version puts the default first, before the body of
    expressions, because I thought that a bit nicer, though it is easy to
    change.

    (import (scheme base)
            (scheme write))
    
    (define (make-try/default proc dflt)
      (lambda args
        (guard (exc (else dflt))
          (apply proc args))))
    
    (define-syntax try/default
      (syntax-rules ()
        ((_  dflt body body1 ...)
         ((make-try/default (lambda () body body1 ...) dflt)))))
    
    (define (test)
      (define car/default (make-try/default car 42))
      (display (map car/default '((2 3 5) (2) ())))
      (newline)
      (display (try/default '(4 2 42)
                 (define lst '(2 3 5))
                 (map car (list lst '(2) '()))))
      (newline))
    
    (test)
    

  2. programmingpraxis said

    @chaw: Your version fails because it evaluates the default expression even when it is not needed. Here is output from my macro, at the top, and your macro/procedure, at the bottom:

    > (try (car '(2)) (begin (display "hello") (newline) 3))
    2
    > (try/default (begin (display "hello") (newline) 3) (car '(2)))
    hello
    2

    Both programs correctly evaluate to the car of the list. But your program also prints “hello” as a side-effect of evaluating the default expression, which it should not do.

    You can run the program at https://ideone.com/9ajhyO.

  3. Daniel said

    Here’s a solution in Python.

    The default value is specified using a lambda. This provides deferred evaluation to address the issue that @programmingpraxis mentions in an earlier comment (so that the default is only evaluated when needed).

    The function returns a second value that indicates whether the expression was evaluated successfully.

    def trydefault(expr, default):
        try:
            output = expr()
            success = True
        except:
            output = default()
            success = False
        return output, success
    
    l = []
    element, _ = trydefault(lambda: l[0], lambda: None)
    print element
    

    Output:

    None
    
  4. John Cowan said

    I disagree that this is in any way good practice. If you are passing a non-pair to car, it should fail immediately and loudly, as something is severely wrong. Changing it to #f just postpones the evil day and makes the error harder to debug.

    That said, long-running programs do need to catch exceptions so as to remain long-running, and the Nulll Object pattern (which returns an object of the correct type with no contents rather than a generic nil) is perfectly good even in Lisps.

  5. programmingpraxis said

    @John: I used (car ‘()) as an example. I would not handle type errors that way in practice. The text makes clear that this is a mechanism for handling some types of exceptions, not as a way to bypass real errors. For instance, if a binary tree lookup doesn’t find an expected key, this mechanism can replace the exception with some default value so the program can continue.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: