1

我正在尝试制作一个小外壳来对 csv 文件进行类似 sql 的查询(出于好奇和尝试学习 Racket)。为此,我想用这种粗略的结构实现一个选择宏(我计划让 x 成为 db 的列,但现在只传递了一行):

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

(其中细节是文件 IO 和管道代码)

x 的范围不是我想的那样:

x: undefined;
 cannot reference an identifier before its definition

我发现了一个 let-like 宏的示例

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

然后我开始尝试像这样生成 lambda:

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

然后尝试模仿 let 示例的结构:

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

这两个在调用时都给了我同样的错误((my-lambda (+ x 1)) 0)

x: undefined;
 cannot reference an identifier before its definition

根据我的阅读,这是由于卫生问题,但我似乎无法很好地掌握它来自己解决这个问题。我做错了什么,如何定义这些宏?为什么 let 示例有效但 lambda 示例无效?

4

2 回答 2

4

就像你猜到的,问题是关于卫生的。

let 示例有效,因为在给定正文中使用的标识符被传递给宏。

但是,如果您尝试x在正文中定义一个标识符,而编写正文的人却没有明确地知道它,那么您就是在破坏卫生(在范围内插入任意绑定)。

您要创建的内容称为照应宏。幸运的是,Racket 有你需要的东西。

语法参数

如果你以前使用过 Racket 参数,它的工作方式有点相同,但适用于宏。

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

这将定义一个名为的参数<x>,您的宏用户将能够在您的select宏中使用该参数。为了防止它在外部使用,默认情况下,该参数被配置为引发语法错误。

要定义可以使用它的唯一位置,请调用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>)]))

这将创建一个绑定到conditionin lambda的新范围。<x>x

然后你可以像这样调用你的宏:

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

如果您尝试<x>在宏之外使用,您将收到语法错误:

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

完整代码

#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))
于 2019-04-29T08:28:17.220 回答
0

这有效:

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

(my-lambda x (+ x 1))

虽然这不起作用:

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

(my-lambda* (+ x 1))

可能有助于理解差异的一件事是,只要重命名是一致的,Racket 可以随意重命名变量(实际上在这里很难定义一致这个词,但直觉上它应该对你有意义)。球拍需要能够重命名以保持卫生。

在第一种情况下,您调用(my-lambda x (+ x 1)). Racket 可能会将其重命名为(my-lambda x$0 (+ x$0 1)). 展开宏,我们得到(lambda (x$0) (+ x$0 1))哪个是有效的。

在第二种情况下,您调用(my-lambda* (+ x 1)). Racket 可能会将其重命名为(my-lambda* (+ x$0 1)). 展开宏,我们得到(lambda (x) (+ x$0 1)),所以x$0是未绑定的。

(请注意,我简化了很多事情。事实上,Racket 需要在第一种情况下扩展宏才能知道它需要同时重命名两者x。)

为了得到你想要的,你需要打破卫生。这是一种简单的方法:

(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))

另请参阅https://stackoverflow.com/a/55899542/718349和下面的评论,了解另一种破坏卫生的方法。

于 2019-04-29T08:28:29.693 回答