Syntax parameter encapsulation

For a long time now, when I've wanted an "anaphoric macro" I've used syntax parameters. Consider a library that defines a define/return macro for defining functions that can be short circuited using return. The way I'd normally implement that is like this:

(provide define/return
         return)

(define-syntax-parameter return
  (λ (stx)
    (raise-syntax-error 'return "must be used inside define/return" stx)))

(define-syntax-parse-rule (define/return header:expr body:expr ...+)
  (define header
    (let/ec escape
      (syntax-parameterize ([return (make-rename-transformer #'escape)])
        body ...))))

This works, but it means that return is exposed as a syntax parameter to clients of the library. Someone could do this:

(require "my-awesome-return-library.rkt")

(define (explode)
  (error "everything exploded"))

(define/return (foo)
  (syntax-parameterize ([return (make-rename-transformer #'explode)])
    (return)))

This fails with the error everything exploded, which is a bit weird. I'm not sure clients should be allowed to redefine return out from under themselves like that.

The define/return library can put in a little work to prevent this however. The key lies in the observation that only the state passed from define/return to return needs to be a syntax parameter, not the entire return form. So the library could do this:

(provide define/return
         return)

(define-syntax-parameter return-continuation #false)

(define-syntax-parse-rule (return expr:expr ...)
  #:fail-unless (syntax-parameter-value #'return-continuation)
  "must be used inside define/return"
  #:with escape (syntax-parameter-value #'return-continuation)
  (escape expr ...))

(define-syntax-parse-rule (define/return header:expr body:expr ...+)
  (define header
    (let/ec escape
      (syntax-parameterize ([return-continuation #'escape])
        body ...))))

And now clients can't redefine return. The syntax parameter used to communicate between define/return and return is fully encapsulated.

Lately I've come around to believing that this is a Good Idea™ and maybe even a Best Practice™. Does anyone else do this?

What do you think about the following code? Should it fail?

(define/return (foo)
  (let ([return explode])
    (return)))

That ought to explode, but in the same way it would if you used any other form name. It's just ordinary lexical scoping:

(define (foo)
  (let ([define explode])
    (define)))

The reason syntax parameters are particularly dangerous is that people can write macros on top of them that are affected by parameterization, like this:

(define-syntax-parse-rule (return-when condition:expr)
  (when condition
    (return)))

(define/return (foo)
  (return-when (should-return?))
  (displayln "foo"))

In that case, reparameterizing return will change the behavior of return-when. In the case of more complex macros, that can cause weird and confusing breakages. This breakage doesn't occur with a simple let-based shadowing of return.

Arguably also a ‘feature’:

(module client racket

  (provide foo)

  (require (except-in (submod ".." my-awesome-return-library) return))
  (require [rename-in (submod ".." my-awesome-return-library) [return return0]])
  (require racket/stxparam)
  
  (define-syntax-parameter return
    (λ (stx)
      (raise-syntax-error 'return "must be used inside define/return" stx)))
  
  (define/return (foo)
    (syntax-parameterize ([return (λ (stx) (syntax-case stx () [(_ x) #'(return0 (begin (eprintf "log ~a\n" x) x))]))])
      (return 1))))

(require 'client)

[foo]

Obscure but I bet you have thought of this too. — Matthias

Based on Matthias’s reply, I’d say it’s a question of what you want to expose: an extremely flexible but possibly dangerous or confusing interface, or a rigid but easier to use as intended interface? Different libraries or projects or people make different tradeoffs.

A middle option is to document return as syntax, not a syntax parameter, and if someone has problems when they syntax-parameterize it, well, they’re holding it wrong. They can if they want, but it’s not expected that the library supports whatever weird thing they’re doing.

Apropos variations of syntax parameters:
https://docs.racket-lang.org/syntax-implicits/index.html