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

Chr*_*lor 20 lisp macros scheme haskell

我正在Haskell中编写一个Lisp(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
Run Code Online (Sandbox Code Playgroud)

它的工作原理如下:

  1. 如果传递一个包含初始表达式和一堆其他表达式的列表......
  2. 评估第一个表达式
  3. 如果它是一个宏,则将其应用于参数,并评估结果
  4. 如果它不是宏,则评估参数并将函数应用于结果

就好像宏与函数完全相同,除了eval/apply的顺序被切换.

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

Eli*_*lay 21

答案是"不"和"是".

看起来你已经开始使用一个很好的宏模型,其中宏级别和运行时级别在不同的世界中.其实,这背后是要点之一球拍宏系统.您可以在"球拍"指南中阅读有关它的一些简短文本,或者查看描述此功能的原始文章以及为什么这样做是个好主意.请注意,Racket的宏观系统非常复杂,而且卫生 - 但无论卫生如何,相分离都是一个好主意.总结主要优点,它可以始终以可靠的方式扩展代码,因此您可以获得诸如单独编译之类的好处,并且您不依赖于代码加载顺序和此类问题.

然后,你进入了一个失去这个环境的单一环境.在大多数Lisp世界中(例如,在CL和Elisp中),这正是事情的完成方式 - 显然,你遇到了上面描述的问题.("明显",因为相分离的目的是为了避免这些,你恰好以与历史相反的顺序获得你的发现.)无论如何,为了解决其中的一些问题,有一种eval-when特殊的形式,它可以指定在运行时或宏扩展时评估某些代码.在Elisp你可以得到它eval-when-compile,但在CL你会得到更多的头发,还有一些其他的"*时间".(CL也有阅读时间,并且拥有相同的环境,其他任何东西都是乐趣的三倍.)即使它看起来是个好主意,你应该四处看看,看看有些lispers 因为这个烂摊子而失去了头发.

在描述的最后一步中,您将进一步追溯并发现一些被称为FEXPR的东西.我甚至不会提出任何指示,你可以找到大量关于它的文章,为什么有些人认为这是一个非常糟糕的主意,为什么其他人认为这是一个非常好的主意.实际上,这两个"一些"分别是"最多"和"少数" - 尽管剩余的少数FEXPR据点可以发声.翻译所有这些:这是爆炸性的东西......询问有关它的问题是获得长期火焰战争的好方法.(作为一个认真讨论的最近例子,你可以看到R7RS的最初讨论期,其中FEXPR出现并导致这些类型的火焰.)无论你选择坐哪一侧,有一点是显而易见的:a FEXPRs的语言与没有它们的语言有很大不同.[巧合的是,在Haskell中实现一个实现可能会影响你的观点,因为你有一个地方去寻找一个理智的静态世界代码,所以"可爱的"超级动态语言的诱惑可能更大......]

最后一点说明:既然你正在做类似的事情,你应该研究一个在Haskell中实现Scheme的类似项目--IIUC,它甚至还有卫生的宏.


Joh*_*nts 16

不完全的.实际上,你已经非常简洁地描述了"按名称呼叫"和"按价值呼叫"之间的区别; 逐个值调用语言在替换之前减少了对值的参数,逐个调用语言首先执行替换,然后执行减少.

关键的区别在于宏允许您破坏参照透明度; 特别地,宏可以检查代码,因此可以以普通代码不能的方式区分(3 + 4)和7.这就是为什么宏更强大也更危险的原因; 大多数程序员如果发现(f 7)产生了一个结果并且(f(+ 3 4))产生了不同的结果,他们会感到不安.

  • @Kaz我认为他的意思是函数和宏*之间的差异*是参考透明度.宏可以区分`(+ 3 4)`和`7`,而函数则不能. (3认同)

Kaz*_*Kaz 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))))
Run Code Online (Sandbox Code Playgroud)

这对打印的影响4,426.让我们使用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))))) ;
Run Code Online (Sandbox Code Playgroud)

(首先,关于这些locally形式.为什么它们在那里?请注意它们对应于我们所拥有的地方symbol-macrolet.这可能是为了声明.如果symbol-macrolet表单的主体有声明,则必须将其作用于该主体并且locally会这样做.如果扩展symbol-macrolet不会留下这种locally包装,那么声明的范围将是错误的.)

从这个宏扩展中,您可以看到任务是什么.宏扩展器必须遍历代码并识别所有绑定结构(所有特殊形式,实际上),而不仅仅是与宏系统有关的绑定结构.

注意如何(print x)保留其中一个实例:属于范围的实例(let ((x ..)) ...).另一个成了(print (+ 2 2)),按照符号宏x.

我们可以从中学到的另一件事是宏扩展只是替代扩展并删除symbol-macrolet表单.因此,剩下的环境是原始环境,减去在扩展过程中被清除的所有宏观材料.宏扩展在一个大的"大统一"环境中尊重所有词法绑定,但随后慷慨地蒸发,留下代码(print (+ 2 2))和其他类似的痕迹(locally ...),只有非宏绑定结构导致缩小版本原始环境.

因此,现在当评估扩展代码时,只有简化环境的运行时特性才会发挥作用.该let绑定被实例化,并用初始值等在膨胀期间毛绒,这些方法都发生了什么; 非宏绑定只是在那里断言它们的范围,并暗示在运行时的未来存在.