阅读Paul Graham关于编程语言的论文,人们会认为Lisp宏是唯一可行的方法.作为一个忙于开发人员,在其他平台上工作,我没有使用Lisp宏的特权.作为想要了解嗡嗡声的人,请解释是什么让这个功能如此强大.
还请将此与我从Python,Java,C#或C开发世界中理解的内容联系起来.
简而言之,宏用于定义Common Lisp或Domain Specific Languages(DSL)的语言语法扩展.这些语言嵌入到现有的Lisp代码中.现在,DSL可以具有类似于Lisp的语法(如Peter Norvig的Prolog Interpreter for Common Lisp)或完全不同(例如用于Clojure的Infix符号数学).
这是一个更具体的例子:
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定义了几种特殊的语法形式.quote('
)表示下一个标记是文字.quasiquote或backtick(`
)表示下一个标记是带有转义的文字.转义由逗号运算符指示.文字'(1 2 3)
相当于Python的文字[1, 2, 3]
.您可以将其分配给另一个变量或在适当的位置使用它.您可以将其`(1 2 ,x)
视为Python的等价物,[1, 2, x]
其中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的最佳灵活性.
你会在这里找到关于lisp宏的全面辩论.
该文章的一个有趣的子集:
在大多数编程语言中,语法很复杂.宏必须拆分程序语法,分析它并重新组装它.他们无法访问程序的解析器,因此他们必须依赖于启发式和最佳猜测.有时他们的降价分析是错误的,然后他们就会破裂.
但是Lisp是不同的.Lisp的宏做访问解析器,这是一个非常简单的解析器. 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)
Common Lisp宏本质上扩展了代码的"语法原语".
例如,在C中,switch/case结构只适用于整数类型,如果你想将它用于浮点数或字符串,你可以使用嵌套的if语句和显式比较.你也无法编写C宏来为你完成这项工作.
但是,由于lisp宏(基本上)是一个lisp程序,它将代码片段作为输入并返回代码以替换宏的"调用",因此您可以根据需要扩展"原语"指令集,通常最终有一个更易读的程序.
要在C中执行相同的操作,您必须编写一个自定义预处理器,它会占用您的初始(非C)源并吐出C编译器可以理解的内容.这不是一个错误的方法,但它不一定是最容易的.
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宏.
希望这可以帮助.
我认为我从未见过Lisp宏解释得比这个人更好:http://www.defmacro.org/ramblings/lisp.html
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和C++的复杂,非常不规则的语法使得编写非常通用的宏变得困难.
Lisp和Lisp宏解决了这些问题.
Lisp宏是用Lisp编写的.你有Lisp的全部功能来编写宏.
Lisp有一个非常规则的语法.
与任何掌握了C++的人交谈,并询问他们花了多长时间学习模板元编程所需的所有模板软件.或者像(现代C++设计)这些(优秀)书籍中的所有疯狂技巧,即使语言已经标准化了十年,仍然难以调试和(在实践中)在现实编译器之间不可移植.如果你用于元编程的语言与你用于编程的语言相同,那么所有这些都会消失!
我不确定我是否可以为每个人的(优秀)帖子添加一些见解,但......
由于Lisp语法本质,Lisp宏工作得很好.
Lisp是一种非常规则的语言(想想一切都是一个列表); 宏使您可以将数据和代码视为相同(不需要字符串解析或其他黑客来修改lisp表达式).您结合这两个功能,并有一个非常干净的方法来修改代码.
编辑:我想说的是Lisp是homoiconic,这意味着lisp程序的数据结构是用lisp本身编写的.
因此,您最终会使用语言本身及其所有功能在语言之上创建自己的代码生成器(例如,在Java中,您必须通过字节码编织来破解您的方式,尽管像AspectJ这样的某些框架允许您使用不同的方法做到这一点,它从根本上说是一个黑客攻击).
在实践中,使用宏,您最终可以在lisp之上构建自己的迷你语言,而无需学习其他语言或工具,并且可以充分利用语言本身的强大功能.
既然现有的答案提供了很好的具体例子来解释宏实现了什么以及如何实现,也许它有助于汇集一些关于为什么宏观设施相对于其他语言获得显着收益的想法; 首先来自这些答案,然后是来自其他地方的伟大答案:
...在C中,你必须编写一个自定义的预处理器[这可能有资格作为一个 足够复杂的C程序 ] ......
- Vatine
与任何掌握了C++的人交谈,并询问他们花了多长时间学习模板元编程所需的模板所有模板[这仍然没有那么强大].
- 马特柯蒂斯
...在Java中你必须使用字节码编织来破解你的方式,尽管像AspectJ这样的一些框架允许你使用不同的方法来实现这一点,但它从根本上说是一个黑客攻击.
- 米格尔·平
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,"Practical Common Lisp"
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上循环,正确关闭它们.