简体   繁体   中英

Making a racket macro that generates a lambda

I am trying to make a small shell for doing sql-like queries on csv files (out of curiosity and as an attempt to learn Racket). For this I wanted to implement a select macro with this rough structure (where I planned to have x be the columns of the db but just passed a single row for now):

(define-syntax select
  (syntax-rules (* from where)
    ((_ col1 ... from db where condition)
     (filter (lambda (x) condition) <minutiae>))))

(Where minutiae is file IO and pipe code)

The scope for x is not what I thought it to be:

x: undefined;
 cannot reference an identifier before its definition

I found this example of a let-like macro :

(define-syntax my-let*
  (syntax-rules ()
    ((_ ((binding expression) ...) body ...)
     (let ()
       (define binding expression) ...
       body ...))))

And then I proceeded to try to just produce the lambda like so:

(define-syntax my-lambda
  (syntax-rules ()
    ((_ body)
     (lambda (x)
       body))))

And then trying to mimic the structure of the let example:

(define-syntax my-lambda
  (syntax-rules ()
    ((_ body)
     (lambda (x_)
       (let ()
         (define x x_)
         body)))))

Both of these gave me the same error upon invocation ((my-lambda (+ x 1)) 0) :

x: undefined;
 cannot reference an identifier before its definition

According to my reading this is due to hygiene, but I can't seem to grasp it well enough to solve this on my own. What am I doing wrong and how would one define these kinds of macros? Why is the let example working but not the lambda one?

Like you guessed, the issue is about hygiene.

The let example works because the identifiers used inside the given body are passed along to the macro.

But If you try to define an x identifier inside the body, without having the person writing the body actually know explicitely about it, then you are breaking hygiene (inserting an arbitrary binding inside the scope).

What you want to create is called an anaphoric macro. Fortunately, Racket has what you need to make one.

The syntax-parameter

If you ever used Racket parameters before, it work a bit the same, but for macros.

(define-syntax-parameter <x>
  (lambda (stx)
    (raise-syntax-error '<x> "Used outside select macro." stx)))

This will define a parameter called <x> that your macro users will be able to use inside your select macro. To prevent it from being used outside, by default, the parameter is configured to raise a syntax error.

To define the only place in which it can be used, you call syntax-parameterize :

(define-syntax select
  (syntax-rules (* from where)
    [(_ col1 ... from db where condition)
     (findf
       (lambda (x)
         (syntax-parameterize ([<x> (make-rename-transformer #'x)])
           condition))
       <minutiae>)]))

This will create a new scope around condition in which <x> is bound to the x in lambda.

You can then call your macro like so:

(select * from db where (eq? <x> 'foo))

If you try using <x> outside of your macro, you'll get a syntax error:

> (displayln <x>)
<x>: Used outside select macro.
  in: <x>

Full code

#lang racket/base

(require
  (for-syntax racket/base)
  racket/stxparam)

(define-syntax-parameter <x>
  (lambda (stx)
    (raise-syntax-error '<x> "Used outside select macro." stx)))

(define-syntax select
  (syntax-rules (* from where)
    [(_ col1 ... from db where condition)
     (findf
       (lambda (x)
         (syntax-parameterize ([<x> (make-rename-transformer #'x)])
           condition))
       db)]))

(module+ test
  (require rackunit)

  (define db '(foo bar baz))

  (check-equal? (select * from db where (eq? <x> 'foo)) 'foo)
  (check-equal? (select * from db where (eq? <x> 'bar)) 'bar)
  (check-equal? (select * from db where (eq? <x> 'boop)) #f))

This works:

(define-syntax my-lambda
  (syntax-rules ()
    ((_ x body)
     (lambda (x) body))))

(my-lambda x (+ x 1))

while this doesn't work:

(define-syntax my-lambda*
  (syntax-rules ()
    ((_ body)
     (lambda (x) body))))

(my-lambda* (+ x 1))

One thing that might help to understand the difference is that Racket can rename variables however it wants as long as the renaming is consistent (it's actually pretty hard to define the word consistent here, but intuitively it should make sense to you). Racket needs to be able to rename to preserve hygiene.

In the first case, you call (my-lambda x (+ x 1)) . Racket might rename it to (my-lambda x$0 (+ x$0 1)) . Expanding the macro, we get (lambda (x$0) (+ x$0 1)) which is valid.

In the second case, you call (my-lambda* (+ x 1)) . Racket might rename it to (my-lambda* (+ x$0 1)) . Expanding the macro, we get (lambda (x) (+ x$0 1)) , so x$0 is unbound.

(Note that I am simplifying things a lot. In fact, Racket needs to expand the macro in the first case to know that it needs to rename both x in the same time.)

To get what you want, you need to break hygiene. Here's one simple way to do so:

(require syntax/parse/define)

(define-simple-macro (my-lambda** body)
  #:with unhygiene-x (datum->syntax this-syntax 'x)
  (lambda (unhygiene-x) body))

(my-lambda** (+ x 1))

Also see https://stackoverflow.com/a/55899542/718349 and the comment below for an alternative way to break hygiene.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM