宏在多大程度上“反向运行?”

时间:2012-04-20 15:30:39

标签: haskell macros lisp scheme

我正在Haskell中编写一个Lisp(code at GitHub),以此来学习更多关于这两种语言的方法。

我添加的最新功能是宏。不卫生的宏或任何花哨的东西 - 只是普通的香草代码转换。我的初始实现有一个单独的宏环境,不同于所有其他值所在的环境。在readeval函数之间我散布了另一个函数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

它的工作原理如下:

  1. 如果传递的是包含初始表达式和一堆其他表达式的列表......
  2. 评估第一个表达式
  3. 如果是宏,则将其应用于参数评估结果
  4. 如果它不是宏,那么评估参数将函数应用于结果
  5. 就好像宏与函数完全相同,除了eval / apply的顺序被切换。

    这是宏的准确描述吗?通过这种方式实现宏,我错过了一些重要的东西吗?如果答案是“是”和“否”,为什么我以前从未见过以这种方式解释过的宏?

6 个答案:

答案 0 :(得分:21)

答案是“不”和“是”。

看起来您已经开始使用良好的宏模型,其中宏级别和运行时级别位于不同的世界中。事实上,这是Racketmacro 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))))

此效果可打印4426。让我们使用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-syntaxletrec-syntax类似于letletrec,但它们将语法关键字绑定到宏变换器,而不是将变量绑定到包含值的位置。

请参阅: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