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

On Wed, Oct 28, 2020 at 9:28 AM Ryan Culpepper <rmculpepp...@gmail.com>
wrote:

> 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
>
>
> On Wed, Oct 28, 2020 at 11:54 AM Jack Firth <jackhfi...@gmail.com> 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 ...)])])
>>
>> 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
>> <https://groups.google.com/d/msgid/racket-users/CAAXAoJVXCRo1CTk8rNDHmPZR_%2BhkMg2f%3DCb8T%3D8KFONUiM%2B_Hw%40mail.gmail.com?utm_medium=email&utm_source=footer>
>> .
>>
> --
> 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/CANy33qnzo2SEpPJjAcuLwhDkHPq_F94J2a-Sb2SUJgwLbEjG_w%40mail.gmail.com
> <https://groups.google.com/d/msgid/racket-users/CANy33qnzo2SEpPJjAcuLwhDkHPq_F94J2a-Sb2SUJgwLbEjG_w%40mail.gmail.com?utm_medium=email&utm_source=footer>
> .
>

-- 
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/CALuKBHt_5SRaQmp7sBhD580PkunzWeqL%3D7fhyaL5T7znRwzH8A%40mail.gmail.com.

Reply via email to