是什么让Lisp宏如此特别?

时间:2008-11-06 07:33:12

标签: macros lisp homoiconicity

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

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

14 个答案:

答案 0 :(得分:281)

简而言之,宏用于定义Common Lisp或Domain Specific Languages(DSL)的语言语法扩展。这些语言嵌入到现有的Lisp代码中。现在,DSL可以具有类似于Lisp的语法(如Peter Norvig的Prolog Interpreter用于Common Lisp)或完全不同(例如,Infix Notation Math用于Clojure)。

这是一个更具体的例子:
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定义了几种特殊的语法形式。引号(')表示下一个标记是文字。 quasiquote或反引号(`)表示下一个标记是带有转义的文字。转义由逗号运算符指示。文字'(1 2 3)相当于Python的[1, 2, 3]。您可以将其分配给另一个变量或在适当的位置使用它。您可以将`(1 2 ,x)视为Python [1, 2, x]的等价物,其中x是先前定义的变量。这个列表符号是进入宏的魔力的一部分。第二部分是Lisp阅读器,它智能地将宏代替代码,但最好说明如下:

因此我们可以定义一个名为lcomp的宏(列表推导的简称)。它的语法与我们在示例[x for x in range(10) if x % 2 == 0]中使用的python完全相同 - (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的最佳灵活性。

答案 1 :(得分:102)

你会发现有关lisp macro here的全面辩论。

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

  

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

     

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

     

这是一个扩展示例。 Lisp有一个名为“setf”的宏,它执行赋值。最简单的setf形式是

  (setf x whatever)
     

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

     

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

     

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

  (setf (car somelist) whatever)
     

设置某人的车。

     

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

  (rplaca somelist whatever)

答案 2 :(得分:53)

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

例如,在C中,switch / case结构只适用于整数类型,如果你想将它用于浮点数或字符串,你可以使用嵌套的if语句和显式比较。你也无法编写C宏来为你完成这项工作。

但是,由于lisp宏(实质上)是一个lisp程序,它将代码片段作为输入并返回代码以替换宏的“调用”,因此您可以根据需要扩展“原语”指令集,通常以更易读的程序结束。

要在C中执行相同操作,您必须编写一个自定义预处理器,它会占用您的初始(非C)源并吐出C编译器可以理解的内容。这不是一个错误的方法,但它不一定是最容易的。

答案 3 :(得分:39)

Lisp宏允许您决定何时(如果有的话)将评估任何部分或表达式。举一个简单的例子,想想C:

expr1 && expr2 && expr3 ...

这说的是:评估expr1,如果是真的,请评估expr2等。

现在尝试将此&&变成一个函数......这是对的,你做不到。打电话给:

and(expr1, expr2, expr3)

无论exprs是否为假,都会在得到答案之前评估所有三个expr1

使用lisp宏,您可以编写如下代码:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

现在你有一个&&,你可以像一个函数一样调用它,它不会评估你传递给它的任何表格,除非它们都是真的。

要了解这是如何有用的,对比:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

你可以用宏做的其他事情是创建新的关键字和/或迷你语言(例如,查看(loop ...)宏),将其他语言集成到lisp中,例如,你可以写一个宏你可以这样说:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

那甚至没有进入Reader macros

希望这有帮助。

答案 4 :(得分:28)

我认为我从未见过Lisp宏解释得比这个人更好:http://www.defmacro.org/ramblings/lisp.html

答案 5 :(得分:10)

lisp宏将程序片段作为输入。该程序片段表示一种数据结构,可以按照您喜欢的方式进行操作和转换。最后,宏输出另一个程序片段,这个片段是在运行时执行的。

C#没有宏功能,但如果编译器将代码解析为CodeDOM树,并将其传递给方法,则将其转换为另一个CodeDOM,然后将其编译为IL。 / p>

这可用于实现“糖”语法,如for each - 语句using - 子句,linq select - 表达式等,作为转换为底层代码的宏

如果Java有宏,你可以在Java中实现Linq语法,而不需要Sun更改基本语言。

以下是C#中用于实现using的lisp样式宏的伪代码:

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }

答案 6 :(得分:9)

考虑使用宏和模板在C或C ++中可以做什么。它们是管理重复代码的非常有用的工具,但它们在很严重的方面受到限制。

  • 有限的宏/模板语法限制了它们的使用。例如,您不能编写扩展为类或函数以外的模板的模板。宏和模板无法轻松维护内部数据。
  • C和C ++的复杂,非常不规则的语法使得编写非常通用的宏变得困难。

Lisp和Lisp宏解决了这些问题。

  • Lisp宏是用Lisp编写的。你有Lisp的全部功能来编写宏。
  • Lisp有一个非常规则的语法。

与任何掌握了C ++的人交谈,并询问他们花了多长时间学习模板元编程所需的模板软件。或者(优秀)书籍中的所有疯狂技巧,如现代C ++设计,即使语言已经标准化,仍然难以调试和(实际上)在现实编译器之间不可移植十年。如果用于元编程的语言与用于编程的语言相同,那么所有这些都会消失!

答案 7 :(得分:8)

我不确定我是否可以为每个人的(优秀)帖子添加一些见解,但是......

由于Lisp语法本质,Lisp宏工作得很好。

Lisp是一种非常规则的语言(想想一切都是列表);宏使您可以将数据和代码视为相同(不需要字符串解析或其他黑客来修改lisp表达式)。您将这两个功能结合起来,并且有一种非常干净的方式来修改代码。

编辑:我想说的是Lisp是homoiconic,这意味着lisp程序的数据结构是用lisp编写的。

所以,你最终会想到一种在语言之上创建自己的代码生成器的方法,使用语言本身的所有功能(例如,在Java中你必须使用字节码编织来破解你的方式,尽管像AspectJ这样的一些框架允许你使用不同的方法做到这一点,它从根本上说是一个黑客攻击。)

实际上,使用宏,您最终会在lisp之上构建自己的迷你语言,而无需学习其他语言或工具,并且可以充分利用语言本身的强大功能。

答案 8 :(得分:8)

由于现有的答案提供了很好的具体例子来解释宏实现了什么以及如何实现,也许它有助于收集关于为什么宏设施与其他语言相关的重要收益的一些想法;首先来自这些答案,然后是来自其他地方的伟大答案:

  

...在C中,您必须编写一个自定义预处理器[可能有资格作为sufficiently complicated C program] ......

- Vatine

  

与任何掌握了C ++的人交谈并询问他们花了多长时间学习模板元编程所需的所有模板技巧[仍然没有那么强大]。

- Matt Curtis

  

...在Java中你必须使用字节码编织来破解你的方式,尽管像AspectJ这样的一些框架允许你使用不同的方法来实现这一点,但它从根本上说是一个黑客攻击。

- Miguel Ping

  

DOLIST类似于Perl的foreach或Python的。作为JSR-201的一部分,Java在Java 1.5中添加了类似的循环结构和“增强”for循环。注意宏有什么区别。一个Lisp程序员在他们的代码中注意到一个共同的模式,可以编写一个宏来给自己提供该模式的源级抽象。注意到相同模式的Java程序员必须说服Sun,这种特殊的抽象值得添加到该语言中。然后Sun必须发布一个JSR并召集一个行业范围的“专家组”来解决所有问题。根据Sun的说法,这个过程平均需要18个月。之后,编译器编写者都必须升级他们的编译器以支持新功能。甚至一旦Java程序员最喜欢的编译器支持新版本的Java,他们可能“仍然”不能使用新功能,直到它们被允许破坏与旧版Java的源兼容性。因此,Common Lisp程序员可以在五分钟内解决自己的烦恼,困扰Java程序员多年。

- Peter Seibel, in "Practical Common Lisp"

答案 9 :(得分:6)

Lisp宏代表几乎任何大型编程项目中出现的模式。最终在一个大型程序中你有一段代码,你会发现它会更简单,更容易出错,你可以编写一个程序,将源代码作为文本输出,然后你就可以粘贴了。

在Python中,对象有两种方法__repr____str____str__只是人类可读的表示。 __repr__返回一个有效Python代码的表示,也就是说,可以作为有效的Python输入到解释器中的东西。通过这种方式,您可以创建一小段Python,生成可以粘贴到实际源代码中的有效代码。

在Lisp中,整个过程已由宏系统正式化。当然它可以让你创建语法的扩展并做各种奇特的事情,但它的实际用处总结如上。当然,Lisp宏系统允许您使用整个语言的全部功能来操纵这些“片段”。

答案 10 :(得分:5)

简而言之,宏是代码的转换。它们允许引入许多新的语法结构。例如,考虑C#中的LINQ。在lisp中,存在由宏实现的类似语言扩展(例如,内置循环构造,迭代)。宏显着减少了代码重复。宏允许嵌入«小语言»(例如,在c#/ java中使用xml进行配置,在lisp中,使用宏可以实现相同的功能)。宏可能会隐藏使用库的困难。

例如,在lisp中你可以写

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

这隐藏了所有数据库内容(事务,正确连接关闭,获取数据等),而在C#中,这需要创建SqlConnections,SqlCommands,将SqlParameters添加到SqlCommands,在SqlDataReaders上循环,正确关闭它们。

答案 11 :(得分:2)

虽然上面都解释了什么是宏,甚至有很酷的例子,但我认为宏和普通函数之间的关键区别在于LISP在调用函数之前首先计算所有参数。对于宏,它是相反的,LISP将未评估的参数传递给宏。例如,如果将(+ 1 2)传递给函数,函数将接收值3.如果将其传递给宏,它将收到一个List(+ 1 2)。这可以用来做各种非常有用的东西。

  • 添加新的控制结构,例如循环或解构列表
  • 测量执行传入函数所需的时间。使用函数,在控制传递给函数之前,将评估参数。使用宏,您可以在秒表的开始和结束之间拼接代码。下面在宏和函数中具有完全相同的代码,输出非常不同。 注意:这是一个人为的例子,选择的实现是相同的,以便更好地突出差异。

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0
    

答案 12 :(得分:0)

我从普通的lisp食谱中得到了这个,但我认为它解释了为什么lisp宏很好用。

“宏是一段普通的Lisp代码,它运行在另一个推定的Lisp代码上,将其转换为(更接近于可执行Lisp的版本)。这可能听起来有点复杂,所以让我们举一个简单的例子。假设你想要一个将两个变量设置为相同值的setq版本。所以如果你写了

(setq2 x y (+ z 3))

z=8 x和y都设置为11.(我想不出任何用途,但这只是一个例子。)

很明显,我们不能将setq2定义为函数。如果x=50y=-5,此函数将收到值50,-5和11;它不知道应该设置哪些变量。我们真正想说的是,当你(Lisp系统)看到(setq2 v1 v2 e)时,将其视为等同于(progn (setq v1 e) (setq v2 e))。实际上,这不太对,但它现在会做。宏允许我们通过指定用于将输入模式(setq2 v1 v2 e)“转换为输出模式(progn ...)的程序来完成此操作。”

如果您认为这很好,您可以继续阅读: http://cl-cookbook.sourceforge.net/macros.html

答案 13 :(得分:-3)

在python中你有装饰器,你基本上有一个函数,它将另一个函数作为输入。你可以做任何你想做的事情:调用函数,做其他事情,在资源获取版本中包装函数调用等等,但你不能窥探那个函数。假设我们想让它更强大,说你的装饰器收到函数的代码作为列表然后你不仅可以执行函数,但你现在可以执行它的一部分,重新​​排序函数的行等。