我正在Haskell中编写一个Lisp(code at GitHub),以此来学习更多关于这两种语言的方法。
我添加的最新功能是宏。不卫生的宏或任何花哨的东西 - 只是普通的香草代码转换。我的初始实现有一个单独的宏环境,不同于所有其他值所在的环境。在read
和eval
函数之间我散布了另一个函数macroExpand
,它走了代码树在最终表单传递给eval
进行评估之前,只要在宏环境中找到关键字,就会执行适当的转换。这样做的一个很好的优点是宏具有与其他函数相同的内部表示,这减少了一些代码重复。
虽然有两个环境看起来很笨重,但如果我想加载一个文件,eval
必须能够访问宏环境,以防文件中包含宏定义,这让我感到恼火。所以我决定引入宏类型,在与函数和变量相同的环境中存储宏,并将宏扩展阶段合并到eval
中。我起初有点不知所措,直到我认为我可以写下这段代码:
eval env (List (function : args)) = do
func <- eval env function
case func of
(Macro {}) -> apply func args >>= eval env
_ -> mapM (eval env) args >>= apply func
它的工作原理如下:
就好像宏与函数完全相同,除了eval / apply的顺序被切换。
这是宏的准确描述吗?通过这种方式实现宏,我错过了一些重要的东西吗?如果答案是“是”和“否”,为什么我以前从未见过以这种方式解释过的宏?
答案 0 :(得分:21)
答案是“不”和“是”。
看起来您已经开始使用良好的宏模型,其中宏级别和运行时级别位于不同的世界中。事实上,这是Racket的macro system背后的主要观点之一。您可以阅读有关它的一些简短文字in the Racket guide,或查看描述此功能的original paper以及为什么这样做是个好主意。请注意,Racket的宏观系统非常复杂,而且卫生 - 但无论卫生如何,相分离都是一个好主意。总结主要优点,它可以始终以可靠的方式扩展代码,因此您可以获得单独编译等好处,并且不依赖于代码加载顺序和此类问题。
然后,你进入了一个失去这个环境的单一环境。在大多数Lisp世界中(例如,在CL和Elisp中),这正是事情的完成方式 - 显然,你遇到了上面描述的问题。 (“明显”,因为相分离的目的是为了避免这些,你恰好以与历史相反的顺序获得你的发现。)无论如何,为了解决其中的一些问题,有eval-when
特殊形式,可以指定在运行时或宏扩展时评估某些代码。在Elisp中你可以使用eval-when-compile
得到它,但是在CL中你可以得到更多的头发,还有一些其他的“* -time”。 (CL也有阅读时间,并且拥有与其他所有内容相同的环境三倍的乐趣。)即使它似乎是一个好主意,你应该四处阅读并看看一些lispers lose hair because of this mess。 / p>
在你描述的最后一步中,你会更进一步回到过去,发现一些被称为FEXPR的东西。我甚至不会提出任何指示,你可以找到大量关于它的文章,为什么有些人认为这是一个非常糟糕的主意,为什么其他人认为这是一个非常好的主意。实际上,这两个“一些”分别是“最多”和“少数” - 尽管剩余的少数FEXPR据点可以发声。翻译所有这些:这是爆炸性的东西......询问有关它的问题是获得长期火焰战争的好方法。 (作为一个认真讨论的最近例子,你可以看到R7RS的最初讨论期,其中FEXPR出现并导致这些类型的火焰。)无论你选择坐哪一侧,有一点是显而易见的:a使用FEXPR的语言与没有它们的语言非常不同。 [巧合的是,在Haskell中实现一个实现可能会影响你的观点,因为你有一个地方去寻找一个理智的静态世界代码,所以“可爱的”超级动态语言的诱惑可能更大......]
最后一点:由于你正在做类似的事情,你应该研究一个实现Scheme in Haskell - IIUC的类似项目,它甚至还有卫生的宏。
答案 1 :(得分:16)
不完全。实际上,你已经非常简洁地描述了“按名称呼叫”和“按价值呼叫”之间的区别;一个按值调用的语言在替换之前减少了对值的参数,逐个调用语言首先执行替换,然后是还原。
关键的区别在于宏允许你破坏参照透明度;特别地,宏可以检查代码,因此可以以普通代码不能的方式区分(3 + 4)和7。这就是为什么宏更强大也更危险的原因;大多数程序员如果发现(f 7)产生了一个结果并且(f(+ 3 4))产生了不同的结果,他们会感到不安。
答案 2 :(得分:6)
背景漫游
你所拥有的是非常晚的绑定宏。这是一种可行的方法,但效率很低,因为重复执行相同的代码会反复扩展宏。
从积极的方面来说,这对于互动发展是友好的。如果程序员更改了一个宏,然后重新调用一些使用它的代码,例如先前定义的函数,则新宏立即生效。这是一种直观的“做我的意思”行为。
在早期扩展宏的宏系统下,程序员必须在宏发生变化时重新定义依赖于宏的所有函数,否则现有定义将继续基于旧的宏扩展,而不考虑新版本宏观。
一种合理的方法是让这个后期绑定宏系统用于解释代码,但是用于编译代码的“常规”(缺少更好的单词)宏系统。
扩展宏不需要单独的环境。它不应该,因为本地宏应该与变量在同一名称空间中。例如,如果我们这样做(let (x) (symbol-macrolet ((x 'foo)) ...))
,在Common Lisp中,内部符号宏会影响外部词法变量。宏扩展器必须知道变量绑定表单。反之亦然!如果变量let
存在内部x
,则会隐藏外部symbol-macrolet
。宏扩展器不能盲目地替换身体中出现的x
的所有事件。换句话说,Lisp宏扩展必须意识到宏和其他类型的绑定共存的完整词法环境。当然,在宏扩展期间,您不会以相同的方式实例化环境。当然,如果有(let ((x (function)) ..)
,则不会调用(function)
,并且x
没有给出值。但宏扩展器知道在此环境中存在x
,因此x
的出现不是宏。
因此,当我们说一个环境时,我们真正的意思是统一环境有两种不同的表现形式或实例:扩展时表现形式,然后是评估时间表现形式。后期绑定宏通过将这两次合并为一个来简化实现,正如您所做的那样,但它不一定是那样。
另请注意,Lisp宏可以接受&environment
参数。如果宏需要在用户提供的某些代码上调用macroexpand
,则需要这样做。通过宏返回宏扩展器的这种递归必须通过适当的环境,以便用户的代码可以访问其词法周围的宏并正确扩展。
具体示例
假设我们有这段代码:
(symbol-macrolet ((x (+ 2 2)))
(print x)
(let ((x 42)
(y 19))
(print x)
(symbol-macrolet ((y (+ 3 3)))
(print y))))
此效果可打印4
,42
和6
。让我们使用Common Lisp的CLISP实现,并使用CLISP的特定于实现的函数system::expand-form
来扩展它。我们不能使用常规的标准macroexpand
,因为它不会递归到本地宏中:
(system::expand-form
'(symbol-macrolet ((x (+ 2 2)))
(print x)
(let ((x 42)
(y 19))
(print x)
(symbol-macrolet ((y (+ 3 3)))
(print y)))))
-->
(LOCALLY ;; this code was reformatted by hand to fit your screen
(PRINT (+ 2 2))
(LET ((X 42) (Y 19))
(PRINT X)
(LOCALLY (PRINT (+ 3 3))))) ;
(首先,关于这些locally
形式。为什么它们在那里?请注意它们对应于我们有symbol-macrolet
的地方。这可能是为了声明。如果是symbol-macrolet
表单有声明,它们必须作用于该主体,locally
将执行此操作。如果symbol-macrolet
的扩展不会留下此locally
包装,那么声明的范围就会错误。)
通过此宏扩展,您可以看到任务是什么。宏扩展器必须遍历代码并识别所有绑定结构(所有特殊形式,实际上),而不仅仅是与宏系统有关的绑定结构。
请注意(print x)
的其中一个实例是如何保持不变的:(let ((x ..)) ...)
范围内的实例。另一个变为(print (+ 2 2))
,符合x
的符号宏。
我们可以从中学到的另一件事是宏扩展只是替换扩展并删除symbol-macrolet
形式。因此,剩余的环境是原始的,减去所有在扩展过程中被清除的宏材料。宏扩展尊重所有词汇绑定,在一个大的“大统一”环境中,但随后慷慨地蒸发,留下像(print (+ 2 2))
这样的代码和其他遗迹,如{{1仅使用非宏绑定结构导致原始环境的缩减版本。
因此,现在评估扩展代码时,只有简化环境的运行时特性才会发挥作用。 (locally ...)
绑定被实例化并填充初始值等。在扩展期间,没有发生任何事情;非宏绑定只是在那里断言它们的范围,暗示了未来在运行时的存在。
答案 3 :(得分:4)
你所遗漏的是,当你separate analysis from evaluation时,这种对称性会失效,这是所有实际的Lisp实现所做的。在分析阶段会发生宏扩展,因此eval
可以保持简单。
答案 4 :(得分:2)
我真的建议让一些Lisp书籍得心应手。推荐使用例如 Christian Queinnec , Lisp in Small Pieces 。这本书是关于Scheme的实现。
http://pagesperso-systeme.lip6.fr/Christian.Queinnec/WWW/LiSP.html
第9章是关于宏:http://pagesperso-systeme.lip6.fr/Christian.Queinnec/WWW/chap9.html
答案 5 :(得分:1)
对于它的价值,方案R 5 RS部分语法关键词的绑定结构有这样的说法:
Let-syntax
和letrec-syntax
类似于let
和letrec
,但它们将语法关键字绑定到宏变换器,而不是将变量绑定到包含值的位置。
请参阅:http://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-Z-H-7.html#%_sec_4.3.1
这似乎意味着应该使用单独的策略,至少对于syntax-rules
宏系统而言。
<小时/> 您可以在Scheme中编写一些有趣的代码,这些代码对宏使用单独的“位置”。在任何“真实”代码中混合使用相同名称的宏和变量没有多大意义,但如果你只是想尝试一下,请考虑鸡计划中的这个例子:
#;1> let
Error: unbound variable: let
#;1> (define let +)
#;2> (let ((talk "hello!")) (write talk))
"hello!"
#;3> let
#<procedure C_plus>
#;4> (let 1 2)
Error: (let) not a proper list: (let 1 2)
Call history:
<syntax> (let 1 2) <--
#;4> (define a let)
#;5> (a 1 2)
3