Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try(-with) - yet another try/catch/finally macro #12

Open
eutro opened this issue Aug 3, 2021 · 0 comments
Open

try(-with) - yet another try/catch/finally macro #12

eutro opened this issue Aug 3, 2021 · 0 comments

Comments

@eutro
Copy link

eutro commented Aug 3, 2021

Macro

A try/catch/finally macro for "forwards" error handling and finalization. Hello #9 and #10!

Source code: https://github.com/eutro/try-catch-match/blob/master/main.rkt

#lang racket/base

(provide try catch finally
         try-with try-with*)

(require racket/match (for-syntax syntax/parse racket/base))

(begin-for-syntax
  (define ((invalid-expr name) stx)
    (raise-syntax-error name "invalid in expression context" stx)))

(define-syntax catch (invalid-expr 'catch))
(define-syntax finally (invalid-expr 'finally))

(begin-for-syntax
  (define-syntax-class catch-clause
    #:description "catch clause"
    #:literals [catch]
    (pattern (catch binding:expr body:expr ...+)))

  (define-syntax-class finally-clause
    #:description "finally clause"
    #:literals [finally]
    (pattern (finally body:expr ...+)))

  (define-syntax-class body-expr
    #:literals [catch finally]
    (pattern (~and :expr
                   (~not (~or (finally . _)
                              (catch . _)))))))

(define-syntax (try stx)
  (syntax-parse stx
    [(_ body:body-expr ...+)
     #'(let () body ...)]
    [(_ body:body-expr ...+
        catch:catch-clause ...
        finally:finally-clause)
     #'(call-with-continuation-barrier
        (lambda ()
         (dynamic-wind
           void
           (lambda ()
             (try body ... catch ...))
           (lambda ()
             finally.body ...))))]
    [(_ body:body-expr ...+
        catch:catch-clause ...)
     #'(with-handlers
         ([void
           (lambda (e)
             (match e
               [catch.binding catch.body ...] ...
               [_ (raise e)]))])
         body ...)]))

(define-syntax (try-with stx)
  (syntax-parse stx
    [(_ ([name:id val:expr] ...)
        body:body-expr ...+)
     #'(let ([cust (make-custodian)])
         (try
          (define-values (name ...)
            (parameterize ([current-custodian cust])
              (values val ...)))
          body ...
          (finally (custodian-shutdown-all cust))))]))

(define-syntax (try-with* stx)
  (syntax-parse stx
    [(_ ([name:id val:expr] ...)
        body:body-expr ...+)
     #'(let ([cust (make-custodian)])
         (try
          (define-values (name ...)
            (parameterize ([current-custodian cust])
              (define name val) ...
              (values name ...)))
          body ...
          (finally (custodian-shutdown-all cust))))]))

Documentation: https://docs.racket-lang.org/try-catch-match/index.html

try/catch/finally is a common and familiar syntax for handling exceptions, used in many languages such as Java, C++ and Clojure. Errors thrown within the try block may be "caught" by the catch clauses. In any case, whether by normal return or exception, the finally clause is executed.

The try macro achieves a similar result. Any exceptions thrown within the try expression's body will be matched against the catch clauses in succession, returning the result of the catch clause if the exception matches. Then, regardless of means, the finally clause is executed when leaving the dynamic extent of the try expression's body.

The expressiveness of match syntax makes it sufficiently flexible for any case, and grants familiarity to those that are used to it.

The try-with macro (and its cousin try-with*), influenced by with-open from Clojure and the try-with-resources from Java generalises resource cleanup in an exception-safe way.

Example

Occasionally with-handlers is unwieldy. Predicates and handlers have to be wrapped in functions, and the error handling code comes before the code that can cause the error. With try it can instead be declared after, without requiring explicit lambdas:

(try
 (read port)
 (catch (? exn:fail:read?) #f)

Perform cleanup such as decrementing a counter on exit:

(try
 (increment-counter!)
 (do-stuff)
 (finally (decrement-counter!)))

Open a file and close it on exit:

(try-with ([port (open-output-file "file.txt")])
 (displayln "Hello!" port))

Before and After

Exception-handling code is incredibly easy to get wrong. Typically it gets very little testing. with-handlers and dynamic-wind especially can be difficult to understand, and clunky to use. try/catch/finally presents a familiar syntax that is hopefully easy to use and leads to less bugs.

At the time of writing, R16 has two examples of erroneous code that could benefit from try:


This example currently doesn't handle exceptions properly.
A thread is notified and a counter incremented. A procedure that was passed in is executed, and the counter is decremented again.
However, the counter is not properly decremented for unexpected returns, such as exceptions.

    (define (with-typing-indicator thunk)
      (let ([payload (list client (hash-ref (current-message) 'channel_id))])
        (thread-send typing-thread (cons 1 payload))
        (let ([result (call-with-values thunk list)])
          (thread-send typing-thread (cons -1 payload))
          (apply values result))))

It could be rewritten with dynamic-wind, which may confuse those unfamiliar with it.

    (define (with-typing-indicator thunk)
      (let ([payload (list client (hash-ref (current-message) 'channel_id))])
        (dynamic-wind
          (lambda () (thread-send typing-thread (cons 1 payload)))
          thunk
          (lambda () (thread-send typing-thread (cons -1 payload))))))

Or with try:

    (define (with-typing-indicator thunk)
      (let ([payload (list client (hash-ref (current-message) 'channel_id))])
        (thread-send typing-thread (cons 1 payload))
        (try
          (thunk)
          (finally (thread-send typing-thread (cons -1 payload))))))

This example is a simple mistake made by the author, who forgot to wrap #f in const. A read exception thrown in this causes an error trying to apply #f.

      (define (read-args)
        (with-handlers ([exn:fail:read? #f])
          (sequence->list (in-producer read eof (open-input-string args)))))

It could be rewritten with try, without requiring a const, as:

      (define (read-args)
        (try (sequence->list (in-producer read eof (open-input-string args)))
             (catch (? exn:fail:read?) #f)))

Licence

This code is under the same MIT License that the Racket language uses. https://github.com/eutro/try-catch-match/blob/master/LICENSE
The associated text is licensed under the Creative Commons Attribution 4.0 International License http://creativecommons.org/licenses/by/4.0/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant