【问题标题】:Translating this to Common Lisp将其翻译为 Common Lisp
【发布时间】:2014-02-20 00:32:52
【问题描述】:

我一直在阅读 Olin Shivers 的一篇题为 Stylish Lisp programming techniques 的文章,发现那里的第二个示例(标记为“Technique n-1”)有点令人费解。它描述了一个如下所示的自修改宏:

(defun gen-counter macro (x)
       (let ((ans (cadr x)))   
     (rplaca (cdr x)       
         (+ 1 ans))
     ans))

它应该将其调用形式作为参数x(即(gen-counter <some-number>))。这样做的目的是能够做这样的事情:

> ;this prints out the numbers from 0 to 9.
  (do ((n 0 (gen-counter 1)))
      ((= n 10) t)
    (princ n))
0.1.2.3.4.5.6.7.8.9.T
>

问题是函数名后带有macro 符号的这种语法在Common Lisp 中无效。我一直试图在 Common Lisp 中获得类似的行为,但没有成功。有人可以提供一个 CL 中类似宏的工作示例吗?

【问题讨论】:

    标签: macros lisp common-lisp self-reference


    【解决方案1】:

    普通的 Lisp:

    (defmacro gen-counter (&rest x)
      (let ((ans (car x)))   
        (rplaca x (+ 1 ans))
        ans))
    

    但以上仅适用于解释器,不适用于编译器。

    使用编译后的代码,宏调用消失了——它被扩展了——没有什么可修改的。

    不知情的读者注意:您可能希望非常仔细阅读 Olin Shivers 的论文,并尝试找出他的真正意思...

    【讨论】:

    • 不是(defmacro gen-counter (&whole x num) (let ((ans num)) (rplaca (cdr x) (+ 1 ans)) ans)) 来捕获表单...?不是说它可以在我可以访问的任何 Lisp 中工作,无论如何,但是……
    • BRPocock:类似,但仍然只适用于解释器。
    • 在 Olin Shivers 的论文中,x 实际上类似于&whole x;它绑定到整个表单,因此源是正在使用(和修改)的文字数据。
    • @JoshuaTaylor:是的,但它只在解释器中有效。 &rest 形式也有类似的效果。
    【解决方案2】:

    为什么代码有效

    首先,考虑论文中的第一个示例很有用:

    > (defun element-generator ()
        (let ((state '(() . (list of elements to be generated)))) ;() sentinel.
          (let ((ans (cadr state)))       ;pick off the first element
            (rplacd state (cddr state))   ;smash the cons
            ans)))
    ELEMENT-GENERATOR
    > (element-generator)
    LIST
    > (element-generator)
    OF
    > (element-generator)
    

    这行得通,因为有 一个 文字列表

    (() . (list of elements to be generated)
    

    并且它正在被修改。请注意,这实际上是 Common Lisp 中未定义的行为,但在某些 Common Lisp 实现中您会得到相同的行为。请参阅Unexpected persistence of data 和其他一些相关问题,以讨论此处发生的情况。

    在 Common Lisp 中逼近它

    现在,你引用的论文和代码实际上有一些关于这段代码在做什么的有用的 cmets:

      (defun gen-counter macro (x)    ;X is the entire form (GEN-COUNTER n)
        (let ((ans (cadr x)))         ;pick the ans out of (gen-counter ans)
          (rplaca (cdr x)             ;increment the (gen-counter ans) form
                  (+ 1 ans))
          ans))                       ;return the answer
    

    它的工作方式与&rest 参数不太像Rainer Joswig's answer,而是实际上是&whole 参数,其中整个表单可以绑定到一个变量。这是使用程序的 source 作为被破坏性修改的字面值!现在,在论文中,此示例中使用了它:

    > ;this prints out the numbers from 0 to 9.
      (do ((n 0 (gen-counter 1)))
          ((= n 10) t)
        (princ n))
    0.1.2.3.4.5.6.7.8.9.T
    

    但是,在 Common Lisp 中,我们希望宏只被扩展一次。也就是说,我们希望(gen-counter 1) 被一些代码替换。不过,我们仍然可以生成一段这样的代码:

    (defmacro make-counter (&whole form initial-value)
      (declare (ignore initial-value))
      (let ((text (gensym (string 'text-))))
        `(let ((,text ',form))
           (incf (second ,text)))))
    
    CL-USER> (macroexpand '(make-counter 3))
    (LET ((#:TEXT-1002 '(MAKE-COUNTER 3)))
      (INCF (SECOND #:TEXT-1002)))
    

    然后我们可以用do重新创建示例

    CL-USER> (do ((n 0 (make-counter 1)))
                 ((= n 10) t)
               (princ n))
    023456789
    

    当然,这是未定义的行为,因为它正在修改文字数据。它不适用于所有 Lisps(上面的运行来自 CCL;它在 SBCL 中不起作用)。

    但不要错过重点

    整篇文章有点意思,但要知道这也是个笑话。它指出您可以在不编译代码的评估器中做一些有趣的事情。主要是讽刺指出 Lisp 系统在评估和编译时具有不同行为的不一致性。注意最后一段:

    一些短视的人会指出,这些编程 技术,当然值得称赞的是它们提高了清晰度和 效率,编译代码会失败。可悲的是,这是真的。至少 上述两种技术将使大多数编译器进入无限 环形。但众所周知,大多数 lisp 编译器不会 实现完整的 lisp 语义——例如动态范围。这 只是编译器未能保留语义的另一种情况 正确性。 编译器实现者的任务仍然是 调整他的系统以正确实现源语言,而不是 比用户诉诸丑陋、危险、不便携、不健壮 ``hacks'' 以便围绕有缺陷的编译器进行编程。

    我希望这能让我们深入了解干净、优雅的本质 Lisp 编程技术。

    ——奥林颤抖

    【讨论】:

    • 感谢详尽的回答。我之前实际上没有使用过&whole 说明符。 CL对我来说仍然充满惊喜:)。
    猜你喜欢
    • 1970-01-01
    • 2010-12-15
    • 1970-01-01
    • 2012-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-18
    相关资源
    最近更新 更多