阅读有关编程语言的 Paul Graham's essays 会认为 Lisp macros 是唯一的出路。作为一个忙碌的开发人员,在其他平台上工作,我没有使用 Lisp 宏的特权。作为一个想了解嗡嗡声的人,请解释一下是什么让这个功能如此强大。
还请将此与我从 Python、Java、C# 或 C 开发领域理解的内容联系起来。
简而言之,宏用于定义 Common Lisp 或领域特定语言 (DSL) 的语言语法扩展。这些语言直接嵌入到现有的 Lisp 代码中。现在,DSL 可以具有类似于 Lisp 的语法(例如 Peter Norvig 的用于 Common Lisp 的 Prolog Interpreter)或完全不同的语法(例如 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 定义了一些特殊的语法形式。引号 ('
) 表示下一个标记是文字。准引号或反引号 (`
) 表示下一个标记是带有转义的文字。转义符由逗号运算符指示。文字 '(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 吸引人们的地方——终极的灵活性。
您会发现围绕 lisp macro here 的全面辩论。
那篇文章的一个有趣的子集:
在大多数编程语言中,语法很复杂。宏必须分解程序语法,分析它,然后重新组合它。他们无法访问程序的解析器,因此他们必须依赖启发式和最佳猜测。有时他们的降价分析是错误的,然后他们就破产了。但 Lisp 不同。 Lisp 宏确实可以访问解析器,它是一个非常简单的解析器。一个 Lisp 宏不是交给一个字符串,而是一个预先解析的以列表形式存在的源代码,因为 Lisp 程序的源不是一个字符串;这是一个列表。 Lisp 程序非常擅长分解列表并将它们重新组合在一起。他们每天都可靠地做到这一点。这是一个扩展示例。 Lisp 有一个宏,叫做“setf”,它执行赋值。 setf 的最简单形式是 (setf x whatever),它将符号“x”的值设置为表达式“whatever”的值。 Lisp 也有列表。您可以使用“car”和“cdr”函数分别获取列表的第一个元素或列表的其余部分。现在,如果您想用新值替换列表的第一个元素怎么办?有一个标准函数可以做到这一点,令人难以置信的是,它的名字甚至比“汽车”还要糟糕。它是“rplaca”。但是您不必记住“rplaca”,因为您可以编写 (setf (car somelist) 不管) 来设置 somelist 的汽车。这里真正发生的是“setf”是一个宏。在编译时,它检查它的参数,并发现第一个参数的形式为 (car SOMETHING)。它对自己说:“哦,程序员正在尝试设置汽车。为此使用的函数是'rplaca'。”它悄悄地将代码重写为:(rplaca somelist 不管)
Common Lisp 宏本质上扩展了代码的“句法原语”。
例如,在 C 中,switch/case 构造仅适用于整数类型,如果您想将其用于浮点数或字符串,则剩下嵌套的 if 语句和显式比较。您也无法编写 C 宏来为您完成这项工作。
但是,由于 lisp 宏(本质上)是一个 lisp 程序,它将代码片段作为输入并返回代码以替换宏的“调用”,因此您可以根据需要扩展您的“原始”曲目,通常结束具有更易读的程序。
要在 C 中做同样的事情,您必须编写一个自定义预处理器,它会吃掉您的初始(不是完全 C)源代码并吐出 C 编译器可以理解的东西。这不是一个错误的方法,但它不一定是最简单的。
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。
希望这可以帮助。
(and ...
将评估表达式,直到一个评估为假,请注意,错误评估产生的副作用将会发生,只有后续的表达式将被跳过。
我想我从来没有见过比这个人解释得更好的 Lisp 宏:http://www.defmacro.org/ramblings/lisp.html
lisp 宏将程序片段作为输入。这个程序片段表示一个数据结构,可以按照您喜欢的任何方式进行操作和转换。最后宏输出另一个程序片段,这个片段是在运行时执行的。
C# 没有宏工具,但是如果编译器将代码解析为 CodeDOM 树,并将其传递给一个方法,该方法将其转换为另一个 CodeDOM,然后将其编译为 IL,则相当于 C# 工具。
这可用于实现“糖”语法,如 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();
}
using
的宏将是 look like this ;)
由于现有的答案提供了很好的具体示例来解释宏实现什么以及如何实现,也许它有助于收集一些关于为什么宏工具相对于其他语言具有显着优势的一些想法;首先来自这些答案,然后是来自其他地方的一个很好的答案:
...在 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, in "Practical Common Lisp"
想想你可以在 C 或 C++ 中使用宏和模板做什么。它们是管理重复代码的非常有用的工具,但它们的局限性非常严重。
有限的宏/模板语法限制了它们的使用。例如,您不能编写扩展为类或函数以外的其他内容的模板。宏和模板无法轻松维护内部数据。
C 和 C++ 的复杂、非常不规则的语法使得编写非常通用的宏变得困难。
Lisp 和 Lisp 宏解决了这些问题。
Lisp 宏是用 Lisp 编写的。您拥有 Lisp 的全部功能来编写宏。
Lisp 有一个非常规则的语法。
与任何精通 C++ 的人交谈,并询问他们花了多长时间学习进行模板元编程所需的所有模板捏造。或者像 Modern C++ Design 这样的(优秀的)书籍中的所有疯狂技巧,即使该语言已经标准化了十年,它们仍然难以调试并且(在实践中)在现实世界的编译器之间不可移植。如果您用于元编程的语言与您用于编程的语言相同,那么所有这些都会消失!
我不确定我是否可以为每个人的(优秀)帖子添加一些见解,但是......
由于 Lisp 语法性质,Lisp 宏工作得很好。
Lisp 是一种非常规则的语言(认为一切都是一个列表);宏使您能够将数据和代码视为相同(修改 lisp 表达式不需要字符串解析或其他技巧)。您将这两个功能结合起来,您就有了一种非常简洁的方式来修改代码。
编辑:我想说的是 Lisp 是 homoiconic,这意味着 lisp 程序的数据结构是用 lisp 本身编写的。
因此,您最终会得到一种在语言之上创建自己的代码生成器的方法,使用语言本身的所有功能(例如,在 Java 中,您必须通过字节码编织来破解自己的方式,尽管 AspectJ 等一些框架允许您使用不同的方法来做到这一点,这基本上是一种黑客攻击)。
在实践中,使用宏,您最终会在 lisp 之上构建自己的迷你语言,而无需学习其他语言或工具,并使用语言本身的全部功能。
S-expressions
组成的,而不是列表。
Lisp 宏代表了一种几乎出现在任何大型编程项目中的模式。最终,在一个大型程序中,您有一段代码,您意识到编写一个将源代码输出为文本的程序会更简单,更不容易出错,然后您可以将其粘贴进去。
在 Python 中,对象有两个方法 __repr__
和 __str__
。 __str__
只是人类可读的表示。 __repr__
返回有效 Python 代码的表示形式,也就是说,可以作为有效 Python 输入解释器的内容。通过这种方式,您可以创建生成有效代码的 Python 小片段,这些代码可以粘贴到您的实际源代码中。
在 Lisp 中,这整个过程已经被宏系统形式化了。当然,它使您能够创建语法扩展并做各种花哨的事情,但上面总结了它的实际用处。当然,Lisp 宏系统允许您使用整个语言的全部功能来操作这些“片段”,这很有帮助。
简而言之,宏是代码的转换。它们允许引入许多新的语法结构。例如,考虑 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 上循环、正确关闭它们。
虽然以上都解释了宏是什么,甚至还有很酷的例子,但我认为宏和普通函数之间的主要区别在于 LISP 在调用函数之前首先评估所有参数。使用宏则相反,LISP 将未计算的参数传递给宏。例如,如果您将 (+ 1 2) 传递给函数,该函数将收到值 3。如果您将其传递给宏,它将收到 List(+ 1 2)。这可以用来做各种非常有用的事情。
添加新的控制结构,例如循环或列表的解构
测量执行传入函数所需的时间。对于函数,将在将控制权传递给函数之前评估参数。使用宏,您可以在秒表的开始和停止之间拼接代码。以下在宏和函数中具有完全相同的代码,并且输出非常不同。注意:这是一个人为的示例,选择实现是为了更好地突出差异。 (defmacro working-timer (b) (let ( (start (get-universal-time)) (result (eval b))) ;; 不在这里拼接以保持简单 ((- (get-universal-time) start) ))) (defun my-broken-timer (b) (let ( (start (get-universal-time)) (result (eval b))) ;; 甚至不需要 eval ((- (get-universal- time) start)))) (working-timer (sleep 10)) => 10 (broken-timer (sleep 10)) => 0
单线答案:
最小语法 => 宏优于表达式 => 简洁 => 抽象 => 强大
Lisp 宏只是以编程方式编写代码。也就是说,在扩展宏之后,你得到的只是没有宏的 Lisp 代码。因此,原则上,他们没有取得任何新成果。
但是,它们与其他编程语言中的宏的不同之处在于它们在表达式级别编写代码,而其他宏在字符串级别编写代码。由于括号,这是 lisp 独有的;或者更准确地说,他们的 minimal syntax 是可能的,这要归功于他们的括号。
正如该线程中的许多示例以及 Paul Graham 的 On Lisp 中所示,lisp 宏可以成为使您的代码更加简洁的工具。当简洁达到一定程度时,它提供了新的抽象级别,使代码更加简洁。再次回到第一点,原则上它们不提供任何新东西,但这就像说因为纸和铅笔(几乎)形成了图灵机,我们不需要实际的计算机。
如果有人知道一些数学,想想为什么函子和自然变换是有用的想法。原则上,他们不提供任何新东西。但是,通过将它们扩展到较低级别的数学,您会发现一些简单的想法(就范畴论而言)的组合可能需要 10 页才能写下来。你更倾向哪个?
我从 common lisp 食谱中得到了这个,我认为它解释了为什么 lisp 宏很有用。
“宏是一段普通的 Lisp 代码,它对另一段假定的 Lisp 代码进行操作,将其转换为(更接近)可执行 Lisp 的版本。这听起来可能有点复杂,所以让我们举一个简单的例子。假设你想要一个setq 的版本,它将两个变量设置为相同的值。所以如果你写
(setq2 x y (+ z 3))
当 z=8
x 和 y 都设置为 11 时。(我想不出这有什么用,但这只是一个例子。)
很明显,我们不能将 setq2 定义为函数。如果是 x=50
和 y=-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
x
和 y
通过引用传递,则可以将 setq2
定义为函数。但是,我不知道在 CL 中是否有可能。因此,对于特别不了解 Lisps 或 CL 的人来说,这不是 IMO 的说明性示例
在 python 中,你有装饰器,你基本上有一个函数,它接受另一个函数作为输入。你可以做任何你想做的事情:调用函数,做其他事情,将函数调用包装在资源获取释放中,等等,但你不能窥视该函数的内部。假设我们想让它更强大,假设你的装饰器将函数的代码作为列表接收,那么你不仅可以按原样执行函数,而且现在可以执行它的一部分,重新排序函数的行等。
(loop for x from 0 below 10 when (evenp x) collect x)
、more examples here。但实际上,循环是“只是一个宏”(我实际上是 re-implemented it from scratch some time ago)