阅读Paul Graham的文章关于编程语言,你会认为Lisp宏是唯一正确的选择。作为忙碌的开发者,在其他平台上工作,我没有使用Lisp宏的特权。作为一个想要理解这种功能强大之处的人,请解释一下是什么让这个特性如此强大。
请还将其与Python、Java、C#或C开发领域中的某些东西联系起来,以便我更好地理解。
阅读Paul Graham的文章关于编程语言,你会认为Lisp宏是唯一正确的选择。作为忙碌的开发者,在其他平台上工作,我没有使用Lisp宏的特权。作为一个想要理解这种功能强大之处的人,请解释一下是什么让这个特性如此强大。
请还将其与Python、Java、C#或C开发领域中的某些东西联系起来,以便我更好地理解。
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 )
;; 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
(列表推导式)的宏。其语法将与我们在示例中使用的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)
,更多示例在此处。但确实,loop只是一个宏(我曾经重新实现过它)。 - Suzanne Soy该文章的一个有趣子集:
在大多数编程语言中,语法是复杂的。宏必须拆解程序语法、分析它并重新组装。它们无法访问程序的解析器,因此必须依赖启发式和猜测。有时它们的简易分析是错误的,然后它们会出错。
但是Lisp不同。Lisp宏确实可以访问解析器,而且它是一个非常简单的解析器。Lisp宏得到的不是字符串,而是形式为列表的预解析源代码,因为Lisp程序的源不是字符串,而是列表。 Lisp程序非常擅长拆解和重新组合列表,每天都可靠地执行这项工作。
这里是一个扩展示例。Lisp有一个名为“setf”的宏,用于执行赋值操作。 setf 的最简单形式为
(setf x whatever)
这个代码用于将符号"x"的值设置为表达式"whatever"的值。
Lisp也有列表;你可以使用“car”和“cdr”函数分别获取列表的第一个元素或者剩余部分。
那么如果你想要用一个新值替换列表的第一个元素呢?有一个标准函数可以做到这一点,令人难以置信的是,它的名称甚至比“car”还糟糕。它就是“rplaca”。但你不必记住“rplaca”,因为你可以写
(setf (car somelist) whatever)
设置 somelist 的 car。
这里实际上使用了一个宏 "setf"。在编译时,它检查其参数,并发现第一个参数的形式为 (car SOMETHING)。它会自言自语地说:"哦,程序员想要设置 something 的 car。用于此操作的函数是 '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)
无论 expr1
是否为假,都会在返回答案之前评估所有三个 exprs
!
使用Lisp宏,您可以编写类似以下的代码:
(defmacro && (expr1 &rest exprs)
`(if ,expr1 ;` Warning: I have not tested
(&& ,@exprs) ; this and might be wrong!
nil))
现在您有一个 &&
,您可以像调用函数一样调用它,除非传递给它的所有表达式都为 true,否则它不会计算这些表达式。(&& (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%")
这还未涉及到 读取宏。
希望这有所帮助。
(and ...)
将评估表达式,直到其中一个表达式评估为 false,注意由 false 评估生成的副作用将发生,只有后续的表达式将被跳过。 - ocodo我觉得从这位作者的角度出发,没有比解释Lisp宏更好的了:http://www.defmacro.org/ramblings/lisp.html
由于现有答案提供了解释宏实现及其作用的良好具体示例,因此将一些关于为什么宏设施相对于其他语言是一个重大收益的想法汇集在一起可能会有所帮助;首先来自这些答案,然后是来自其他地方的一个伟大答案:
……在C中,您必须编写自定义预处理器[这可能符合足够复杂的C程序]……
与掌握C ++的任何人交谈,并问问他们花了多长时间学习他们需要进行模板元编程的所有模板调整 [这仍然不如强大]。
……在Java中,您必须使用字节码编织来进行黑客攻击,尽管像AspectJ这样的一些框架允许您使用不同的方法来执行此操作,但它从根本上讲仍然是一种黑客攻击。
DOLIST类似于Perl的foreach或Python的for。Java在Java 1.5中添加了一种类似的循环结构,称为“增强型”for循环,作为JSR-201的一部分。宏的作用是显而易见的。Lisp程序员注意到他们代码中的一个常见模式后,可以编写一个宏,以便在源级别抽象出该模式。Java程序员则需要说服Sun该特定抽象值得添加到语言中。然后Sun必须发布JSR并召集全行业的“专家组”来讨论所有内容。根据Sun的说法,这个过程平均需要18个月。之后,编译器编写者都必须升级他们的编译器以支持新功能。即使Java程序员喜欢的编译器支持Java的新版本,他们可能仍然不能使用新功能,直到允许与旧版本的Java不兼容。因此,Common Lisp程序员可以在五分钟内解决的问题困扰Java程序员多年。
想一想你可以在C或C++中使用宏和模板来完成什么。它们是管理重复代码的非常有用的工具,但它们在很多方面都有严格的限制。
Lisp和Lisp宏解决了这些问题。
与精通C++的任何人交谈,并问他们花费了多长时间学习所有需要进行模板元编程的模板技巧。或者像《现代C++设计》这样的(优秀)书籍中的所有疯狂技巧,即使语言已经标准化十年,它们在实践中仍然难以调试并且(实际上)在真实世界的编译器之间不可移植。如果您用于元编程的语言与您用于编程的语言相同,则所有这些问题都会消失!
Lisp宏以程序片段作为输入。该程序片段表示为可以任意操作和转换的数据结构。最终,宏输出另一个程序片段,并在运行时执行该片段。
C#没有宏机制,但等效的方法是,编译器将代码解析为CodeDOM树,并将其传递给一个方法,该方法将其转换为另一个CodeDOM,然后编译为IL。
可以使用此方法实现“语法糖”语法,例如for each
-语句、using
-子句、linq select
表达式等,这些都是将宏转换为基础代码。
如果Java有宏,您可以在Java中实现Linq语法,而无需更改基本语言。
以下是一种在C#中实现using
的Lisp风格宏的伪代码:
define macro "using":
using ($type $varname = $expression) $block
into:
$type $varname;
try {
$varname = $expression;
$block;
} finally {
$varname.Dispose();
}
我不确定能否给大家(优秀的)的帖子提供一些见解,但是...
Lisp宏之所以效果很好是因为Lisp语法的本质。
Lisp是一种非常规则的语言(认为一切都是一个列表);宏使您能够将数据和代码视为相同的内容(不需要对lisp表达式进行字符串分析或其他骚操作即可修改),您将这两个特性结合起来,就可以用非常简洁干净的方式修改代码。
编辑:我的意思是Lisp是同构的,这意味着Lisp程序的数据结构本身就是用Lisp编写的。
因此,使用该语言本身及其全部功能(例如,在Java中,您必须通过字节码编织来处理,尽管某些框架如AspectJ采用不同的方法,但它本质上仍然是一种hack),最终可以创建自己的代码生成器。
在实践中,使用宏,您最终会在Lisp之上构建自己的迷你语言,而无需学习其他语言或工具,并且可以使用该语言本身的全部功能。
S表达式
构成的,而不是列表。 - ribamar