是什么让Lispmacros如此特别?

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

请把这与我从Python,Java,C#或C开发世界中理解的东西联系起来。

为了给出简短的答案,macros用于定义Common Lisp或域特定语言(DSL)的语法扩展。 这些语言embedded到现有的Lisp代码中。 现在,DSL可以具有类似于Lisp的语法(如Peter Norvig的Common Lisp的Prolog解释器 )或完全不同的(例如,用于Clojure的Infix Notation Math )。

这是一个更具体的例子:
Python具有内置在语言中的列表parsing。 这给一个普通的情况一个简单的语法。 该线

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 ) 

这些在function上是等同的。 让我们调用我们暂停的怀疑,并假装Lisp有一个非常有限的循环macros,只是迭代,并没有简单的方法来做相当于列表parsing。

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

在我走之前,我应该更好地解释一下macros是什么。 这是通过代码对代码进行的转换。 也就是说,由解释器(或编译器)读取的一段代码将代码作为参数进行操作,然后返回结果,然后运行。

当然,这是很多打字和程序员很懒。 所以我们可以定义DSL来完成列表parsing。 事实上,我们已经在使用一个macros(循环macros)。

Lisp定义了一些特殊的语法forms。 quote(')表示下一个标记是文字。 quasiquote或反向(`)表示下一个标记是带有转义的文字。 逗号由逗号运算符表示。 文字'(1 2 3)相当于Python的[1,2,3]。 您可以将其分配给另一个variables或使用它。 你可以认为`(1 2,x)等价于Python的[1,2,x],其中x是先前定义的variables。 这个列表符号是进入macros的魔法的一部分。 第二部分是Lisp阅读器,智能地将macrosreplace为代码,但是最好在下面说明:

所以我们可以定义一个名为lcomp的macros(简称列表理解)。 它的语法与我们在例子中使用的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#的语法。 或.NET的LINQ语法。 最后,Lisp吸引人们 – 最终的灵活性。

你会在这里find关于lispmacros的全面的讨论。

该文章的一个有趣的子集:

在大多数编程语言中,语法是复杂的。 macros必须拆开程序语法,分析它,然后重新组装。 他们无法访问该程序的parsing器,所以他们不得不依赖启发式和最好的猜测。 有时他们的削减率分析是错误的,然后他们打破。

但Lisp是不同的。 Lispmacros可以访问parsing器,它是一个非常简单的parsing器。 Lispmacros不是一个string,而是一个列表forms的源代码,因为Lisp程序的源不是一个string; 这是一个列表。 Lisp程序非常擅长将列表拆分并重新组合。 他们每天都可靠地做到这一点。

这是一个扩展的例子。 Lisp有一个名为“setf”的macros来执行任务。 setf最简单的forms是

  (setf x whatever) 

它将符号“x”的值设置为expression式“whatever”的值。

Lisp也有列表; 您可以使用“car”和“cdr”函数分别获取列表的第一个元素或列表的其余部分。

现在,如果你想用新值replace列表的第一个元素呢? 这样做有一个标准的function,令人难以置信的是,它的名字比“汽车”还要糟糕。 这是“rplaca”。 但是你不必记得“rplaca”,因为你可以写

  (setf (car somelist) whatever) 

设置somelist的汽车。

这里真正发生的是“setf”是一个macros。 在编译时,它检查它的论点,并且它看到第一个有forms(汽车SOMETHING)。 它对自己说:“哦,程序员正试图设置一些东西的汽车,使用的function是'rplaca'。 它悄悄地重写代码到:

  (rplaca somelist whatever) 

Common Lispmacros实质上扩展了你的代码的“语法原语”。

例如,在C语言中,switch / case结构只能用于整型,如果你想用浮点型或者string,你可以使用嵌套if语句和显式比较。 也没有办法可以写一个Cmacros来为你做这个工作。

但是,由于lispmacros(本质上)是一个lisp程序,它以代码片段作为input,并返回代码来代替macros的“调用”,所以可以根据需要扩展“primitives”更可读的程序。

要在C中做同样的事,你必须编写一个自定义的预处理器来处理你的初始(not-quite-C)源代码,并且吐出一些C编译器可以理解的东西。 这不是一个错误的方式去做,但它不一定是最简单的。

Lispmacros允许你决定何时(如果有的话)任何部分或expression式将被评估。 举个简单的例子,想想C:

 expr1 && expr2 && expr3 ... 

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

现在尝试使这个&&成为一个function…这是正确的,你不能。 调用类似于:

 and(expr1, expr2, expr3) 

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

用lispmacros你可以编写如下代码:

 (defmacro && (expr1 &rest exprs) `(if ,expr1 ;` Warning: I have not tested (&& ,@exprs) ; this and might be wrong! nil)) 

现在你有一个&& ,你可以调用它就像一个函数,它不会评估你传递给它的任何forms,除非它们全都是真实的。

要看看这是如何有用,对比:

 (&& (very-cheap-operation) (very-expensive-operation) (operation-with-serious-side-effects)) 

和:

 and(very_cheap_operation(), very_expensive_operation(), operation_with_serious_side_effects()); 

你可以用macros做的其他事情是创build新的关键字和/或迷你语言(例如,查看(loop ...)macros),将其他语言集成到lisp中,例如,可以编写一个macros让你说一些像:

 (setvar *rows* (sql select count(*) from some-table where column1 = "Yes" and column2 like "some%string%") 

而这甚至没有进入Readermacros 。

希望这可以帮助。

我不认为我见过比这个家伙更好地解释Lispmacros: http : //www.defmacro.org/ramblings/lisp.html

lispmacros将程序片段作为input。 这个程序片段代表一个数据结构,可以任何你喜欢的方式进行操作和转换。 最后macros输出另一个程序片段,这个片段是在运行时执行的。

C#没有macros设施,但是如果编译器将代码parsing到CodeDOM树中,并将其传递给一个方法,该方法将其转换成另一个CodeDOM,然后编译为IL,那么该方法是等价的。

这可以用来实现“糖”的语法,如using -clause,linq select -expressions等for each语句作为转换为底层代码的macros。

如果Java有macros,则可以在Java中实现Linq语法,而不需要Sun来更改基本语言。

这里是伪代码如何在C#实施using Lisp风格的macros可以看:

 define macro "using": using ($type $varname = $expression) $block into: $type $varname; try { $varname = $expression; $block; } finally { $varname.Dispose(); } 

想想你可以在C或C ++中用macros和模板做什么。 它们是pipe理重复代码的非常有用的工具,但它们受到的限制非常严格。

  • 有限的macros/模板语法限制了它们的使用。 例如,你不能写一个扩展到某个类或函数之外的模板。 macros和模板不能轻易维护内部数据。
  • C和C ++的复杂非常不规则的语法使得编写非常macros的macros很困难。

Lisp和Lispmacros解决了这些问题。

  • Lispmacros是用Lisp编写的。 你有Lisp的全部力量来写macros。
  • Lisp有一个非常规律​​的语法。

与任何掌握C ++的人交谈,询问他们花了多长时间学习模板元编程所需的所有模板。 或者像“ 现代C ++devise” (优秀)书籍中的所有疯狂的技巧,即使语言已经被标准化了十年,仍然难以debugging,并且(实际上)在真实世界的编译器之间是不可移植的。 如果用于元编程的语言与编程使用的语言相同,所有这些都会融化掉!

我不知道我可以添加一些洞察力,每个人的(优秀)职位,但…

由于Lisp语法本质,Lispmacros很好用。

Lisp是一个非常规则的语言(想一切都是列表 ); macros使您可以将数据和代码视为相同(不需要stringparsing或其他修改lispexpression式的操作)。 您将这两个function结合起来,您就可以非常干净地修改代码。

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

所以,最终你可以用语言本身来创build你自己的代码生成器(例如在Java中你必须用字节码编织方式来破解你的方式,尽pipe一些像AspectJ这样的框架可以让你使用不同的方法来做到这一点,它从根本上是一个黑客)。

实际上,使用macros,您最终可以在lisp之上构build您自己的迷你语言 ,而无需学习其他语言或工具,并充分利用语言本身的力量。

Lispmacros代表几乎任何规模巨大的编程项目中都会出现的模式。 最终在一个大的程序中,你有一段代码,你意识到它会更简单,更不容易出错,你可以编写一个程序,输出源代码作为文本,然后你可以粘贴。

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

在Lisp中,整个过程已经被macros观系统所forms化。 当然,它可以让你创build语法的扩展,并做各种奇特的事情,但它的实际用处归结于上述。 当然,这有助于Lispmacros观系统允许您使用整个语言的全部function来操作这些“片段”。

既然现有的答案给出了很好的具体例子来解释macros是如何实现的以及如何实现的,也许这将有助于收集关于为什么macros观设施相对于其他语言显着增益的一些想法; 首先从这些答案,然后从别的伟大的一个:

…在C中,你将不得不写一个自定义的预处理器[这可能会被认为是一个足够复杂的C程序 ] …

– Vatine

与任何掌握C ++的人交谈,询问他们花了多长时间学习模板元编程所需的所有模板,但仍然不够强大。

– 马特柯蒂斯

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

– 米格尔·平

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

– Peter Seibel在“Practical Common Lisp”

总之,macros是代码的转换。 他们允许引入许多新的语法结构。 例如,考虑在C#中的LINQ。 在lisp中,有类似的语言扩展是由macros实现的(例如,内置循环结构,迭代)。 macros可以显着减less代码重复。 macros允许embedded«小语言»(例如,在C#/ java中,将使用xml进行configuration,在lisp中可以使用macros来实现同样的function)。 macros可能会隐藏使用库使用的困难。

例如,在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)) 

这隐藏了所有数据库的东西(事务,正确的连接closures,取数据等),而在C#中,这需要创buildSqlConnections,SqlCommands,将SqlParameters添加到SqlCommands,在SqlDataReaders上循环,正确closures它们。

虽然以上都解释了什么是macros,甚至有很酷的例子,但我认为macros和普通函数的关键区别在于,在调用函数之前,LISP先评估所有的参数。 使用macros是相反的,LISP将未评估的parameter passing给macros。 例如,如果将(+ 1 2)传递给某个函数,则该函数将接收到值3.如果将其传递给macros,它将收到一个List(+ 1 2)。 这可以用来做各种令人难以置信的有用的东西。

  • 添加一个新的控制结构,例如循环或解构列表
  • 测量执行传入函数所需的时间。使用函数时,参数将在控制传递给函数之前进行计算。 使用macros,您可以在秒表的开始和停止之间拼接代码。 下面在macros和函数中有完全相同的代码,输出是非常不同的。 注意:这是一个人为的例子,select的实现是相同的,以更好地突出差异。

     (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食谱中得到了这个,但是我认为它解释了为什么lispmacros是一个很好的方法。

“一个macros是一个普通的Lisp代码片,它运行在另一个可能的Lisp代码上,将它翻译成一个可执行的Lisp代码(听起来有些复杂),所以我们举一个简单的例子:假设你想要一个设置两个variables为相同值的setq版本,所以如果你写的话

 (setq2 xy (+ z 3)) 

z=8 ,x和y都被设置为11.(我想不出有什么用处,但这仅仅是一个例子)。

很明显,我们不能将setq2定义为一个函数。 如果x=50y=-5 ,则该函数将接收值50,-5和11; 它将不知道应该设置什么variables。 我们真正想说的是,当你(Lisp系统)看到(setq2 v1 v2 e) ,将它视为等同于(progn (setq v1 e) (setq v2 e)) 。 其实这不太对,但是现在就可以了。 一个macros允许我们通过指定一个用于将input模式(setq2 v1 v2 e) “转换成输出模式(progn ...) ”的程序来做到这一点。

如果你认为这很好,你可以继续阅读: http : //cl-cookbook.sourceforge.net/macros.html

在Python中你有装饰器,你基本上有一个function,另一个function作为input。 你可以做任何你想要的:调用函数,做其他的事情,将函数调用包装在资源获取版本中等,但是你不能在这个函数里面偷看。 假设我们想让它变得更加强大,比如你的装饰器收到了函数的代码作为列表,那么你不仅可以按原样执行函数,而且现在可以执行它的一部分,重新​​sorting函数的行数等等。

Interesting Posts