制作球拍宏以生成lambda

时间:2019-04-29 06:04:22

标签: lambda macros racket hygiene

我正在尝试制作一个小外壳,用于对csv文件执行类似sql的查询(出于好奇并尝试学习Racket)。为此,我想用这种粗略的结构实现一个select宏(我计划将x作为数据库的列,但现在只传递了一行):

(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

我发现了this example of a let-like macro

(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示例?

2 个答案:

答案 0 :(得分:3)

就像您猜到的那样,问题出在卫生上。

let示例之所以起作用,是因为给定主体内使用的标识符将传递给宏。

但是,如果您尝试在体内定义x标识符,而又没有让人们真正地明确地了解,那么您正在破坏卫生习惯(插入任意绑定在范围内)。

您要创建的内容称为 anaphoric 宏。 幸运的是,球拍有您需要做的一个。

语法参数

如果您以前曾使用过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>)]))

这将在condition周围创建一个新范围,其中<x>绑定到lambda中的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))

答案 1 :(得分: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))。球拍可能会将其重命名为(my-lambda x$0 (+ x$0 1))。扩展宏,我们得到(lambda (x$0) (+ x$0 1))

在第二种情况下,您致电(my-lambda* (+ x 1))。球拍可能会将其重命名为(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和下面的评论,以了解另一种破坏卫生的方法。