syntax-objects / Summer2021

Syntax Parse Bee 2021
11 stars 3 forks source link

try—a try/catch/finally for sarna #9

Open benknoble opened 3 years ago

benknoble commented 3 years ago

See also https://github.com/benknoble/try-make-sarna-happy and the docs on the package server (when ready).

Macro

#lang racket/base

(provide try)

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

(begin-for-syntax
  (define-syntax-class catch-clause
    #:attributes ((pred 1) (name 1) (body 2))
    #:datum-literals (catch)
    (pattern (catch ([(pred:expr name:id) body:expr ...+] ...))))

  (define-syntax-class finally-clause
    #:attributes ((body 1))
    #:datum-literals (finally)
    (pattern (finally body:expr ...+))))

;; 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
  [(_ {~and body:expr {~not _:catch-clause} {~not _:finally-clause}} ...+
      {~optional c:catch-clause}
      {~optional f:finally-clause})
   #'(call-with-try-finally
       (λ ()
         (with-handlers ((~? (~@ [c.pred (λ (c.name) c.body ...)] ...)))
           body ...))
       (~? (λ () f.body ...) 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"))

with-handlers and dynamic-wind can be used to implement catch and finally, respectively, but in the traditional Lisp-like format they read "backwards"—handlers are shown long before the actual "main code." For dynamic-wind this may not be terrible (see go's defer func for a similar finally clause that comes ahead of the code). In general, though this is hard to read.

try flips the script: it presents the body code first, a series of exception-handlers in an optional catch, and an optional finally block.

Example

Here are some examples from the scribble documentation:

> (try
    (/ 10 0)
    (catch ([(exn? e) (exn-message e)])))
"/: division by zero"

> (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

Before and After

  • Code Cleaning : Please share the code that you used to write before creating your macro. Briefly explain how the code works.

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.

There are many before/after examples in the docs.

; before
> (let ([resource (get-handle)])
    (dynamic-wind
      void
      (λ () (with-handlers ([exn? (λ (e) (displayln (exn-message e)))])
              (use-might-break resource)))
      (λ () (close resource)))
    (is-closed? resource))
use-might-break: something went wrong
#t

; after
> (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

Licence

I affirm that I am submitting this code under the same MIT License that the Racket language uses. https://github.com/racket/racket/blob/master/racket/src/LICENSE-MIT.txt and that the associated text is licensed under the Creative Commons Attribution 4.0 International License http://creativecommons.org/licenses/by/4.0/

benknoble commented 3 years ago

The syntax has undergone a slight adjustment (less parens).

I've also added catch/match for using match patterns.

benknoble commented 3 years ago

docs and impl have been improved, but the concepts remain fairly similar

spdegabrielle commented 3 years ago

Thank you for your contribution!

If you haven’t already please take the time to fill in the form https://forms.gle/Z5CN2xzK13dfkBnF7

Bw Stephen