【问题标题】:How to implement a Lisp macro system?如何实现一个 Lisp 宏系统?
【发布时间】:2011-03-28 19:25:35
【问题描述】:

我已经在 node.js 上实现了自己的 Lisp,我可以像这样运行 s-expression:

(断言 (= 3 (+ 1 2))) (def even? (fn [n] (= 0 (bit-and n 1)))) (断言(甚至?4)) (assert (= false (even?5)))

现在我想添加宏 - defmacro 函数 - 但这是我卡住的地方。我想知道如何在其他 Lisps 中实现宏系统,但我找不到很多指针(除了 thisthis)。

我查看了 Clojure 宏系统——我最熟悉的 Lisp——但这似乎太复杂了,我找不到可以轻松应用的其他线索(Clojure 宏最终编译为字节码'不适用于 javascript,我也无法理解 macroexpand1 函数。)

所以我的问题是:给定一个没有宏但有 AST 的 Lisp 实现,如何添加像 Clojure 的宏系统这样的宏系统?这个宏系统可以在 Lisp 中实现,还是需要在宿主语言中实现额外的功能?

补充一点:我还没有实现quote ('),因为我无法弄清楚返回列表中应该包含什么样的值。它是否应该包含 AST 元素或对象,例如 SymbolKeyword(Clojure 就是后者)?

【问题讨论】:

    标签: clojure macros lisp


    【解决方案1】:

    宏所做的只是将未计算的形式作为参数并在其主体上执行替换。实现宏系统的技巧是告诉你的编译器是lazy

    换句话说,当编译器遇到一个函数时,它首先评估它的形式参数列表,产生结果并将它们传递给函数。当编译器找到一个宏时,它会将参数 unevaluate 传递给主体,然后执行主体请求的任何计算,最后用这些结果替换自身。

    例如,假设你有一个函数:

    (defun print-3-f (x) (progn (princ x) (princ x) (princ x)))
    

    还有一个宏:

    (defmacro print-3-m (x) `(progn (princ ,x) (princ ,x) (princ ,x)))
    

    那你马上就能看出区别了:

    CL-USER> (print-3-f (rand))
    * 234
    * 234
    * 234
    
    CL-USER> (print-3-m (rand))
    * 24
    * 642
    * 85
    

    要理解为什么会这样,从某种意义上说,您需要在脑海中运行编译器。

    当 Lisp 遇到该函数时,它会构建一棵树,其中首先评估 (rand),并将结果传递给函数,该函数将上述结果打印 3 次。

    另一方面,当 Lisp 遇到宏时,它会将(rand) untouched 形式传递给主体,该主体返回一个带引号的列表,其中x(rand) 替换,产生:

    (progn (princ (rand)) (princ (rand)) (princ (rand)))
    

    并替换此新表单的宏调用。

    Here 您会发现大量关于各种语言(包括 Lisp)的宏的文档。

    【讨论】:

    • 感谢您的回复,我可以将您的答案总结如下:对于defun:1)评估AST(返回javascript function对象)2)执行javascripts函数3)传递结果值作为 lisp 函数的参数。这就是我已经在做的事情。对于defmacro:1)同上2)跳过3)将javascript函数作为参数传递给宏。 ` 返回的结果应该是应该评估和执行的 AST 元素。这留下了一个未解决的问题:quote 应该返回什么?应该是 AST 元素列表还是 javascript 函数和其他 obj 列表?
    • (quote ...) 返回一个“stuff”列表,其中“stuff”的形式可以稍后进行评估。 lisp 的美妙之处在于它的列表表示与 AST 表示相同,因此返回列表或 AST 是等价的。
    • 对我来说,宏不评估其参数似乎是其性质的副作用,而不是其主要定义属性。
    【解决方案2】:

    您需要在评估链中有一个宏扩展阶段:

    text-input -> read -> macroexpand -> compile -> load
    

    请注意,宏扩展应该是递归的(宏扩展直到没有可扩展的宏)。

    您的环境需要能够“保存”在此阶段可以通过名称查找的宏扩展函数。请注意,defmacro 本身是 Common Lisp 中的一个宏,它设置了正确的调用以将名称与该环境中的宏扩展函数相关联。

    【讨论】:

    • 感谢您的回复。如果我理解正确,macroexpand 函数会将宏及其参数转换为无宏的 lisp 代码。对吗?
    • 您的描述有些不准确,但我认为您的想法是正确的。 :)
    • 好的,我已经做出了心理点击。我正在重构我的实现如下: 1/ 读取用户代码,转换为包含 AstSymbol、AstKeyword、AstString、AstInteger 的列表,当然还有其他列表; 2/ 将库宏 (defmacro) 转换为 AST; 3/ 遍历用户AST寻找宏(defmacro); 4/ 实际的宏扩展阶段:将调用宏的 AST 元素(在此处检测宏调用)替换为宏的 AST 元素(将宏参数替换为相关的用户 AST 元素); 5/ AST 现在应该是无宏的,解释。我发现this 也很有帮助。
    【解决方案3】:

    看看this 的例子。这是一个类似 Arc 的编译器的玩具实现,具有不错的宏支持。

    【讨论】:

    • 链接已损坏。你能在别的地方找到它吗?
    【解决方案4】:

    这是来自 Peter Norvig 的 Paradigms of Artificial Intelligence Programming - 任何 LISP 程序员书架上的必备书籍。

    他假设您正在实现一种解释型语言,并提供了在 LISP 中运行的方案解释器的示例。

    The following two examples here 展示了他如何将宏添加到主要的 eval 函数 (interp)

    这是在处理宏之前解释 S 表达式的函数:

    (defun interp (x &optional env)
      "Interpret (evaluate) the expression x in the environment env."
      (cond
        ((symbolp x) (get-var x env))
        ((atom x) x)
        ((case (first x)
           (QUOTE  (second x))
           (BEGIN  (last1 (mapcar #'(lambda (y) (interp y env))
                                  (rest x))))
           (SET!   (set-var! (second x) (interp (third x) env) env))
           (IF     (if (interp (second x) env)
                       (interp (third x) env)
                       (interp (fourth x) env)))
           (LAMBDA (let ((parms (second x))
                         (code (maybe-add 'begin (rest2 x))))
                     #'(lambda (&rest args)
                         (interp code (extend-env parms args env)))))
           (t      ;; a procedure application
                   (apply (interp (first x) env)
                          (mapcar #'(lambda (v) (interp v env))
                                  (rest x))))))))
    

    这是在添加了宏评估之后(为了清楚起见,子方法已在参考链接中

    (defun interp (x &optional env)
      "Interpret (evaluate) the expression x in the environment env.
      This version handles macros."
      (cond
        ((symbolp x) (get-var x env))
        ((atom x) x)
    
        ((scheme-macro (first x))              
         (interp (scheme-macro-expand x) env)) 
    
        ((case (first x)
           (QUOTE  (second x))
           (BEGIN  (last1 (mapcar #'(lambda (y) (interp y env))
                                  (rest x))))
           (SET!   (set-var! (second x) (interp (third x) env) env))
           (IF     (if (interp (second x) env)
                       (interp (third x) env)
                       (interp (fourth x) env)))
           (LAMBDA (let ((parms (second x))
                         (code (maybe-add 'begin (rest2 x))))
                     #'(lambda (&rest args)
                         (interp code (extend-env parms args env)))))
           (t      ;; a procedure application
                   (apply (interp (first x) env)
                          (mapcar #'(lambda (v) (interp v env))
                                  (rest x))))))))
    

    有趣的是Christian Queinnec'sLisp In Small Pieces的开头章节有一个非常相似的功能,他称之为eval

    【讨论】:

      猜你喜欢
      • 2013-05-06
      • 2012-04-06
      • 2013-11-09
      • 1970-01-01
      • 1970-01-01
      • 2017-07-17
      • 2011-07-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多