cat*_*ory 19 compiler-construction macros scheme common-lisp
希望这不是一个多余的问题.
作为计划的新手,我意识到syntax-case宏比syntax-rules替代方案更强大,代价是不必要的复杂性.
但是,是否有可能在方案中实现Common Lisp的宏系统,它比syntax-rules使用syntax-case?更强大?
Eli*_*lay 38
我会尽量简短 - 这很难,因为这通常是一个非常深刻的问题,超过问答的平均SO水平......所以这仍然会很长.我也会尽量不偏不倚; 虽然我来自Racket的观点,但我过去一直在使用Common Lisp,而且我总是喜欢在两个世界中使用宏(实际上在其他世界中).它看起来不像你的问题的直接答案(在极端情况下,这只是"是"),但它正在比较这两个系统,希望这将有助于澄清人们的问题 - 特别是外面的人lisp(所有口味)的人会想知道为什么这么大.我还将描述如何defmacro 可以在"语法 - 案例"系统中实现,但通常只是为了保持清晰(并且因为您可以找到这样的实现,在评论和其他答案中给出一些实现).
首先,你的问题非常多余 - 这是非常合理的,而且(正如我暗示的那样)是Lisp新成员与Lisp的新成员遇到的事情之一.
其次,非常浅薄,非常简短的答案是人们告诉你的:是的,可以defmacro在支持的Scheme中实现CL syntax-case,并且正如预期的那样,你有多个指向这些实现的指针.走另一条路,syntax-case用简单的方法实现defmacro是一个棘手的主题,我不会谈得太多; 我只是说它已经完成,只是以非常高的成本重新实现lambda和其他绑定结构,这意味着它基本上是一种新语言的重新实现,如果你想使用该实现,你应该承诺.
另一个澄清:人们,尤其是CLers,经常会崩溃与Scheme宏相关的两件事:卫生,和syntax-rules.事情就是在R5RS中你所拥有的syntax-rules是一个非常有限的基于模式的重写系统.与其他重写系统一样,您可以天真地使用它,或者一直使用重写来定义一种小语言,然后您可以使用它来编写宏.请参阅此文本有关如何完成的已知解释.虽然有可能这样做,但最重要的是它很难,你正在使用一些与你的实际语言没有直接关系的奇怪的小语言,这使得它远离Scheme编程 - 可能更糟糕在CL中使用卫生宏实现的方式并不是真正使用普通CL.简而言之,它可以只使用syntax-rules,但这主要是在理论意义上,而不是你想要在"真实"代码中使用的东西.这里的要点是卫生并不意味着受到限制syntax-rules.
然而,syntax-rules并不打算作为"方案"宏系统 - 这个想法始终是你有一些"低级"宏实现,用于实现,syntax-rules但也可以实现卫生破坏宏 - 只是没有就特定的低级别实施达成协议.R6RS通过标准化"语法案例"宏系统来修复它(注意我使用"syntax-case"作为系统的名称,不同于syntax-case它的主要亮点形式).似乎为了说明讨论仍然存在,R7RS退后一步并将其排除在外,恢复到syntax-rules对低级别系统没有承诺,至少就"小语言"而言.
现在,要真正理解两个系统之间的区别,最好澄清的是它们正在处理的类型之间的差异.因此defmacro,变换器基本上是一个接收S表达式并返回S表达式的函数.这里的S表达式是由一堆文字类型(数字,字符串,布尔值),符号和列表嵌套结构组成的类型.(使用的实际类型稍微多一点,但这足以说明问题.)问题在于,这是一个非常简单的世界:你得到的东西非常具体 - 你实际上可以打印输入/输出值,这就是你所拥有的一切.请注意,此系统用于符号表示标识符 - 在这种意义上,符号是非常具体的东西:a x是一段具有该名称的代码,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宏博客文章.如果您来自计划方面,它将没有那么有用,但它仍然有助于澄清整个问题.
为了得到更接近结论,我应该注意到处理不卫生的宏仍然是棘手的.更具体地说,有各种方法来实现这样的绑定,但它们在各种微妙的方式上是不同的,并且通常可以回来咬你在每种情况下留下略微不同的牙齿痕迹.在defmacro像CL这样的"真实" 系统中,你会学会使用一组特定的牙齿痕迹,这些牙齿痕迹是相对众所周知的,因此有些东西是你不做的.最值得注意的是,这种语言的模块化组合具有与Racket频繁使用的相同名称的不同绑定.在语法案例系统中,更好的方法是fluid-let-syntax用于"调整"词法范围名称的含义 - 最近,它已演变为"语法参数".对卫生破坏宏的问题有一个很好的概述,其中包括如何尝试用卫生syntax-rules,基本语法案例,CL风格defmacro,最后用语法参数来解决它的描述.这篇文章有点技术性,但是前几页相对容易阅读,如果你理解了这一点,那么你将对整个辩论有一个很好的了解.(还有一篇较旧的博客文章,在论文中有更好的内容.)
我还要提一下,这远不是围绕宏的唯一"热门"问题.Scheme计划内部关于哪个低级别宏观系统更好的争论有时会变得非常热门.围绕宏存在其他问题,例如如何使它们在模块系统中工作的问题,其中库可以提供宏以及值和函数,或者是否将宏扩展时间和运行时分成单独的阶段等等.
希望这能够更全面地了解问题,以便了解权衡并能够自己决定最适合您的问题.我也希望这澄清了通常火焰的一些来源:卫生宏当然不是无用的,但由于新类型不仅仅是简单的S表达式,它们周围还有更多的功能 - 而且常常是浅的 - 阅读旁观者得出的结论是"它太复杂了".更糟糕的是"在计划世界中人们对元编程几乎一无所知"的精神中的火焰:非常痛苦地意识到增加的成本和期望的好处,计划世界的人们已经花费了数量级更多的集体努力就此主题而言.坚持下去是个不错的选择defmacro如果围绕S表达的额外包装对你的品味来说太复杂了,但是你应该意识到学习的成本与你通过倾倒卫生所付出的代价相比(以及你通过拥抱它得到的代价).
不幸的是,对于新手来说,任何风味的宏都是一个非常难的主题(可能不包括极其有限的syntax-rules),所以人们往往发现自己处于这样的火焰中,而没有足够的经验从你的右边知道你的左边.最终,没有什么能比两个世界都有更好的经验来澄清权衡.(这是来自非常具体的个人经验:如果PLT Scheme在N年前没有切换到语法案例,我可能永远不会理会它......一旦他们切换,我花了很长时间来转换我的代码 - 和只有到那时我才意识到拥有一个强大的系统是多么伟大,没有名字被错误地"混淆"(这会导致奇怪的错误,并且被混淆%%__names__).)
(尽管如此,评论火焰很可能会发生......)
这是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)))))))))))
Run Code Online (Sandbox Code Playgroud)
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)))))))))))
Run Code Online (Sandbox Code Playgroud)
这是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)))
Run Code Online (Sandbox Code Playgroud)