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

   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
   
<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+unsubscr...@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.

Reply via email to