是什么让Lisp宏如此特别?

阅读Paul Graham关于编程语言的散文,人们会认为Lisp宏是唯一的出路。 作为一名繁忙的开发人员,在其他平台上工作,我没有使用Lisp宏的特权。 作为想要了解这一嗡嗡声的人,请解释是什么让这个功能如此强大。

请将此与我从Python,Java,C#或C开发世界中理解的内容联系起来。


为了简短回答,宏用于定义Common Lisp或域特定语言(DSL)的语法扩展。 这些语言被嵌入到现有的Lisp代码中。 现在,DSL可以具有类似于Lisp的语法(例如Peter Norvig的Common Lisp的Prolog解释器)或完全不同的(例如,用于Clojure的Infix Notation Math)。

这是一个更具体的例子:
Python具有内建在语言中的列表解析。 这为常见情况提供了一个简单的语法。 该线

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

产生一个包含0到9之间的所有偶数的列表。回到Python 1.5天后,没有这样的语法; 你会用更像这样的东西:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

这些在功能上都是等同的。 让我们调用我们暂停的怀疑和假装Lisp有一个非常有限的循环宏,它只是迭代,并没有简单的方法来完成列表解析的等价操作。

在Lisp中,您可以编写以下内容。 我应该注意到这个人为的例子被挑选为与Python代码相同,而不是Lisp代码的一个好例子。

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

在我进一步讨论之前,我应该更好地解释一下宏是什么。 它是由代码执行的代码转换。 也就是说,由解释器(或编译器)读取的一段代码将代码作为参数进行操作,然后返回结果,然后将结果运行到原位。

当然,这是很多打字和程序员都懒惰。 所以我们可以定义DSL来完成列表解析。 事实上,我们已经在使用一个宏(循环宏)。

Lisp定义了几种特殊的语法形式。 quote( ' )表示下一个标记是一个文字。 quasiquote或反引号( ` )表示下一个标记是带有转义的文字。 逗号由逗号运算符表示。 文字'(1 2 3)与Python的[1, 2, 3]等价。 您可以将其分配给其他变量或使用它。 你可以认为`(1 2 ,x)等价于Python的[1, 2, x] ,其中x是先前定义的变量。 这个列表符号是进入宏的魔法的一部分。 第二部分是Lisp阅读器,智能地将宏替换为代码,但下面最好说明这一点:

所以我们可以定义一个名为lcomp的宏(列表理解的简称)。 它的语法与我们在例子中使用的python完全相同[x for x in range(10) if x % 2 == 0] - (lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

现在我们可以在命令行执行:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

漂亮整洁,是吧? 现在它不止于此。 如果你喜欢,你有机制或画笔。 你可以有任何你可能想要的语法。 像Python或C#的with语法。 或.NET的LINQ语法。 最后,Lisp吸引人们 - 最终的灵活性。


你会在这里找到关于lisp宏的全面辩论。

该文章的一个有趣的子集:

在大多数编程语言中,语法很复杂。 宏必须拆开程序语法,分析它,然后重新组装它。 他们无法访问程序的解析器,因此他们必须依赖启发式和最佳猜测。 有时他们的削减率分析是错误的,然后他们破裂。

但是Lisp是不同的。 Lisp宏可以访问解析器,它是一个非常简单的解析器。 Lisp宏不是一个字符串,而是一个列表形式的源代码,因为Lisp程序的源不是一个字符串; 它是一个列表。 Lisp程序非常擅长将列表拆分并重新组合。 他们每天都可靠地做到这一点。

这是一个扩展的例子。 Lisp有一个叫做“setf”的宏来执行任务。 最简单的setf形式是

  (setf x whatever)

它将符号“x”的值设置为表达式“whatever”的值。

Lisp也有列表; 您可以使用“car”和“cdr”函数分别获取列表的第一个元素或列表的其余部分。

现在,如果你想用新值替换列表的第一个元素呢? 这样做有一个标准功能,令人难以置信的是,它的名字比“汽车”还要糟糕。 这是“rplaca”。 但是你不必记得“rplaca”,因为你可以写

  (setf (car somelist) whatever)

设置somelist的汽车。

这里真正发生的是“setf”是一个宏。 在编译时,它检查它的论点,并且它看到第一个有形式(汽车SOMETHING)。 它对自己说:“噢,程序员正在试图设置一些东西,这个功能就是'rplaca'。” 它悄悄地将代码重写为:

  (rplaca somelist whatever)

Common Lisp宏实质上扩展了你的代码的“语法原语”。

例如,在C语言中,switch / case结构只能用于整型类型,如果您想将它用于浮点型或字符串,则会留下嵌套if语句和显式比较。 你也没有办法编写一个C宏来为你完成这项工作。

但是,由于lisp宏(本质上)是一个lisp程序,它将代码片段作为输入并返回代码来代替宏的“调用”,所以可以根据需要扩展“基元”具有更易读的程序。

要在C中执行相同的操作,您必须编写一个定制的预处理器来消化您的初始(not-quite-C)源代码,并且吐出一些C编译器可以理解的东西。 这不是一个错误的方式去做,但它并不一定是最简单的。

链接地址: http://www.djcxy.com/p/60935.html

上一篇: What makes Lisp macros so special?

下一篇: Lisp in the real world