是啥让 Lisp 宏如此特别?

Posted

技术标签:

【中文标题】是啥让 Lisp 宏如此特别?【英文标题】:What makes Lisp macros so special?是什么让 Lisp 宏如此特别? 【发布时间】:2010-09-21 00:55:04 【问题描述】:

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

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

【问题讨论】:

顺便说一下,有一个 LISP 风格的 C# 宏处理器,叫做 LeMP:ecsharp.net/lemp ... javascript 也有一个叫做 Sweet.js:sweetjs.org @Qwertie 现在 sweetjs 还能用吗? 我没用过,但最近一次提交是六个月前...对我来说已经足够了! 【参考方案1】:

你会发现围绕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)

设置somelist的车。

这里真正发生的是“setf”是一个宏。在编译时,它检查它的参数,并发现第一个参数的形式为 (car SOMETHING)。它对自己说:“哦,程序员正在尝试设置汽车。为此使用的函数是 'rplaca'。”它悄悄地重写了代码:

  (rplaca somelist whatever)

【讨论】:

setf 很好地说明了宏的威力,感谢您提供它。 我喜欢突出显示 ..因为 Lisp 程序的源代码不是字符串;这是一个列表。!这是否是 LISP 宏因其括号而优于大多数其他宏的主要原因? @Student 我想是的:books.google.fr/… 表明你是对的。【参考方案2】:

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

expr1 && expr2 && expr3 ...

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

现在试着把这个&& 变成一个函数……没错,你不能。调用类似:

and(expr1, expr2, expr3)

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

使用 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。

希望这会有所帮助。

【讨论】:

我认为应该是:“(apply && ,@exprs) ; 这可能是错误的!” @svante - 有两个方面:首先,&& 是一个宏,而不是一个函数; apply 仅适用于函数。其次,应用要传递的参数列表,因此您需要“(funcall fn,@exprs)”,“(apply fn(list,@exprs)”或“(apply fn,@exprs nil)”之一,而不是“(应用 fn ,@exprs)”。 (and ... 将计算表达式,直到一个计算结果为假,请注意,错误计算产生的副作用会发生,只会跳过后面的表达式。【参考方案3】:

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

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

但是,由于 lisp 宏(本质上)是一个 lisp 程序,它接受 sn-ps 的代码作为输入并返回代码以替换宏的“调用”,因此您可以尽可能地扩展您的“原始”曲目想要,通常以更易读的程序结束。

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

【讨论】:

+!对于以“但是,因为 lisp 宏是……”开头的段落,因为这比其他任何事情都更清楚地说明了整个主题!【参考方案4】:

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

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

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

如果 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();
    

【讨论】:

现在实际上一个用于 C# 的 Lisp 风格的宏处理器,我要指出 using 的宏将是 look like this ;)【参考方案5】:

想想你可以在 C 或 C++ 中使用宏和模板做什么。它们是管理重复代码的非常有用的工具,但它们的局限性非常严重。

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

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

Lisp 宏是用 Lisp 编写的。您拥有 Lisp 编写宏的全部功能。 Lisp 的语法非常规则。

与任何精通 C++ 的人交谈,并询问他们花了多长时间学习进行模板元编程所需的所有模板捏造。或者像 Modern C++ Design 这样的(优秀的)书籍中的所有疯狂技巧,即使该语言已经标准化为十年。如果您用于元编程的语言与您用于编程的语言相同,那么所有这些都会消失!

【讨论】:

好吧,说句公道话,C++ 模板元编程的问题不在于元编程语言不同,而在于它太可怕了——与其说是设计不如说是被发现旨在成为一个更简单的模板功能。 @***s 当然。新兴特征并不总是坏事。不幸的是,在一个缓慢发展的委员会驱动语言中,当它们出现时很难修复它们。遗憾的是,C++ 的现代实用新特性中的许多都是用一种很少有人能读懂的语言编写的,而普通程序员和“大祭司”之间存在巨大差距。 @downvoter:如果我的回答有问题,请发表评论,以便我们共同分享知识。【参考方案6】:

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

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

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

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

因此,您最终会获得一种在语言之上创建自己的代码生成器的方法,使用语言本身的全部功能(例如,在 Java 中,您必须通过字节码编织来破解自己的方式,尽管 AspectJ 等一些框架允许您使用不同的方法来执行此操作,这基本上是一种 hack)。

在实践中,使用宏最终您可以在 lisp 之上构建自己的迷你语言,而无需学习其他语言或工具,并使用语言本身的全部功能。

【讨论】:

这是很有见地的评论,但是,“一切都是列表”的想法可能会吓到新手。要了解列表,您需要了解 cons、cars、cdrs、cells。更准确地说,Lisp 是由 S-expressions 组成的,而不是列表。【参考方案7】:

简而言之,宏是代码的转换。它们允许引入许多新的语法结构。例如,考虑 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 上循环、正确关闭它们。

【讨论】:

【参考方案8】:

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

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

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

【讨论】:

你的第一段对于 Lisp 局外人来说非常清楚,这很重要。【参考方案9】:

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

【讨论】:

特别是如果您有 Java/XML 背景。 周六下午躺在我的沙发上阅读这篇文章真是太高兴了!写得非常清楚,条理清晰。 上帝保佑你和作者。 这是一篇很长的文章,但很值得一读——其中很多是序言,可以归结为——1)Lisp S 表达式可以像 XML 一样表示代码或数据,2 ) 宏不会像函数那样急切地评估其输入,因此可以将输入作为代码或数据的 s 表达式结构进行操作。令人兴奋的时刻是,像“todo 列表”表示这样平凡的东西可以通过实现一个宏来武器化为代码,该宏可以将 todo 数据结构视为项目宏的代码输入。这在大多数语言中都不会考虑,而且很酷。【参考方案10】:

为了给出简短的回答,宏用于定义对 Common Lisp 或领域特定语言 (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))))

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

当然,这需要大量打字,而且程序员很懒惰。所以我们可以定义 DSL 来进行列表推导。事实上,我们已经在使用一个宏(循环宏)。

Lisp 定义了一些特殊的语法形式。引号 (') 表示下一个标记是文字。准引号或反引号 (`) 表示下一个标记是带有转义的文字。转义符由逗号运算符指示。文字 '(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] - (lcomp x for x in (range 10) if (= (% x 2) 0)) 中使用的python 完全相同

(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 用于在 Lisp 中实现列表理解,为什么不呢? @ckb 实际上 LISP 在标准库中已经有一个列表解析宏:(loop for x from 0 below 10 when (evenp x) collect x),more examples here。但实际上,循环“只是一个宏”(我实际上是re-implemented it from scratch some time ago) 我知道这很不相关,但我想知道语法以及解析的实际工作原理......假设我以这种方式调用 lcomp(将 thirs 项目从“for”更改为“azertyuiop”) : (lcomp x azertyuiop x in (range 10) if (= (% x 2) 0)) 宏还会按预期工作吗?还是循环中使用了“for”参数,所以调用时必须是字符串“for”? 我对其他语言的宏感到困惑的一点是,它们的宏受到宿主语言语法的限制。 Lispy 宏可以解释非 Lispy 语法吗?我的意思是想象创建一个类似 haskell 的语法(没有括号)并使用 Lisp 宏解释它。这可能吗?与直接使用词法分析器和解析器相比,使用宏的优缺点是什么? @CMCDragonkai 简单的回答,是的,lisp 宏通常用于创建领域特定语言。宿主语言总是对可以在宏中使用的语法施加一些限制。例如,您显然不能将注释语法用作宏中的活动组件。【参考方案11】:

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

【讨论】:

【参考方案12】:

我从 The common lisp cookbook 中得到了这个,但我认为它以一种很好的方式解释了为什么 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

【讨论】:

如果 xy 通过引用传递,则可以将 setq2 定义为函数。但是,我不知道在 CL 中是否有可能。因此,对于特别不了解 Lisps 或 CL 的人来说,这不是 IMO 的说明性示例 @neoascetic CL 参数仅按值传递(这就是它首先需要宏的原因)。有些值是指针(如列表)。【参考方案13】:

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

...在 C 中,您必须编写一个自定义预处理器 [可能符合sufficiently complicated C program] ...

—Vatine

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

—Matt Curtis

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

—Miguel Ping

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, in "Practical Common Lisp"

【讨论】:

【参考方案14】:

虽然以上所有内容都解释了宏是什么,甚至还有一些很酷的示例,但我认为宏和普通函数之间的主要区别在于 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

【讨论】:

顺便说一句,Scala 已将宏添加到该语言中。虽然它们缺乏 Lisp 宏的美感,因为该语言不是谐音语言,但它们绝对值得研究,它们提供的抽象语法树最终可能更容易使用。现在说我更喜欢哪种宏系统还为时过早。 “LISP 将未计算的参数传递给宏” 终于得到了一个简单明了的答案。但是您忘记了后半部分:宏的结果是转换后的代码,系统将代替原始代码对它进行整体评估,就好像它最初就在那里一样(除非它本身又是对宏的调用,这次宏也将被 that 转换)。【参考方案15】:

单线答案:

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


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

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

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

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

【讨论】:

以上是关于是啥让 Lisp 宏如此特别?的主要内容,如果未能解决你的问题,请参考以下文章

宏(用户自定义代码转换)的想法是啥时候出现的?

是啥让某些 android 类“必须保留”?

是啥让 API 变得“丰富”? [关闭]

是啥让 nativescript 比 ionic 更好

是啥让“FBAudienceNetwork”链接花了 20 秒?

是啥让移动对象比复制更快?