23

希望这不是一个多余的问题。

作为方案的新手,我知道syntax-case宏比syntax-rules替代方案更强大,但代价是不必要的复杂性。

然而,是否有可能在方案中实现 Common Lisp 的宏系统,它比syntax-rules, using更强大syntax-case

4

3 回答 3

42

我会尽量简短——这很难,因为这通常是一个非常深刻的问题,超过问答的平均 SO 水平......所以这仍然会很长。我也会尽量保持公正;尽管我是从 Racket 的角度来看的,但我过去一直在使用 Common Lisp,而且我一直喜欢在两个世界(实际上在其他世界)中使用宏。它看起来不像是对您的问题的直接回答(在极端情况下,这只是“是”),但它正在比较两个系统,希望这将有助于为人们澄清这个问题——尤其是外面的人口齿不清(所有口味)谁会想知道为什么这么重要。我还将描述如何defmacro可以在“语法案例”系统中实现,但通常只是为了保持清楚(并且因为您可以找到这样的实现,一些在评论和其他答案中给出)。

首先,你的问题不是多余的——它是非常有道理的,而且(正如我所暗示的),Lisp 的 Scheme 新手和 Lisp 的 Scheme 新手遇到的事情之一。

其次,非常肤浅、非常简短的回答是人们告诉您的:是的,可以defmacro在支持 的 Scheme 中实现 CL syntax-case,并且正如预期的那样,您有多个指向此类实现的指针。反其道而行之,syntax-case使用 simple实现defmacro是一个比较棘手的主题,我不会过多讨论;我只想说它已经完成了,只是重新实现lambda和其他绑定结构的成本非常高,这意味着它基本上是对一种新语言的重新实现,如果你想使用这种实现,你应该承诺.

另一个澄清:人们,尤其是 CL 人员,经常崩溃与 Scheme 宏相关的两件事:卫生和syntax-rules. 问题是,在 R5RS 中,你所拥有的只是syntax-rules,这是一个非常有限的基于模式的重写系统。与其他重写系统一样,您可以天真地使用它,或者一直使用重写来定义一种小型语言,然后您可以使用它来编写宏。请参阅此文本有关如何完成此操作的已知解释。虽然可以做到这一点,但最重要的是这很难,你正在使用一些与你的实际语言没有直接关系的奇怪的小语言,这使得它与 Scheme 编程相去甚远——可能更糟在 CL 中使用卫生宏实现的方式并不是真正使用普通 CL。简而言之,可以使用 only syntax-rules,但这主要是理论上的,而不是您想要在“真实”代码中使用的东西。这里的要点是,卫生并不意味着仅限于syntax-rules.

然而,syntax-rules它并不是作为“方案”宏系统的——这个想法总是你有一些“低级”的宏实现,用于实现syntax-rules但也可以实现破坏卫生的宏——只是没有就特定的低级实现达成一致。R6RS 通过标准化“syntax-case”宏系统来解决这个问题(请注意,我使用“syntax-case”作为系统的名称,与syntax-case它的主要亮点不同的形式)。好像是为了表明讨论仍然存在,R7RS 退后一步将其排除在外,恢复到syntax-rules对低级系统没有承诺,至少就“小语言”而言。

现在,要真正理解这两个系统之间的区别,最好澄清一下它们所处理的类型之间的区别。有了defmacro,转换器基本上是一个接收 S 表达式并返回 S 表达式的函数。这里的 S 表达式是由一堆文字类型(数字、字符串、布尔值)、符号和它们的列表嵌套结构组成的类型。(实际使用的类型比这多一点,但这足以说明这一点。)问题是这是一个非常简单的世界:你得到的东西非常具体——你实际上可以打印输入/output 值,这就是你所拥有的。请注意,此系统使用符号表示标识符——符号在这个意义上是非常具体的东西:anx是一段只有那个名字的代码,x.

然而,这种简单性是有代价的:你不能将它用于卫生宏,因为你无法区分两个不同的标识符,它们都被称为x. 通常的基于 CL 的defmacro确实有一些额外的位来补偿其中的一些。一个这样的位是gensym- 一种创建“新鲜”符号的工具,这些符号是非内部的,因此保证与任何其他符号不同,包括具有相同名称的符号。另一个这样的位是转换器的&environment参数defmacro,它包含使用宏的位置的词法环境的一些表示。

很明显,这些事情使defmacro世界变得复杂,因为它不再处理普通的可打印值,并且因为您需要了解环境的某些表示 - 这使得宏实际上是一段代码更加清楚一个编译器钩子(因为这个环境本质上是编译器通常处理的某种数据类型,而且比 S 表达式更复杂)。但事实证明,它们不足以实施卫生。使用gensym您可以取消卫生的一个简单方面(避免宏端捕获用户代码),但另一方面(避免用户代码捕获宏代码)仍然保持开放。有些人对此感到满意,认为您可以避免的那种捕获就足够了 - 但是当您处理一个模块化系统时,其中宏的环境通常具有与其实现中使用的不同的绑定,另一面变成重要得多。

切换到语法案例宏系统(并愉快地跳过syntax-rules,这是使用 简单实现的syntax-case)。在这个系统中,如果简单的符号 S 表达式不足以表达完整的词汇知识(即两个不同绑定之间的差异,都称为x),那么我们将“丰富”它们并使用一种数据类型。(请注意,还有其他低级宏系统采用不同的方法来提供额外的信息,例如显式重命名和语法闭包。)

这样做的方法是使宏转换器成为消耗和返回“语法对象”的函数,这正是那种表示。更准确地说,这些语法对象通常建立在普通符号表示之上,仅包装在具有表示词法范围的附加信息的结构中。在某些系统中(特别是在 Racket 中),所有内容都包含在语法对象中——符号以及其他文字和列表。鉴于此,很容易从语法对象中获取 S 表达式也就不足为奇了:您只需提取符号内容,如果它是一个列表,则继续递归地执行此操作。在语法案例系统中,这是通过syntax-e实现语法对象的符号内容的访问器来完成的,并且syntax->datum它实现了递归降低结果的版本以生成完整的 S 表达式。作为旁注,这是一个粗略的解释,为什么在 Scheme 中人们不谈论将绑定表示为符号,而是表示为标识符

另一方面,问题是如何从给定的符号名称开始构造这样的语法对象。这样做的方法是使用datum->syntax函数——但不是让 api 指定如何表示词法范围信息,函数将语法对象作为第一个参数,将符号 S 表达式作为第二个参数,并且它通过使用从第一个获取的词法范围信息正确包装 S 表达式来创建一个语法对象。这意味着要打破卫生习惯,您通常会从用户提供的语法对象(例如,宏的主体形式)开始,并使用其词法信息创建一些新的标识符,就像this在同一范围内可见的那样。

这个简短的描述足以了解您看到的宏是如何工作的。@ChrisJester-Young 展示的宏只接受语法对象,将其剥离为原始 S 表达式syntax->datum,使用 将其发送到defmacro转换器并取回 S 表达式,然后syntax->datum将结果转换回使用用户代码的词法上下文的语法对象。球拍的defmacro实现有点花哨:在剥离阶段,它保留一个哈希表,将生成的 S 表达式映射到它们的原始语法对象,并且在重建步骤中,它查阅该表以获得与最初的代码位相同的上下文。这使得它对于一些更复杂的宏来说是一个更健壮的实现,但它在 Racket 中也更有用,因为语法对象中有更多的信息,比如源位置、属性等,这种仔细的重建通常会产生输出值(语法对象)保留他们在进入宏的途中所拥有的信息。

defmacro有关语法案例系统的程序员的技术性介绍,请参阅我的编写syntax-case博客文章。如果您来自 Scheme 方面,它不会那么有用,但它仍然有助于澄清整个问题。

为了更接近结论,我应该指出,处理不卫生的宏仍然很棘手。更具体地说,有多种方法可以实现这种绑定,但它们在各种微妙的方式上是不同的,通常会回来咬你,在每种情况下留下略微不同的牙印。在像 CL 这样的“真实”defmacro系统中,你学会忍受一组特定的牙痕,这些牙痕相对众所周知,因此有些事情你就是不做。最值得注意的是,Racket 经常使用的相同名称的语言具有不同绑定的模块化组合。在语法案例系统中,更好的方法是fluid-let-syntax它用于“调整”词法范围名称的含义——最近,它已经演变成“语法参数”。对破坏卫生的宏的问题有一个很好的概述,其中包括描述如何尝试使用 hygienic syntax-rules、基本语法案例、CL 样式defmacro以及最后使用语法参数来解决它。这篇文章有点技术性,但它的前几页相对容易阅读,如果你理解了这一点,那么你将对整个辩论有一个很好的了解。(还有一篇较旧的博客文章在本文中得到了更好的介绍。)

我还应该提到,这远非宏的唯一“热点”问题。Scheme 圈子中关于哪个低级宏观系统更好的争论有时会变得非常激烈。还有其他关于宏的问题,例如如何使它们在模块系统中工作,其中库可以提供宏以及值和函数,或者是否将宏扩展时间和运行时分成单独的阶段等等。

希望这可以更全面地了解该问题,以便了解权衡并能够自己决定什么对您最有效。我也希望这能澄清一些常见问题的根源:卫生宏当然不是无用的,但由于新类型不仅仅是简单的 S 表达式,它们周围还有更多的功能——而且往往很肤浅——阅读旁观者会得出“太复杂”的结论。更糟糕的是,“在 Scheme 世界中,人们对元编程几乎一无所知”的精神火焰:非常痛苦地意识到增加的成本和期望的收益,Scheme 世界中的人们花费了更多数量级的集体努力就此主题而言。坚持下去是个不错的选择defmacro如果围绕 S 表达式的额外包装对于您的口味来说太复杂了,但是您应该意识到学习所涉及的成本与倾倒卫生所付出的代价(以及接受它所获得的代价)。

不幸的是,对于新手来说,任何风格的宏总体上都是一个相当困难的主题(也许不包括极其有限的syntax-rules),所以人们往往会发现自己处于这种火焰的中间,而没有足够的经验来了解你的左手和右手。最终,没有什么比在这两个世界中拥有良好的经验更能明确权衡取舍了。(这是来自非常具体的个人经验:如果 PLT Scheme N 年前没有切换到语法案例,我可能永远不会打扰它......一旦他们确实切换了,我花了很长时间来转换我的代码 - 并且直到那时我才意识到拥有一个健壮的系统是多么伟大,其中没有名称会被错误地“混淆”(这会导致奇怪的错误和混淆%%__names__)。)

(不过,很有可能会发生评论火焰......)

于 2013-10-29T23:06:26.193 回答
9

这是 Guile 的define-macro. 请注意,它完全通过以下方式实现syntax-case

(define-syntax define-macro
  (lambda (x)
    "Define a defmacro."
    (syntax-case x ()
      ((_ (macro . args) doc body1 body ...)
       (string? (syntax->datum #'doc))
       #'(define-macro macro doc (lambda args body1 body ...)))
      ((_ (macro . args) body ...)
       #'(define-macro macro #f (lambda args body ...)))
      ((_ macro transformer)
       #'(define-macro macro #f transformer))
      ((_ macro doc transformer)
       (or (string? (syntax->datum #'doc))
           (not (syntax->datum #'doc)))
       #'(define-syntax macro
           (lambda (y)
             doc
             #((macro-type . defmacro)
               (defmacro-args args))
             (syntax-case y ()
               ((_ . args)
                (let ((v (syntax->datum #'args)))
                  (datum->syntax y (apply transformer v)))))))))))

Guile 对 Common Lisp 风格的文档字符串有特殊的支持,所以如果你的 Scheme 实现不使用文档字符串,你的define-macro实现可能会更简单:

(define-syntax define-macro
  (lambda (x)
    (syntax-case x ()
      ((_ (macro . args) body ...)
       #'(define-macro macro (lambda args body ...)))
      ((_ macro transformer)
       #'(define-syntax macro
           (lambda (y)
             (syntax-case y ()
               ((_ . args)
                (let ((v (syntax->datum #'args)))
                  (datum->syntax y (apply transformer v)))))))))))
于 2013-10-29T18:17:02.133 回答
5

这是define-macro我的Standard Prelude的实现,以及 Paul Graham 书中的示例:

(define-syntax (define-macro x)
  (syntax-case x ()
    ((_ (name . args) . body)
      (syntax (define-macro name (lambda args . body))))
    ((_ name transformer)
      (syntax
       (define-syntax (name y)
         (syntax-case y ()
           ((_ . args)
             (datum->syntax-object
               (syntax _)
               (apply transformer
                 (syntax-object->datum (syntax args)))))))))))

(define-macro (when test . body) `(cond (,test . ,body)))

(define-macro (aif test-form then-else-forms)
  `(let ((it ,test-form))
     (if it ,then-else-forms)))

(define-macro (awhen pred? . body)
  `(aif ,pred? (begin ,@body)))
于 2013-10-29T18:22:11.337 回答