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.
--
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.
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CABNTSaEkPD7603XL2WonXQf66cxvQJsD5svEcVR1t-%3DNgtqYWw%40mail.gmail.com.
--
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/CANy33qnzo2SEpPJjAcuLwhDkHPq_F94J2a-Sb2SUJgwLbEjG_w%40mail.gmail.com.
--
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/20201028134830.54plnv6nv5j3lcmt%40topoi.pooq.com.