try-make-sarna-happy
(require try-make-sarna-happy) | |
package: try-make-sarna-happy |
This package was written for the 2021 Syntax Parse bee and to satisfy sarna’s complaints on Discord with with-handlers, which leads to a "backwards" looking program:
; say first what to do with the exception (with-handlers ([exn:fail:syntax? (λ (e) (displayln "got a syntax error"))]) ; and only then what to actually do (raise-syntax-error #f "a syntax error"))
(from the with-handlers examples).
1 The try macro
syntax
(try body-expr ...+ maybe-catch maybe-catch/match maybe-finally)
maybe-catch =
| (catch [pred-expr exn-id handler-expr ...+] ...) maybe-catch =
| (catch/match [match-expr handler-expr ...+] ...) maybe-finally =
| (finally finally-expr ...+)
The catch clauses use with-handlers, but in a different format: when pred-expr returns true for a thrown exception, exn-id is bound to the exception for the body handler-expr.
The catch/match clauses are match forms tested against the exception.
When both catch-style and catch/match-style clauses are present, all of the catch-style clauses are tried before any of the catch/match clauses.
syntax
(catch [pred-expr exn-id handler-expr ...+])
If no pred-exprs succeed, testing proceeds to any catch/match clauses.
syntax
(catch/match [match-expr handler-expr ...+])
If no match-exprs succeed, the exception is re-raised.
syntax
(finally finally-expr ...+)
1.1 Examples
> (try (/ 10 0) (catch [exn? e (exn-message e)])) "/: division by zero"
> (struct my-error [x y])
> (try (raise (my-error 1 2)) (catch/match [(my-error 1 y) y])) 2
> (let ([resource (get-handle)]) (try (use-might-break resource) (catch [exn? e (displayln (exn-message e))]) (finally (close resource))) (is-closed? resource)) use-might-break: something went wrong
#t
1.2 Before and After
This is a "Code Cleaning" macro: it tidies up a common pattern and makes it read in a forward direction. In this it is similar to the threading library but for exceptions.
The following Before/After pairs do not show the call-with-continuation-barrier call because they assumes none of the shown procedures muck with continuations. It is needed in the general case, however, to prevent a captured continuation from re-entering the dynamic-wind and thus causing the finally clause to be run more than once.
Before
| After
|
Before
| After |
The following are from Beautiful Racket: Errors and Exceptions.
Before
| After
|
Before
| After
|
Before
| After
|
1.3 Implementation
The following is provided explicitly for the 2021 Syntax Parse bee.
#lang racket/base |
(provide try catch catch/match finally) |
(require (for-syntax racket/base) |
racket/match |
syntax/parse/define) |
(begin-for-syntax |
(define ((only-in-try name) stx) |
(raise-syntax-error name "not allowed except in try" stx))) |
(define-syntax catch (only-in-try 'catch)) |
(define-syntax catch/match (only-in-try 'catch/match)) |
(define-syntax finally (only-in-try 'finally)) |
(begin-for-syntax |
(define-syntax-class try-body |
#:literals (catch catch/match finally) |
(pattern {~and :expr {~not {~or (catch . _) (catch/match . _) (finally . _)}}})) |
(define-syntax-class catch-clause |
#:attributes ((handlers 1)) |
#:literals (catch) |
(pattern (catch [pred:expr name:id body:expr ...+] ...) |
#:with (handlers ...) #'([pred (λ (name) body ...)] ...))) |
;; this one's for you, notjack |
(define-syntax-class catch-match-clause |
#:attributes (handler) |
#:literals (catch/match) |
(pattern (catch/match [clause:expr body:expr ...+] ...) |
#:with (match-clauses ...) #'([clause body ...] ...) |
#:with handler #'[(λ (_) #t) ;; catch 'em all |
(match-lambda |
match-clauses ... |
;; rethrow as last resort |
[e (raise e)])])) |
(define-syntax-class finally-clause |
#:attributes (handler) |
#:literals (finally) |
(pattern (finally body:expr ...+) |
#:with handler #'(λ () body ...)))) |
;; Calls value-thunk, then post-thunk, with post-thunk guaranteed to be run |
;; even if execution exits value-thunk through an exception or continuation |
;; |
;; value-thunk is prevented from re-entry and continutation shenanigans by a |
;; continuation-barrier |
;; |
;; thanks to Alex Knauth & SamPh on Discord |
(define (call-with-try-finally value-thunk post-thunk) |
(call-with-continuation-barrier |
(λ () (dynamic-wind void value-thunk post-thunk)))) |
(define-syntax-parser try |
[(_ body:try-body ...+ |
{~optional c:catch-clause} |
{~optional m:catch-match-clause} |
{~optional f:finally-clause}) |
#'(call-with-try-finally |
(λ () |
(with-handlers ((~? (~@ c.handlers ...)) |
(~? m.handler)) |
body ...)) |
(~? f.handler void))]) |
(module+ test |
(require racket |
rackunit) |
(check-equal? |
(try 1) |
1) |
(check-equal? |
(try (/ 1 0) |
(catch [exn:fail? e (exn-message e)])) |
"/: division by zero") |
(check-equal? |
(with-output-to-string |
(thunk |
(check-equal? |
(try 1 |
(finally (displayln "cleaning up"))) |
1))) |
"cleaning up\n") |
(check-equal? |
(with-output-to-string |
(thunk |
(check-equal? |
(try (/ 1 0) |
(catch [exn:fail? _ 0]) |
(finally (displayln "cleaning up"))) |
0))) |
"cleaning up\n") |
(check-equal? |
(try (/ 1 0) |
(catch/match [(? exn:fail? e) (exn-message e)])) |
"/: division by zero") |
(struct posn [x y]) |
(check-equal? |
(try (raise (posn 1 2)) |
(catch/match [(posn 1 y) y])) |
2) |
(check-equal? |
(try (raise (posn 1 2)) |
(catch [exn? e (exn-message e)]) |
(catch/match [(posn 1 y) y])) |
2) |
(check-equal? |
(with-output-to-string |
(thunk |
(check-equal? |
(try 1 |
(finally (displayln "cleaning up"))) |
1))) |
"cleaning up\n") |
(check-equal? |
(with-output-to-string |
(thunk |
(check-equal? |
(try (/ 1 0) |
(catch/match [(? exn:fail?) 0]) |
(finally (displayln "cleaning up"))) |
0))) |
"cleaning up\n")) |
2 License and Acknowledgements
The code is licensed with the MIT license of the Racket project. The text of this documentation is licensed with the CCA 4.0 International License.
sarna, for suggesting the macro
Alex Knauth and SamPh, for improving the dynamic-wind usage with call-with-continuation-barrier
notjack, for the excellent suggestion to add match forms to catch clauses