Extensible macro definitions

2020-07-08 07:13发布

问题:

Inspired by a comment thread on a related question regarding functions instead of macros.

Is there any way to extend a Scheme syntax definition so that it can use the previous definition of the syntax in the new definition? Furthermore, this must be extensible, that is, it must be possible to chain the technique together several times.

For example, say we want to extend lambda so that every time a function defined with lambda is called, it prints "foo" before executing the function body. We can do this in the following way:

(define-syntax old-lambda lambda)

(define-syntax lambda
  (syntax-rules ()
    ((_ args body ...)
     (old-lambda args (display "foo") body ...))))

We can also extend this in another way (say, by printing "bar") by doing the following:

(define-syntax old-lambda-2 lambda)

(define-syntax lambda
  (syntax-rules ()
    ((_ args body ...)
     (old-lambda-2 args (display "bar") body ...))))

The end result being that functions defined with our new lambda will print "foo", then "bar" each time they are called.

However, besides polluting the namespace with lots of old-lambda-<x>, this necessitates making a new old-lambda-<x> at the source code level each time we do this; this cannot be automated since you can't, say, use gensym in the syntax definition either. Therefore, there's no good way to make this extensible; the only plausible solution is naming each one old-lambda-print-foo or something similar to disambiguate, which is obviously not a foolproof solution. (For an example of how this could fail, say two different parts of the code were to extend lambda to print "foo"; naturally, they would both name it old-lambda-print-foo, and voila! lambda is now an infinite loop.) Therefore, it would be very nice if we were able to do this in a way which ideally:

  • Doesn't require us to pollute the namespace with lots of old-lambda-<x>
  • Or, failing that, guarantees that we won't have collisions.

回答1:

In Racket, you can do this with modules. You could create a module that re-exports the entire Racket language except for Racket's lambda, and exports your new macro under the name lambda. I'll show one way to arrange the code.

The foo-lambda module defines and exports the foo-lambda form, which creates procedures that print "foo\n" when applied.

(module foo-lambda racket
  (define-syntax-rule (foo-lambda formals body ...)
    (lambda formals (displayln "foo") body ...))
  (provide foo-lambda))

The racket-with-foo-lambda module re-exports the entire Racket language except it provides foo-lambda under the name lambda.

(module racket-with-foo-lambda racket
  (require 'foo-lambda)
  (provide (except-out (all-from-out racket) lambda)
           (rename-out [foo-lambda lambda])))

Now you can write a module in this "new language":

(module some-program 'racket-with-foo-lambda
  (define f (lambda (x) x))
  (f 2))
(require 'some-program)

Note that this doesn't change the Racket version of lambda, and other Racket forms still use the Racket lambda binding. For example, if you rewrote the definition of f above as (define (f x) x), then Racket's define would expand into a use of Racket's lambda, and you would not get the "foo" printout.

You can chain extensions: each extension is defined in a module that imports the previous version. For example, your bar-lambda module would import the foo-lambda module, and so on.

Racket does this internally, in fact. The compiler only understands lambda with positional arguments, but the Racket language has a lambda that supports both positional and keyword arguments. The implementation of the Racket language has a module that replaces the built-in lambda and #%app (implicitly used to handle function application syntax) with versions that handle keyword arguments.