阅读Paul Graham关于编程语言的文章,人们会认为Lisp宏是唯一的选择。作为繁忙的开发人员,在其他平台上工作,我没有使用Lisp宏的特权。作为想了解蜂鸣声的人,请解释一下此功能如此强大的原因。
还请将此与我从Python,Java,C#或C开发的世界中了解的东西联系起来。
阅读Paul Graham关于编程语言的文章,人们会认为Lisp宏是唯一的选择。作为繁忙的开发人员,在其他平台上工作,我没有使用Lisp宏的特权。作为想了解蜂鸣声的人,请解释一下此功能如此强大的原因。
还请将此与我从Python,Java,C#或C开发的世界中了解的东西联系起来。
Answers:
为了给出简短的答案,宏用于定义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定义了几种特殊的语法形式。引号('
)表示下一个标记是文字。准引号或反引号(`
)表示下一个标记是带有转义符的文字。转义由逗号运算符指示。字面'(1 2 3)
量等同于Python的[1, 2, 3]
。您可以将其分配给另一个变量或就地使用它。您可以认为`(1 2 ,x)
它等同于Python[1, 2, x]
where 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的原因-最终的灵活性。
(loop for x from 0 below 10 when (evenp x) collect x)
,这里有更多示例。但是确实,循环是“只是一个宏”(我实际上是从前从头开始重新实现的)
该文章的一个有趣的子集:
在大多数编程语言中,语法很复杂。宏必须分解程序语法,对其进行分析并重新组装。他们没有访问该程序的解析器的权限,因此必须依靠启发式方法和最佳猜测。有时,他们的降价分析是错误的,然后它们崩溃了。
但是Lisp是不同的。Lisp宏确实可以访问解析器,这是一个非常简单的解析器。 Lisp宏不是一个字符串,而是一个准备好的源代码列表,因为Lisp程序的源不是字符串;这是一个列表。Lisp程序确实擅长分解列表并将它们放在一起。他们每天都可靠地做到这一点。
这是一个扩展的示例。Lisp有一个称为“ setf”的宏,可以执行分配。setf最简单的形式是
(setf x whatever)
将符号“ x”的值设置为表达式“ whatever”的值。
Lisp也有清单。您可以使用“ car”和“ cdr”函数分别获取列表的第一个元素或列表的其余元素。
现在,如果要用新值替换列表的第一个元素怎么办?有一个标准功能可以做到这一点,而且令人难以置信的是,它的名称甚至比“ car”还差。是“ rplaca”。但是您不必记住“ rplaca”,因为您可以编写
(setf (car somelist) whatever)
设定某人的汽车。
这里真正发生的是“ setf”是一个宏。在编译时,它检查其参数,并发现第一个参数具有形式(car SOMETHING)。它自言自语:“哦,程序员正在尝试设置东西。要使用的功能是'rplaca'。” 并且它悄悄地将代码重写为:
(rplaca somelist whatever)
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宏。
希望这可以帮助。
(and ...
将对表达式进行求值,直到一个结果为假,注意由假评估产生的副作用将发生,只有随后的表达式将被跳过。
我认为我从未见过比这个人对Lisp宏的解释更好的方法:http : //www.defmacro.org/ramblings/lisp.html
想想您可以使用宏和模板在C或C ++中做什么。它们是用于管理重复代码的非常有用的工具,但是它们在相当严格的方面受到限制。
Lisp和Lisp宏解决了这些问题。
与精通C ++的任何人交谈,并询问他们花了多长时间学习模板元编程所需的所有模板知识。或(出色)书中的所有疯狂技巧,例如Modern C ++ Design,尽管该语言已经标准化了十年,但它们仍然很难调试,并且(在实践中)难以在现实世界的编译器之间移植。如果用于元编程的语言与用于编程的语言相同,那么所有这些都将消失!
lisp宏将程序片段作为输入。该程序片段表示一个数据结构,您可以按照自己喜欢的任何方式对其进行操作和转换。最后,宏输出另一个程序片段,而该片段就是在运行时执行的片段。
C#没有宏功能,但是如果编译器将代码解析为CodeDOM树并将其传递给方法,该方法会将其转换为另一个CodeDOM,然后再编译为IL,则等效。
这可以用来实现“糖”语法,例如for each
-statement -clause using
,linq -expressions select
等,作为转换为基础代码的宏。
如果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();
}
由于现有的答案给出了很好的具体示例,解释了宏的实现方式以及宏的实现方式,因此,也许有必要将一些有关为什么宏工具相对于其他语言而言具有重要意义的想法收集在一起。首先是这些答案,然后是其他地方的出色答案:
...在C语言中,您将必须编写一个自定义的预处理程序[它可能符合 足够复杂的C程序的要求 ] ...
— 瓦汀
与精通C ++的任何人交谈,并询问他们花了多长时间学习模板元编程所需的所有模板功能[仍然没有那么强大]。
...在Java中,您必须通过字节码编织来破解自己的方式,尽管诸如AspectJ之类的某些框架允许您使用不同的方法来做到这一点,但从根本上讲,这是黑客。
- 米格尔·平
DOLIST与Perl的foreach或Python的相似。Java作为JSR-201的一部分,在Java 1.5中用“增强”的for循环添加了类似的循环构造。注意宏的作用。注意到代码中有通用模式的Lisp程序员可以编写宏以为自己提供该模式的源代码级抽象。注意到相同模式的Java程序员必须说服Sun,这种特殊的抽象值得添加到该语言中。然后,Sun必须发布JSR并召集一个行业范围内的“专家组”,以将所有事情都排除在外。根据Sun的说法,该过程平均需要18个月。之后,编译器编写者都必须升级其编译器以支持新功能。即使Java程序员最喜欢的编译器支持新版本的Java,他们可能“仍然”不能使用新功能,直到被允许破坏与旧版本Java的源兼容性。令人讨厌的是,Common Lisp程序员可以在五分钟内自行解决,困扰了Java程序员多年。
我不确定我可以在每个人的(出色)帖子中添加一些见解,但是...
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中,宏可以实现相同的功能)。宏可能隐藏使用库用法的困难。
例如,您可以轻而易举地写下
(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))) ;; 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
我从《常见的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
setq2
如果x
和y
通过引用传递,则可以将其定义为一个函数。我不知道在CL中是否可行。因此,对于不特别了解Lisps或CL的人来说,这不是IMO的说明性示例