是否有可能在方案中实现Common Lisp的宏系统?

时间:2013-10-29 17:18:52

标签: compiler-construction macros scheme common-lisp

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

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

是否有可能在方案中实现Common Lisp的宏系统,使用syntax-rulessyntax-case更强大?

3 个答案:

答案 0 :(得分:37)

我会尝试简短 - 这很难,因为这通常是一个非常深刻的问题,超过Q& A的平均SO水平...所以这仍然会很长。我也会尝试不偏不倚;虽然我来自Racket的观点,但我过去一直在使用Common Lisp,而且我总是喜欢在两个世界中使用宏(实际上在其他世界中)。它不会直接回答你的问题(在极端情况下,这只是"是"),但它正在比较这两个系统,希望这有助于澄清问题对于人们 - 尤其是口齿不清的人(所有口味)谁会想知道为什么这么大。我还将介绍如何在" syntax-case"中实现defmacro。系统,但通常只是为了保持清晰(并且因为你可以找到这样的实现,一些在评论和其他答案中给出)。

首先,你的问题非常多余 - 这是非常合理的,而且(正如我暗示的那样)是Lisp新成员与Lisp的新成员相遇的事情之一。

其次,非常浅薄,非常简短的回答是人们告诉你的:是的,可以在支持defmacro的方案中实施CL syntax-case,正如预期的那样,你有多个指向这种实现的指针。走另一条路并使用简单syntax-case实施defmacro是一个棘手的主题,我不会谈论太多;我只是说它has been done,只是以非常高的成本重新实现lambda和其他绑定结构,这意味着它基本上是一个新的重新实现如果要使用该实现,则应该提交的语言。

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

然而,syntax-rules并不打算作为""" Scheme宏系统 - 这个想法总是让你有一些"低级"宏实现,用于实现syntax-rules,但也可以实现卫生破坏宏 - 它只是没有就特定的低级实现达成一致。 R6RS通过标准化"语法案例"来解决这个问题。宏系统(注意我使用" syntax-case"作为系统的名称,不同于syntax-case,这是它的主要亮点形式)。好像是为了说明讨论仍然存在,R7RS退后一步并将其排除在外,重新回到syntax-rules而没有对低级别系统做出承诺,至少在&#34 ;小语言"去。

现在,要真正理解两个系统之间的区别,最好澄清的是他们正在处理的类型之间的区别。对于defmacro,变换器基本上是一个接收S表达式并返回S表达式的函数。这里的S表达式是由一堆文字类型(数字,字符串,布尔值),符号和列表嵌套结构组成的类型。 (所使用的实际类型不止于此,但这足以证明这一点。)问题在于,这是一个非常简单的世界:你进入了一个非常具体的东西 - 你可以实际打印输入/输出值,这就是你所拥有的一切。请注意,此系统使用符号来表示标识符 - 在这种意义上,符号是非常具体的:x是一段只有该名称的代码,{{1 }}

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

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

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

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

另一方面,问题是如何从给定的符号名称开始并构造这样的语法对象。这样做的方法是使用datum->syntax函数 - 但是不是使api指定如何表示词法范围信息,而是将语法对象作为第一个参数和符号S表达式作为第二个参数,它通过正确包装S-expression与从第一个参数中获取的词法范围信息来创建语法对象。这意味着要破坏卫生,通常用户提供的语法对象(例如,宏的正文形式)开始,并使用其词法信息创建一些新的标识符,如this,相同的范围。

这个快速描述足以让您了解所显示的宏是如何工作的。 @ChrisJester-Young显示的宏只接受语法对象,将其剥离为带有syntax->datum的原始S表达式,将其发送到defmacro转换器并获取S表达式,然后它使用syntax->datum使用用户代码的词汇上下文将结果转换回语法对象。 Racket的defmacro实现有点漂亮:在剥离阶段它保留一个哈希表,将生成的S表达式映射到它们的原始语法对象,并且在重建步骤期间,它会查询此表以获取与最初的代码位相同的上下文。这使得它成为一些更复杂的宏的更强大的实现,但它在Racket中也更有用,因为语法对象中有更多的信息,如源位置,属性等,这种仔细的重建通常会导致输出值(语法对象),用于保存他们进入宏的信息。

有关defmacro程序员对语法案例系统的更多技术性介绍,请参阅我的writing syntax-case macros博文。如果您来自计划方面,它将不会有用,但它仍然有助于澄清整个问题。

为了得到更接近结论,我应该注意到处理不卫生的宏仍然是棘手的。更具体地说,有各种方法来实现这样的绑定,但它们在各种微妙的方式上是不同的,并且通常可以回来咬你在每种情况下留下略微不同的牙齿痕迹。在" true"像CL这样的defmacro系统,你学会了一套特定的牙齿痕迹,这些牙齿标记相对众所周知,因此有些东西是你不能做的。最值得注意的是,这种语言的模块化组合具有与Racket频繁使用的相同名称的不同绑定。在语法案例系统中,更好的方法是fluid-let-syntax,用于"调整"词法范围名称的含义 - 最近,它已演变为"语法参数"。有一个很好的overview of the problems of hygiene-breaking macros,其中包括如何使用卫生syntax-rules尝试解决它的描述,基本语法大小写,CL风格defmacro,最后一个语法参数。这篇文章有点技术性,但是前几页相对容易阅读,如果你理解了这一点,那么你将对整个辩论有一个很好的了解。 (本文还有一个older blog post更好的内容。)

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

希望这能够更全面地了解这个问题,以便了解这些权衡并能够自己决定什么最适合你。我也希望这澄清了通常火焰的一些来源:卫生宏当然不是无用的,但由于新类型不仅仅是简单的S表达式,它们周围还有更多的功能 - 而且常常是浅的 - 阅读旁观者得出的结论是“太复杂了”#34;更糟糕的是,在“计划”世界中,人们对元编程几乎一无所知,并且非常痛苦地意识到增加的成本和预期的好处,计划世界的人们已经花了订单。关于这个问题的更多集体努力。如果围绕S表达的额外包装太复杂而不适合您的口味,那么坚持使用defmacro是一个不错的选择,但是您应该了解学习的成本与您通过倾倒卫生所支付的费用相比(和你通过拥抱它得到的结果)。

不幸的是,对于新手而言,任何风味的宏都是一个非常难的主题(可能排除极其有限的syntax-rules),所以人们往往发现自己处于这样的火焰中,而没有足够的经验知道你的左派从你的权利。最终,没有什么能比两个世界都有更好的经验来澄清权衡。 (这是非常具体的个人经历:如果说PLT Scheme在N年前没有切换到语法案例,我可能永远都不会理会它......一旦他们切换,我花了很长时间来转换我的代码 - 只有在那时我才意识到拥有一个强大的系统是多么伟大,在这个系统中,任何名称都不会被错误地混淆(这会导致奇怪的错误,并且会混淆%%__names__)。 )

(尽管如此,评论火焰很可能会发生......)

答案 1 :(得分:7)

这是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)))))))))))

答案 2 :(得分:3)

以下是我Standard Preludedefine-macro的实施情况,以及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)))