Help implementing an early return macro

142 views
Skip to first unread message

Jack Firth

unread,
Oct 28, 2020, 6:54:44 AM10/28/20
to Racket Users

So I'm a little tired of writing code like this:

(define x ...)
(cond
  [(take-shortcut? x) (shortcut x)]
  [else
   (define y (compute-y x))
   (cond
    [(take-other-shortcut? x y) (other-shortcut x y)]
    [else
     (define z ...)
     (cond ...)])])

That is, I have some logic and that logic occasionally checks for conditions that make the rest of the logic irrelevant, such as an empty or false input or something else that should trigger an early exit. Each check like this requires me to write a cond whose else clause wraps the remainder of the body, leading to an awkward nesting of cond forms. I don't have this issue when the early exits involve raising exceptions: in those cases I can just use when and unless like so:

(define x ...)
(unless (passes-check? x) (raise ...))
(define y ...)
(unless (passes-other-check? x y) (raise ...))
(define z ...)
...

I'm aware of a few macros in the racket ecosystem that try to solve this problem. For example, Jay wrote a blog post that creates a condd form that's like cond but allows embedded definitions using a #:do keyword. I've also seen various approaches that use escape continuations to implement the early exit. There's drawbacks I'm not happy about however:

  • For cond-like macros that allow embedded definitions, it looks too different from regular straight-line Racket code. I like my function bodies to be a sequence of definitions and expressions, with minimal nesting, just like the when and unless version above. I don't have to use a keyword or extra parentheses to signal whether a form is a definition or a when / unless check in error-raising code, why should I have to do that in code that uses early returns?

  • Continuation-based solutions impose a nontrivial performance penalty and have complex semantics. I don't like that the generated code behaves differently from the cond tree I would normally write. What happens if I stick an early exit inside a lambda? Or a thread? What if I set up a continuation barrier? Does that matter? I don't know and I don't want to think about that just to write what would be a simple if (condition) { return ... } block in other languages.

So I wrote a basic macro for this and I have some questions about how to make it more robust. The macro is called guarded-block and it looks like this:

(guarded-block
  (define x (random 10))
  (guard (even? x) else
    (log-info "x wasn't even, x = ~a" x)
    -1)
  (define y (random 10))
  (guard (even? y) else
    (log-info "y wasn't even, y = ~a" y)
    -1)
  (+ x y))

Each guard clause contains a condition that must be true for evaluation to proceed, and if it isn't true the block takes the else branch and finishes. So the above would expand into this:

(block
  (define x (random 10))
  (cond
    [(not (even? x))
     (log-info "x wasn't even, x = ~a" x)
     -1]
    [else
     (define y (random 10))
     (cond
       [(not (even? y))
        (log-info "y wasn't even, y = ~a" y)
        -1]
       [else (+ x y)])]))

This part I got working pretty easily. Where I hit problems, and where I'd like some help, is trying to extend this to support two important features:

  • I should be able to define macros that expand into guard clauses. This is important because I want to implement a (guard-match <pattern> <expression> else <failure-body> ...) form that's like match-define but with an early exit if the pattern match fails. I'd also really like to add a simple (guard-define <id> <option-expression> else <failure-body> ...) form that expects option-expression to produce an option (a value that is either (present v) or absent) and tries to unwrap it, like the guard let construct in Swift.

  • Begin splicing. The begin form should splice guard statements into the surrounding body. This is really an offshoot of the first requirement, since implementing macros that expand to guard can involve expanding into code like (begin (define some-temp-value ...) (guard ...) (define some-result ...)).

Having been around the Racket macro block before, I know I need to do some kind of partial expansion here. But honestly I can't figure out how to use local-expand, syntax-local-context, syntax-local-make-definition-context, and the zoo of related tools. Can someone point me to some existing macros that implement similar behavior? Or does anyone have general advice about what to do here? I'm happy to share more examples of use cases I have for guarded-block if that helps.

Alex Harsanyi

unread,
Oct 28, 2020, 7:23:05 AM10/28/20
to Racket Users
Are you looking for `let/ec`?

(let/ec return
  (define x (random 10))
  (unless (even? x)

    (log-info "x wasn't even, x = ~a" x)
    (return -1))
  (define y (random 10))
  (unless (even? y)

    (log-info "y wasn't even, y = ~a" y)
    (return -1))
  (+ x y))

Alex.

Laurent

unread,
Oct 28, 2020, 7:25:53 AM10/28/20
to Jack Firth, Racket Users
I've also had the same issues for a long time, and condd was almost good enough, but the #:do is too specific.
But recently I'm using something much simpler (no continuation) and straightforward: cond/else

By contrast to other approaches—which I usually try a few times then discard—I'm using this form regularly. 

It does precisely what you request (early return without continuations), but keeps both the readable cond and else structure while removing all unnecessary parentheses.
Your example would look like this:

(define x (random 10))
(cond/else

  [(not (even? x))
   (log-info "x wasn't even, x = ~a" x)
   -1]
  #:else
  (define y (random 10))
  #:cond

  [(not (even? y))
   (log-info "y wasn't even, y = ~a" y)
   -1]
  #:else
  (+ x y))


You say you don't like the keywords, but I find that they actually increase readability.

--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CAAXAoJVXCRo1CTk8rNDHmPZR_%2BhkMg2f%3DCb8T%3D8KFONUiM%2B_Hw%40mail.gmail.com.

Philip McGrath

unread,
Oct 28, 2020, 7:53:35 AM10/28/20
to Laurent, Jack Firth, Racket Users
The most similar example that comes to mind is the way `for`-like forms handle the `body-or-break` nonterminal to support `#:break` and `#:final`. In particular, I think you would end up needing something analogous to these semantics for definitions:
Among the `body`s, besides stopping the iteration and preventing later `body` evaluations, a `#:break guard-expr` or `#:final guard-expr` clause starts a new internal-definition context.

Personally, I'd also want to call your `guard` form something like `return-when` and `return-unless`, with the relationship between the condition expression and the right-hand side being the same as `when` and `unless`.

-Philip


Ryan Culpepper

unread,
Oct 28, 2020, 9:28:45 AM10/28/20
to Jack Firth, Racket Users
This is a nice example of a macro design pattern that I think of as "partial expansion with trampolining". You don't need to deal with the internal definition context API, because you can return definitions to the macro expander, let it handle their interpretation, and then resume your work. Here's an implementation sketch:

1. First, make sure you're in an internal definition context: (guarded-block form ...) => (let () (guarded-block* form ...)). The guarded-block* helper macro has the invariant that it is always used in internal definition context.
2. If guarded-block* has at least one form, it partially expands it, using a stop list containing guard and Racket's primitive syntactic forms. Then it analyzes the partially-expanded form:
- If it is a begin, it recurs with the begin's contents appended to the rest of its argument forms.
- If it is a define-values or define-syntaxes form, it expands into (begin defn (guarded-block* form-rest ...)). The macro expander interprets the definition, adds it to the environment, etc. Then guarded-block* resumes with the rest of the forms (in the same definition context).
- If it is a guard form, then you transform its contents and the rest of the forms into a cond expression, with a recursive call in the right place.
- Anything else, assume it's an expression, and trampoline the same as for a definition.

Also, because you're calling local-expand, you should disarm the result of local-expand and then call syntax-protect on the syntax you produce. If you don't disarm, then you might get "cannot use identifier tainted by macro transformer" errors. If you don't call syntax-protect, your macro can be misused to circumvent other macros' protection.

I've attached an implementation.

Ryan


--
guarded-block.rkt

Sam Caldwell

unread,
Oct 28, 2020, 9:45:22 AM10/28/20
to ry...@racket-lang.org, Jack Firth, Racket Users
Ryan's solution is almost certain to be nicer, but if you do find yourself needing internal definition contexts now or in the future, this is similar to a case I ran into while adding `define` to a language implemented with Turnstile.

I wrote a blog post outlining the solution [1], which I believe implements the kind of local-expand loop asked about. You can skip to the "Internal Definition Contexts" section if you don't care about the particulars of Turnstile. There's some extra machinery for dealing with Turnstile-specific things, but they should be pretty easy to remove and the basic ideas apply. I didn't include splicing begins in the post, but it's comparatively straightforward and in the actual implementation [2].

-Sam Caldwell

[1] http://prl.ccs.neu.edu/blog/2018/10/22/defining-local-bindings-in-turnstile-languages/
[2] https://github.com/tonyg/syndicate/blob/a6fc1f20e41fba49dc70d38b8c5047298e4b1811/racket/typed/core-types.rkt#L1220

Hendrik Boom

unread,
Oct 28, 2020, 9:48:39 AM10/28/20
to Racket Users
On Wed, Oct 28, 2020 at 03:54:29AM -0700, Jack Firth wrote:
> So I'm a little tired of writing code like this:
>
> (define x ...)
> (cond
> [(take-shortcut? x) (shortcut x)]
> [else
> (define y (compute-y x))
> (cond
> [(take-other-shortcut? x y) (other-shortcut x y)]
> [else
> (define z ...)
> (cond ...)])])

Perhaps you could use parendown
https://docs.racket-lang.org/parendown/index.html

#lang parendown racket/base
(define x ...)
(cond
[(take-shortcut? x) (shortcut x)]
#/ else
(define y (compute-y x))
#/ cond
[(take-other-shortcut? x y) (other-shortcut x y)]
#/ else
(define z ...)
#/ cond ...
)

>
> That is,
Frequently the lase element of a list is another list, and a large one
at that.
Using #/ makes that a kind of tail-recursive syntax and eliminates some
explicit parentheses.

Of course when you start using this it becomes so common that you'd like
to drop the ugly #'s, but unfortunately, / is already taken.

-- hendrik

David Storrs

unread,
Oct 28, 2020, 1:24:46 PM10/28/20
to Racket Users
I'm not sure if this is exactly what you want, but the handy module (which I still need to split up into less of a Fibber McGee) includes handy/try.  This would let you do the following:

#lang racket

(require handy/try)

(define x (random 100))

; these are obviously silly functions that are only for the sake of example
(define (fails-first-check? x)   (= 0 (modulo x 10)))
(define (fails-second-check? x)  (= 0 (modulo x 7)))
(define (calculate-final-result z) 'final-result)

(displayln
 (try [(displayln (~a "x is: " x))
       (when (fails-first-check? x) (raise 'failed-first-check))
       (when (fails-second-check? x) (raise 'failed-second-check))
       (define z 'z)
       ; ...do something with z                                                                
       (calculate-final-result z)]
      [catch
        ((curry equal? 'failed-first-check)   (lambda (e) "failure #1"))
        ((curry equal? 'failed-second-check)  (lambda (e) "failure #2"))
        (any/c                                (lambda (e) 'last-chance-processing-here))]))

It also supports pre and post checks:

#lang racket

(require handy/try)

(define x (random 100))

; real code would do something more sensible                                                  
(define (fails-first-check?     x) (= 0 (modulo x 10)))
(define (fails-second-check?    x) (= 0 (modulo x 7)))
(define (calculate-final-result z) 'final-result)

(displayln
 (try [pre (displayln "pre checks are guaranteed to happen")]
      [(displayln (~a "x is: " x))
       (when (fails-first-check? x) (raise 'failed-first-check))
       (when (fails-second-check? x) (raise 'failed-second-check))
       (define z 'z)
       ; ...do something with z                                                                
       (calculate-final-result z)]
      [catch
          ((curry equal? 'failed-first-check)   (lambda (e) "failure #1"))
          ((curry equal? 'failed-second-check)  (lambda (e) "failure #2"))
          (any/c                                (lambda (e) 'last-chance-processing-here))]
      [finally
       (displayln "do final cleanup here -- delete temp files, close sockets, etc")
       (displayln "this is guaranteed to run even if an uncaught exception is raised")
       ]))

The main body is required but 'pre', 'catch', and 'finally' are all optional.  The whole thing is a dynamic-wind wrapped around a with-handlers so pre and finally are guaranteed to execute no matter what.  pre is out of scope for the main body which is suboptimal but if there was interest then I might see about fixing that.

Unfortunately, like many of the submodules in handy 'try' has extensive documentation in the comments but not in Scribble.  Again, I should get around to that in my Copious Free Time.  For now, it's here: https://github.com/dstorrs/racket-dstorrs-libs/blob/master/try.rkt



--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.

Dominik Pantůček

unread,
Oct 28, 2020, 2:15:53 PM10/28/20
to racket...@googlegroups.com
Hi racketeers,

I would second this suggestion. Although it might look slightly
un-rackety at first sight, for certain types of code flow it does the
job really well.

And most importantly - I am using escape continuations in much wilder
setup (yes, futures) and it imposes no noticeable performance impact. It
is still on my TODO list to measure that empirically though. On CS that
is - on BC I didn't have time to test it yet.


Dominik

P.S.: Thanks go out to Matthew for pointing ECs out at RacketCon
informal discussions. Futures with generic CC come with absurdly huge
performance penalty even if they stay within the future thread ...
> <http://jeapostrophe.github.io/2013-11-12-condd-post.html> that
> creates a |condd| form that's like |cond| but allows embedded
> definitions using a |#:do| keyword. I've also seen various
> approaches that use escape continuations to implement the early
> exit. There's drawbacks I'm not happy about however:
>
> *
>
> For |cond|-like macros that allow embedded definitions, it looks
> too different from regular straight-line Racket code. I like my
> function bodies to be a sequence of definitions and expressions,
> with minimal nesting, just like the |when| and |unless| version
> above. I don't have to use a keyword or extra parentheses to
> signal whether a form is a definition or a |when| / |unless|
> check in error-raising code, why should I have to do that in
> code that uses early returns?
>
> *
> *
>
> I should be able to define macros that /expand/ into |guard|
> clauses. This is important because I want to implement a
> |(guard-match <pattern> <expression> else <failure-body> ...)|
> form that's like |match-define| but with an early exit if the
> pattern match fails. I'd also really like to add a simple
> |(guard-define <id> <option-expression> else <failure-body>
> ...)| form that expects |option-expression| to produce an option
> <https://gist.github.com/jackfirth/docs.racket-lang.org/rebellion/Option_Values.html>
> (a value that is either |(present v)| or |absent|) and tries to
> unwrap it, like the |guard let| construct in Swift
> <https://www.hackingwithswift.com/sixty/10/3/unwrapping-with-guard>.
>
> *
>
> Begin splicing. The |begin| form should splice |guard|
> statements into the surrounding body. This is really an offshoot
> of the first requirement, since implementing macros that expand
> to |guard| can involve expanding into code like |(begin (define
> some-temp-value ...) (guard ...) (define some-result ...))|.
>
> Having been around the Racket macro block before, I know I need to
> do some kind of partial expansion here. But honestly I can't figure
> out how to use |local-expand|, |syntax-local-context|,
> |syntax-local-make-definition-context|, and the zoo of related
> tools. Can someone point me to some existing macros that implement
> similar behavior? Or does anyone have general advice about what to
> do here? I'm happy to share more examples of use cases I have for
> |guarded-block| if that helps.
>
> --
> You received this message because you are subscribed to the Google
> Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send
> an email to racket-users...@googlegroups.com
> <mailto:racket-users...@googlegroups.com>.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/racket-users/29552b1b-39e6-4608-86ae-466194f44263n%40googlegroups.com
> <https://groups.google.com/d/msgid/racket-users/29552b1b-39e6-4608-86ae-466194f44263n%40googlegroups.com?utm_medium=email&utm_source=footer>.

jackh...@gmail.com

unread,
Oct 31, 2020, 6:25:32 AM10/31/20
to Racket Users
Wow, these are a lot of great responses. First of all, awesome job Ryan. That implementation is exactly what I needed to figure out. I'm definitely starting there first.


> Are you looking for `let/ec`?

I'd forgotten about that one. That has the syntax I want. However my issue with continuation-based approaches isn't the syntax, or even the performance. It's the semantics. What if someone writes code like this?


(guarded-block
  (define x (random 10))
  (thread
    (lambda ()
      (guard (even? x) else #false)
      ...)))

If I implemented guarded-block in terms of let/ec, then what does this code even do? I honestly don't know. It would probably run without error and do... something. I am extremely sure that regardless of what it did, it would be confusing and it wouldn't solve any problem I had. I just flat out don't want to allow this or any related nonsense, such as:

; Aliasing
(define return guard)

; Higher-order usage
(map guard (some-list ...))

; Capturing via closure
(guarded-block
  (define (check-foo x) (guard (foo? x) else #false))
  (check-foo ...)
  ...)

; Capturing via mutation
(set! previous-guard guard)

; Oh great, now I have to think about even more continuation jumps
(dynamic-wind
  (lambda () (guard ...))
  (lambda () ...)
  (lambda () ...))

There might be valid use cases for some of these, but I certainly don't understand those use cases well enough to commit to a semantics for them.

As for why not use condd, cond/else, or parendown: because they don't look right. Specifically, I often write code like this:

(define (f x y z)
  ... a few lines of checking preconditions ...
  ... a dozen or two lines of actual useful logic ...)

I don't want to indent the useful logic a bunch. That's the most important part of the function - the preconditions are minor things that you should be able to skim over and forget about. If my functions without preconditions look wildly different from my functions with preconditions, it becomes difficult to tell what the main focus of a function is. Not to mention that going from zero preconditions to one (and from one to zero) now introduces a bunch of busywork.

On naming: I like the symmetry between return-when / return-unless and when / unless, but the problem is the word "return". If I call it a return statement, people will naturally expect this to work:

(define (first-owl animals)
  (for ([animal animals])
    (return-when (owl? animal) animal)))

I don't want to have to explain why that doesn't work forty times.

The names guard-when and guard-unless are kind of ambiguous; does "guard-when" mean "when this condition is true, enter this guard block and escape" or does it mean "guard the function with this condition, if it fails enter the block and escape". Does "guard" mean "prevent this condition" or "ensure this condition"? Having two forms means you have to remember which one means which and figure out a way to keep them straight. I'd rather just have one form so there's only one way to do it, and you can teach yourself that "ah yes, guard always means 'make sure this is true'".

George Neuner

unread,
Nov 1, 2020, 4:04:15 PM11/1/20
to racket...@googlegroups.com
On Sat, 31 Oct 2020 03:25:32 -0700 (PDT),
"jackh...@gmail.com"
<jackh...@gmail.com> wrote:

>Wow, these are a lot of great responses. First of all, *awesome* job Ryan.
>That implementation is exactly what I needed to figure out. I'm definitely
>starting there first.
>
>> Are you looking for `let/ec`?
>
>I'd forgotten about that one. That has the *syntax* I want. However my
>issue with continuation-based approaches isn't the syntax, or even the
>performance. It's the semantics. What if someone writes code like this?
>
>(guarded-block
> (define x (random 10))
> (thread
> (lambda ()
> (guard (even? x) else #false)
> ...)))
>
>If I implemented guarded-block in terms of let/ec, then what does this code
>even *do*? I honestly don't know. It would probably run without error and
>do... something.

(let/ec return
(thread
(lambda ()
(return -1)
42)))

throws an error: "continuation application: attempt to jump into an
escape continuation"

There is a continuation barrier between the threads.


However, let/cc works: e.g.,

(let/cc return
(thread
(lambda ()
(return -1)
42)))

returns -1.


>I am extremely sure that regardless of what it did, it
>would be confusing and it wouldn't solve any problem I had.

I am sure it would solve *some* problems.


>I just flat out don't want to allow this or any related nonsense,
>such as:
>
>; Aliasing
>(define return guard)
>
>; Higher-order usage
>(map guard (some-list ...))
>
>; Capturing via closure
>(guarded-block
> (define (check-foo x) (guard (foo? x) else #false))
> (check-foo ...)
> ...)
>
>; Capturing via mutation
>(set! previous-guard guard)
>
>; Oh great, now I have to think about even more continuation jumps
>(dynamic-wind
> (lambda () (guard ...))
> (lambda () ...)
> (lambda () ...))
>
>There might be valid use cases for some of these, but I certainly don't
>understand those use cases well enough to commit to a semantics for them.

It always is safe to jump upwards OUT of a lower level computation.
The sticky issues [and mental gyrations] with continuations all have
to do with jumping downwards or sideways.

I'm not sure what problems you might have with continuation barriers:
the example of the thread shows that (upward-only) "escape"
continuations don't work across threads ... but "full" continations do
work, and would still work even if the threads were siblings rather
than in a parent/child relationship.

The issue for your purpose would be making sure the continuation is
called from a position that is guaranteed to terminate the errant
thread: you might need to recognize a thread as a special case, wrap
it and (like an exception) catch/rethrow the continuation.


Or, if you don't care about sibling threads, just use exceptions which
always can be thrown upward out of child threads.

George

jackh...@gmail.com

unread,
Nov 4, 2020, 3:04:27 AM11/4/20
to Racket Users
A brief update on this: I went with Ryan's approach and used an implementation of guarded-block throughout Rebellion's codebase. You can see the diff here: https://github.com/jackfirth/rebellion/pull/466. A couple of things to note:

- I added a define/guard form that's like define, but with the function body implicitly wrapped with guarded-block.
- I tweaked the syntax of guard to allow either (guard <condition> else <body> ...) or (guard <condition> then <body> ...). If else is used, the guard is taken if the condition is false. If then is used, the guard is taken if the condition is true. This made it easier to organize my code such that the short-circuiting precondition checks came before what I considered to be the main branch of the function. With just the else case, sometimes I had to awkwardly negate things.

There's a few problems related to robustness I'd like to figure out eventually, before documenting this macro and publicly exposing it:

- It allows shadowing. If a variable is defined both before and after a guard statement, the definition after shadows the one before it, instead of raising a duplicate definition error.
- If any definitions raise syntax errors, the error is reported in terms of the expanded define-values form instead of the definition the user actually wrote.
- The last body form of a guarded block shouldn't be allowed to be a guard statement, and a good error message should be raised if a user does that. The current implementation doesn't check for that.

The last one is pretty easy to fix, but the other two I'm not sure how to fix.
Reply all
Reply to author
Forward
0 new messages