#lang racket/base
(provide try catch finally => _)
(require syntax/parse/define (for-syntax racket/base))
(begin-for-syntax
(define (not-allowed-as-an-expression stx)
(raise-syntax-error #f "not allowed as an expression" stx))
(define-syntax-class finally-clause #:literals [finally]
[pattern (finally e:expr ...+) #:with post-thunk #'(λ () e ...)])
(define-syntax-class catch-clause #:literals [catch => _]
[pattern (catch pred:expr => handler:expr)]
[pattern (catch (pred:expr x:id) b:expr ...+)
#:with handler #'(λ (x) b ...)]
[pattern (catch (x:id) b:expr ...+)
#:with pred #'void
#:with handler #'(λ (x) b ...)]
[pattern (catch _ b:expr ...+)
#:with pred #'void
#:with handler #'(λ (x) b ...)])
(define-syntax-class body #:literals [finally catch]
[pattern {~and :expr {~not {~or (finally . _) (catch . _)}}}]))
(define-syntax catch not-allowed-as-an-expression)
(define-syntax finally not-allowed-as-an-expression)
(define-syntax-parser try
[(_ b:body ...+ f:finally-clause)
#'(call-with-try-finally (λ () b ...) f.post-thunk)]
[(_ b:body ...+ c:catch-clause ...)
#'(with-handlers ([c.pred c.handler] ...) b ...)]
[(_ b:body ...+ c:catch-clause ... f:finally-clause)
#'(call-with-try-finally
(λ () (with-handlers ([c.pred c.handler] ...) b ...))
f.post-thunk)])
;; call-with-try-finally : [-> X] [-> Any] -> X
;; Calls value-thunk, then post-thunk, with post-thunk guaranteed to be run
;; even if execution exits value-thunk through an exception or continuation
(define (call-with-try-finally value-thunk post-thunk)
(call-with-continuation-barrier
(λ () (dynamic-wind void value-thunk post-thunk))))
Please explain the purpose of the macro.
Example
An example with try/catch:
> (try
(raise-syntax-error #f "a syntax error")
(catch (exn:fail:syntax? e)
(displayln "got a syntax error")))
got a syntax error
This runs a computation that raises a syntax exception, and catches that with a handler written after the main body, rather than written before as in with-handlers.
An example with try/finally:
> (let/cc up
(try
(displayln "at before")
(up (void))
(displayln "at after")
(finally (displayln "out"))))
at before
out
This runs a computation that exits due to a continuation jump, and still runs the "post-body" set up in the finally.
Before and After
Code Cleaning : Please share the code that you used to write before creating your macro. Briefly explain how the code works.
Similarly to #9 and #12, this also can replace uses of with-handlers with try/catch, and replace many uses of dynamic-wind with try/finally.
However, part of the reason I wanted to provide the same functionality as Gerbil Scheme was to compare my syntax-parse implementation to their procedural / syntax-case implementation, and therefore:
Macro Engineering : Please share the old macro that you revised. Briefly explain the changes.
Changes in supporting runtime function definitions.
a. My try macro doesn't need a supporting with-catch function because it can use a combination of the syntax/parse technique Variants with Uniform Meanings and the existing Racket with-handlers form.
b. Gerbil's supporting function with-unwind-protect uses mutable state to make a "one use" closure dynamically check that it's only called once. While my version, call-with-try-finally, uses a continuation-barrier as suggested by SamPh on Discord, to accomplish the same without mutable state.
Their supporting runtime functions take 11 sloc while mine take only 3 sloc, mostly due to Racket features such as with-handlers and continuation-barriers that Gerbil's version doesn't have.
Changes in the compile-time syntax transformation definitions.
a. My catch and finally literals use a compile-time helper function not-allowed-as-an-expression to get a better error message, while Gerbil's version uses a simple empty defrules to get a generic Bad syntax error message.
b. My try macro uses syntax-parse's ...+ to express one-or-more repetition, while Gerbil's version uses manual null? checks and stx-null? checks, seen in their generate-thunk helper function and their finally case.
c. My try macro uses a syntax-parse syntax-class for recognizing body expressions that aren't catch or finally clauses, using the ~not pattern to exclude them. While Gerbil's version uses a named let loop with 2 extra nested syntax-case expressions (beyond the normal syntax-case at the top) to separate body expressions from catch and finally clauses.
d. My try macro uses a syntax-parse syntax-class for handling catch clauses as Variants with Uniform Meanings allowing repetition with ellipses, while Gerbil's version uses the helper function generate-catch, with its own with-syntax, named let loop, match, and syntax-case expressions, in combination with yet another named let loop and another nested syntax-case expression (beyond the ones mentioned above) in the main body of the macro to separate the catch clauses from the finally clause.
e. My try macro uses a syntax-parse syntax-class for handling the finally clause, while Gerbil's version uses a helper function generate-fini and 2 different finally cases in different syntax-case expressions in the main body of the macro. One of these is to separate body expressions from finally in the case when there are no catches in between, and the other is to separate catch clauses from finally. Their first finally case requires a manual stx-null? check to make sure nothing comes after finally, while their second finally case encodes that into a syntax-case pattern for a 1-element list. Syntax-classes allow my try macro to express this simply by putting the finally-clause pattern at the end of the main syntax-pattern, after the previous patterns and their ellipses.
Their syntax definitions take 62 sloc while mine take 28 sloc, with syntax/parse features such as syntax-classes and better ellipsis support making it much shorter, as well as with-handlers allowing the output to be simpler and allowing the ellipsis repetitions of syntax-classes to do more of the work. I also subjectively find mine easier to read, though I am biased on that of course.
Macro
A
try
/catch
/finally
macro inspired by Gerbil Scheme'stry
macro in:std/sugar
.Source code: https://github.com/AlexKnauth/try-catch-finally/blob/main/try-catch-finally-lib/main.rkt
Documentation: https://docs.racket-lang.org/try-catch-finally/index.html
Example
An example with
try
/catch
:This runs a computation that raises a syntax exception, and catches that with a handler written after the main body, rather than written before as in
with-handlers
.An example with
try
/finally
:This runs a computation that exits due to a continuation jump, and still runs the "post-body" set up in the
finally
.Before and After
Similarly to #9 and #12, this also can replace uses of
with-handlers
withtry
/catch
, and replace many uses ofdynamic-wind
withtry
/finally
.However, part of the reason I wanted to provide the same functionality as Gerbil Scheme was to compare my
syntax-parse
implementation to their procedural /syntax-case
implementation, and therefore:Gerbil Scheme's
try
implementation is here: https://github.com/vyzo/gerbil/blob/fa9537be0848e54d2c68165503b9cc48babb9334/src/std/sugar.ss#L32-L97With supporting function definitions here: https://github.com/vyzo/gerbil/blob/17fbcb95a8302c0de3f88380be1a3eb6fe891b95/src/gerbil/runtime/gx-gambc0.scm#L1625-L1636
try
macro doesn't need a supportingwith-catch
function because it can use a combination of thesyntax/parse
technique Variants with Uniform Meanings and the existing Racketwith-handlers
form. b. Gerbil's supporting functionwith-unwind-protect
uses mutable state to make a "one use" closure dynamically check that it's only called once. While my version,call-with-try-finally
, uses a continuation-barrier as suggested by SamPh on Discord, to accomplish the same without mutable state.Their supporting runtime functions take 11 sloc while mine take only 3 sloc, mostly due to Racket features such as
with-handlers
and continuation-barriers that Gerbil's version doesn't have.catch
andfinally
literals use a compile-time helper functionnot-allowed-as-an-expression
to get a better error message, while Gerbil's version uses a simple emptydefrules
to get a genericBad syntax
error message. b. Mytry
macro uses syntax-parse's...+
to express one-or-more repetition, while Gerbil's version uses manualnull?
checks andstx-null?
checks, seen in theirgenerate-thunk
helper function and theirfinally
case. c. Mytry
macro uses a syntax-parsesyntax-class
for recognizingbody
expressions that aren'tcatch
orfinally
clauses, using the~not
pattern to exclude them. While Gerbil's version uses a namedlet
loop with 2 extra nestedsyntax-case
expressions (beyond the normalsyntax-case
at the top) to separate body expressions fromcatch
andfinally
clauses. d. Mytry
macro uses a syntax-parsesyntax-class
for handlingcatch
clauses as Variants with Uniform Meanings allowing repetition with ellipses, while Gerbil's version uses the helper functiongenerate-catch
, with its ownwith-syntax
, namedlet
loop,match
, andsyntax-case
expressions, in combination with yet another namedlet
loop and another nestedsyntax-case
expression (beyond the ones mentioned above) in the main body of the macro to separate thecatch
clauses from thefinally
clause. e. Mytry
macro uses a syntax-parsesyntax-class
for handling thefinally
clause, while Gerbil's version uses a helper functiongenerate-fini
and 2 differentfinally
cases in differentsyntax-case
expressions in the main body of the macro. One of these is to separatebody
expressions fromfinally
in the case when there are nocatch
es in between, and the other is to separatecatch
clauses fromfinally
. Their firstfinally
case requires a manualstx-null?
check to make sure nothing comes afterfinally
, while their secondfinally
case encodes that into asyntax-case
pattern for a 1-element list. Syntax-classes allow mytry
macro to express this simply by putting thefinally-clause
pattern at the end of the main syntax-pattern, after the previous patterns and their ellipses.Their syntax definitions take 62 sloc while mine take 28 sloc, with
syntax/parse
features such as syntax-classes and better ellipsis support making it much shorter, as well aswith-handlers
allowing the output to be simpler and allowing the ellipsis repetitions of syntax-classes to do more of the work. I also subjectively find mine easier to read, though I am biased on that of course.Licence
I confirm that I am submitting this code under the same MIT License that the Racket language uses. https://github.com/AlexKnauth/try-catch-finally/blob/main/LICENSE.txt and that the associated text is licensed under the Creative Commons Attribution 4.0 International License http://creativecommons.org/licenses/by/4.0/