335

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

还请将此与我从 Python、Java、C# 或 C 开发领域理解的内容联系起来。

4

15 回答 15

351

简而言之,宏用于定义 Common Lisp 或领域特定语言 (DSL) 的语言语法扩展。这些语言直接嵌入到现有的 Lisp 代码中。现在,DSL 可以具有类似于 Lisp 的语法(例如 Peter Norvig 的Common Lisp的Prolog 解释器)或完全不同的语法(例如 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 定义了一些特殊的语法形式。引号 ( ') 表示下一个标记是文字。准引号或反引号 ( `) 表示下一个标记是带有转义的文字。转义符由逗号运算符指示。文字'(1 2 3)相当于 Python 的[1, 2, 3]. 您可以将其分配给另一个变量或就地使用它。您可以将其`(1 2 ,x)视为 Python 的[1, 2, x]where xis a variable 先前定义的等价物。这个列表符号是进入宏的魔法的一部分。第二部分是 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 吸引人们的地方——终极的灵活性。

于 2011-01-07T01:56:36.450 回答
109

您将在此处找到有关 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”是一个宏。在编译时,它检查它的参数,并发现第一个参数的形式为 (car SOMETHING)。它对自己说:“哦,程序员正在尝试设置汽车。为此使用的函数是'rplaca'。” 它悄悄地重写了代码:

  (rplaca somelist whatever)
于 2008-11-06T07:45:13.620 回答
58

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

例如,在 C 中,switch/case 构造仅适用于整数类型,如果您想将其用于浮点数或字符串,则剩下嵌套的 if 语句和显式比较。您也无法编写 C 宏来为您完成这项工作。

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

要在 C 中做同样的事情,您必须编写一个自定义预处理器,它会吃掉您的初始(不是完全 C)源代码并吐出 C 编译器可以理解的东西。这不是一个错误的方法,但它不一定是最简单的。

于 2008-11-06T09:39:50.660 回答
47

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%")

那甚至没有进入阅读器宏

希望这可以帮助。

于 2008-11-06T09:21:51.790 回答
33

我想我从来没有见过比这个家伙解释得更好的 Lisp 宏:http: //www.defmacro.org/ramblings/lisp.html

于 2011-01-07T00:58:55.527 回答
10

Think of what you can do in C or C++ with macros and templates. They're very useful tools for managing repetitive code, but they're limited in quite severe ways.

  • Limited macro/template syntax restricts their use. For example, you can't write a template which expands to something other than a class or a function. Macros and templates can't easily maintain internal data.
  • The complex, very irregular syntax of C and C++ makes it difficult to write very general macros.

Lisp and Lisp macros solve these problems.

  • Lisp macros are written in Lisp. You have the full power of Lisp to write the macro.
  • Lisp has a very regular syntax.

Talk to anyone that's mastered C++ and ask them how long they spent learning all the template fudgery they need to do template metaprogramming. Or all the crazy tricks in (excellent) books like Modern C++ Design, which are still tough to debug and (in practice) non-portable between real-world compilers even though the language has been standardised for a decade. All of that melts away if the langauge you use for metaprogramming is the same language you use for programming!

于 2008-11-06T11:32:01.433 回答
9

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

C# 没有宏工具,但是如果编译器将代码解析为 CodeDOM 树,并将其传递给一个方法,该方法将其转换为另一个 CodeDOM,然后将其编译为 IL,则相当于 C#。

这可用于实现“糖”语法,如for each-statement -clause using、linq -expressionsselect等,作为转换为底层代码的宏。

如果 Java 有宏,您可以在 Java 中实现 Linq 语法,而无需 Sun 更改基础语言。

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

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }
于 2008-11-06T10:27:52.123 回答
9

由于现有的答案提供了很好的具体示例来解释宏实现什么以及如何实现,也许它有助于收集一些关于为什么宏工具相对于其他语言具有显着优势的一些想法;首先来自这些答案,然后是来自其他地方的一个很好的答案:

...在 C 中,您必须编写一个自定义预处理器 [这可能有资格作为一个 足够复杂的 C 程序] ...

梵蒂冈

与任何精通 C++ 的人交谈,并询问他们花了多长时间学习进行模板元编程所需的所有模板捏造(这仍然没有那么强大)。

马特·柯蒂斯

...在Java中,您必须使用字节码编织来破解自己的方式,尽管像AspectJ这样的一些框架允许您使用不同的方法来做到这一点,但它基本上是一种破解。

米格尔·平

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

Peter Seibel,在“实用的普通 Lisp”中

于 2014-02-09T19:36:30.593 回答
8

I'm not sure I can add some insight to everyone's (excellent) posts, but...

Lisp macros work great because of the Lisp syntax nature.

Lisp is an extremely regular language (think of everything is a list); macros enables you to treat data and code as the same (no string parsing or other hacks are needed to modify lisp expressions). You combine these two features and you have a very clean way to modify code.

Edit: What I was trying to say is that Lisp is homoiconic, which means that the data structure for a lisp program is written in lisp itself.

So, you end up with a way of creating your own code generator on top of the language using the language itself with all its power (eg. in Java you have to hack your way with bytecode weaving, although some frameworks like AspectJ allows you to do this using a different approach, it's fundamentally a hack).

In practice, with macros you end up building your own mini-language on top of lisp, without the need to learn additional languages or tooling, and with using the full power of the language itself.

于 2008-11-06T12:12:39.010 回答
6

Lisp 宏代表了一种几乎出现在任何大型编程项目中的模式。最终,在一个大型程序中,您有一段代码,您意识到编写一个将源代码输出为文本的程序会更简单,更不容易出错,然后您可以将其粘贴进去。

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

在 Lisp 中,这整个过程已经被宏系统形式化了。当然,它使您能够创建语法扩展并做各种花哨的事情,但上面总结了它的实际用处。当然,Lisp 宏系统允许您使用整个语言的全部功能来操作这些“片段”,这很有帮助。

于 2008-11-06T14:31:30.603 回答
5

In short, macros are transformations of code. They allow to introduce many new syntax constructs. E.g., consider LINQ in C#. In lisp, there are similar language extensions that are implemented by macros (e.g., built-in loop construct, iterate). Macros significantly decrease code duplication. Macros allow embedding «little languages» (e.g., where in c#/java one would use xml to configure, in lisp the same thing can be achieved with macros). Macros may hide difficulties of using libraries usage.

E.g., in lisp you can write

(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))

and this hides all the database stuff (transactions, proper connection closing, fetching data, etc.) whereas in C# this requires creating SqlConnections, SqlCommands, adding SqlParameters to SqlCommands, looping on SqlDataReaders, properly closing them.

于 2008-11-06T12:27:01.027 回答
3

虽然以上都解释了宏是什么,甚至还有很酷的例子,但我认为宏和普通函数之间的主要区别在于 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
    
于 2017-05-19T03:33:58.377 回答
0

我从 The common lisp cookbook 中得到了这个,但我认为它解释了为什么 lisp 宏很好用。

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

(setq2 x y (+ z 3))

z=8x 和 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

于 2013-04-02T16:49:18.667 回答
0

单线答案:

最小语法 => 宏优于表达式 => 简洁 => 抽象 => 强大


Lisp 宏无非就是以编程方式编写代码。也就是说,在扩展宏之后,你得到的只是没有宏的 Lisp 代码。因此,原则上,他们没有取得任何新成果。

但是,它们与其他编程语言中的宏的不同之处在于它们在表达式级别编写代码,而其他宏在字符串级别编写代码。由于括号,这是 lisp 独有的;或者更准确地说,是它们的最小语法,这要归功于它们的括号。

正如该线程中的许多示例以及 Paul Graham 的On Lisp所示,lisp 宏可以成为使您的代码更加简洁的工具。当简洁达到一定程度时,它提供了新的抽象级别,使代码更加简洁。再次回到第一点,原则上它们不提供任何新东西,但这就像说因为纸和铅笔(几乎)形成了图灵机,我们不需要实际的计算机。

如果有人知道一些数学,想想为什么函子和自然变换是有用的想法。原则上,他们不提供任何新东西。但是,通过将它们扩展到较低级别的数学,您会发现一些简单的想法(就范畴论而言)的组合可能需要 10 页才能写下来。你更倾向哪个?

于 2022-01-01T22:28:51.250 回答
-4

在 python 中,你有装饰器,你基本上有一个函数,它接受另一个函数作为输入。你可以做任何你想做的事情:调用函数,做其他事情,将函数调用包装在资源获取释放中,等等,但你不能窥视该函数的内部。假设我们想让它更强大,假设你的装饰器将函数的代码作为列表接收,那么你不仅可以按原样执行函数,而且现在可以执行它的一部分,重新​​排序函数的行等。

于 2011-01-07T17:02:44.060 回答