我想我理解Lisp宏及其在编译阶段的作用。
但是在Python中,你可以将一个函数传递给另一个函数
def f(filename, g):
try:
fh = open(filename, "rb")
g(fh)
finally:
close(fh)
所以,我们在这里得到懒惰的评价。我可以用宏做什么而不用函数作为第一类对象?
答案 0 :(得分:28)
首先,Lisp也有一流的功能,所以你也可以问:“如果我已经拥有一流的功能,为什么我需要在Lisp中使用宏”。答案是,一流的功能不允许你使用语法。
在化妆品级别,一流的功能允许您编写f(filename, some_function)
或f(filename, lambda fh: fh.whatever(x))
,但不能编写f(filename, fh, fh.whatever(x))
。虽然可以说这是一件好事,因为在最后一种情况下,fh
突然来自哪里不太清楚。
更重要的是,函数只能包含有效的代码。因此,您不能编写一个高阶函数reverse_function
,它将函数作为参数并“反向”执行,以便reverse_function(lambda: "hello world" print)
执行print "hello world"
。使用宏,您可以执行此操作。当然,这个特定的例子非常愚蠢,但是当嵌入特定于域的语言时,这种能力非常有用。
例如,您无法在python中实现常见的lisp loop
构造。地狱,你甚至不能在python中实现python的for ... in
构造,如果它不是真正的内置 - 至少不是那种语法。当然你可以实现像for(collection, function)
这样的东西,但那不是很漂亮。
答案 1 :(得分:12)
这是Matthias Felleisen从2002年开始的答案(通过http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg01539.html):
我想建议有 宏的三个纪律用途:
数据子语言:我可以编写简单的表达式并创建 复杂的嵌套列表/数组/表 引用,unquote等整齐地穿着 用宏。
绑定构造:我可以用宏引入新的绑定构造。 这有助于我摆脱lambda和 将东西放在一起 属于一起。例如,一个 我们的教学包含一个表格
(网络查询([姓氏 (string-append“Hello”first-name“你最后的名字是什么? 名称?”]) ......姓氏......名字......)a之间有明显的互动 程序和网络消费者暗示 [注意:在ML你可以写 web-query(fn last-name => ...)string_append(...)但是由golly 这是痛苦而且是不必要的 图案。]- 醇>
评估重新排序:我可以介绍一下这些结构 推迟/推迟评估 根据需要表达。想想循环, 新条件,延迟/强制等。
[注意:在Haskell中,你不需要那个 一个。]我理解Lispers使用宏 出于其他原因。老实说,我 相信这部分是由于 编译器缺陷,部分原因 对“语义”的不规范性 目标语言。
我挑战人们解决所有问题 当他们说X语言时有三个问题 可以做宏可以做什么。
- 马蒂亚斯
Felleisen是该领域最具影响力的宏观研究人员之一。 (不过我不知道他是否会同意这个消息。)
更多阅读:Paul Graham的On Lisp(http://www.paulgraham.com/onlisp.html; Graham 绝对不同意Felleisen这些是宏的唯一有用用途),而Shriram Krishnamurthi的论文“Automata via宏“(http://www.cs.brown.edu/~sk/Publications/Papers/Published/sk-automata-macros/)。
答案 2 :(得分:8)
宏在编译时扩展。闭包是在运行时构造的。使用宏,您可以实现嵌入式域特定语言的高效编译器,并且通过高阶函数,您只能实现低效的解释器。 eDSL编译器可以进行各种静态检查,执行您想要实现的任何昂贵的优化,但是当您只有运行时,您就无法做任何昂贵的事情。
不用说,宏为您的eDSL和语言扩展提供了更灵活的语法(字面意思,任何语法)。
有关详细信息,请参阅此问题的答案:Collection of Great Applications and Programs using Macros
答案 3 :(得分:8)
宏执行代码转换
宏转换源代码。懒惰的评价没有。想象一下,您现在可以编写将任意代码转换为任意不同代码的函数。
非常简单的代码转换
简单语言结构的创建也只是一个非常简单的例子。考虑打开文件的示例:
(with-open-file (stream file :direction :input)
(do-something stream))
VS
(call-with-stream (function do-something)
file
:direction :input)
宏给我的是一种略有不同的语法和代码结构。
嵌入式语言:高级迭代构造
接下来考虑一个稍微不同的例子:
(loop for i from 10 below 20 collect (sqr i))
VS
(collect-for 10 20 (function sqr))
我们可以定义一个函数COLLECT-FOR
,它对一个简单的循环执行相同的操作,并且包含start,end和step函数的变量。
但LOOP
提供了一种新语言。 LOOP
宏是此语言的编译器。此编译器可以执行LOOP
特定的优化,还可以在编译时检查此新语言的语法。更强大的循环宏是ITERATE
。现在,语言级别的这些强大工具可以编写为库,而无需任何特殊的编译器支持。
在宏中移动代码树并进行更改
接下来是另一个简单的例子:
(with-slots (age name) some-person
(print name)
(princ " "
(princ age))
VS。类似的东西:
(flet ((age (person) (slot-value person 'age))
(name (person) (slot-value person 'name)))
(print (name))
(princ " ")
(princ (age)))
WITH-SLOTS
宏导致封闭源树的完整遍历,并通过调用(SLOT-VALUE SOME-PERSON 'name)
替换变量名:
(progn
(print (slot-value some-person 'name))
(princ " "
(princ (slot-value some-person 'age)))
在这种情况下,宏可以重写代码的选定部分。它理解Lisp语言的结构,并且知道名称和年龄是变量。它还了解在某些情况下name
和age
可能不是变量,不应该重写。这是一个所谓的 Code Walker 的应用程序,这是一个可以遍历代码树并对代码树进行更改的工具。
宏可以修改编译时环境
另一个简单的例子,一个小文件的内容:
(defmacro oneplus (x)
(print (list 'expanding 'oneplus 'with x))
`(1+ ,x))
(defun example (a b)
(+ (oneplus a) (oneplus (* a b))))
在此示例中,我们对宏ONEPLUS
不感兴趣,但对宏DEFMACRO
本身不感兴趣。
有趣的是什么?在Lisp中,您可以拥有一个包含上述内容的文件,并使用文件编译器来编译该文件。
;;; Compiling file /private/tmp/test.lisp ...
;;; Safety = 3, Speed = 1, Space = 1, Float = 1, Interruptible = 1
;;; Compilation speed = 1, Debug = 2, Fixnum safety = 3
;;; Source level debugging is on
;;; Source file recording is on
;;; Cross referencing is on
; (TOP-LEVEL-FORM 0)
; ONEPLUS
(EXPANDING ONEPLUS SOURCE A)
(EXPANDING ONEPLUS SOURCE (* A B))
; EXAMPLE
;; Processing Cross Reference Information
所以我们看到,文件编译器扩展了ONEPLUS
宏的使用。
有什么特别之处?文件中有一个宏定义,在下一个表单中我们已经使用了新的宏ONEPLUS
。我们从未将宏定义加载到Lisp中。不知何故,编译器知道并注册定义的宏ONEPLUS
,然后才能使用它。
因此宏DEFMACRO
在编译时环境中注册新定义的宏ONEPLUS
,以便编译器知道这个宏 - 而无需加载代码。然后宏可以在宏扩展期间在编译时执行。
使用功能我们不能这样做。编译器为函数调用创建代码,但不运行它们。但是宏可以在编译时运行并向编译器添加“知识”。然后,这些知识在编译器运行期间有效,稍后会被遗忘。 DEFMACRO
是一个在编译时执行的宏,然后通知编译时环境新的宏。
另请注意,宏ONEPLUS
也会运行两次,因为它在文件中使用了两次。副作用是它打印出一些东西。但是ONEPLUS
也可能有其他任意的副作用。例如,它可以针对规则库检查封闭的源,并提醒您是否附带的代码违反了某些规则(想想样式检查器)。
这意味着,一个宏(这里是DEFMACRO
)可以在编译文件时更改语言及其环境。在其他语言中,编译器可能会提供特殊的编译器指令,这些指令将在编译期间被识别。此类定义宏有很多影响编译器的示例:DEFUN
,DEFCLASS
,DEFMETHOD
,...
宏可以缩短用户代码
一个典型的例子是用于定义记录类数据结构的DEFSTRUCT
宏。
(defstruct person name age salary)
以上defstruct
宏为
person
person
make-person
函数另外它可能:
定义结构的原始代码是一条短线。扩展代码要长得多。
DEFSTRUCT
宏不需要访问语言的元级别来创建这些不同的东西。它只是使用典型的语言结构将一小段描述性代码转换为通常较长的定义代码。
答案 4 :(得分:4)
这是一个具体的建议,而不是高级答案:阅读Shriram的The Swine Before Perl。我展示了如何开发一个正在做几个不同事情的宏 - 一个特定的控制流,一个绑定和一个数据语言。 (另外,你会看到如何真正做到这一点。)
答案 5 :(得分:2)
宏在使用相同形式的数据和代码的语言中最有用,因为宏将代码视为数据并生成新代码。宏扩展是即时代码生成,在评估开始之前的编译阶段执行。这使得设计DSLs非常容易。
在Python中,您无法获取某些代码,将其传递给生成新代码的函数,以便执行新代码。为了在Python中实现像宏一样的东西,你必须从Python代码生成AST,修改AST并评估修改后的AST。
这意味着您无法在Python中编写if
语句。您只能使用现有的,但不能修改它或编写自己的语句。但Lisp宏允许您编写自己的语句。例如,您可以编写fi
语句,其行为类似于if
,但将 else 部分作为其第一个参数,将然后部分作为第二个参数
以下文章更详细地描述了宏和过程之间的区别: ftp://ftp.cs.utexas.edu/pub/garbage/cs345/schintro-v13/schintro_130.html
答案 6 :(得分:1)
首先,宏可以执行该功能不能执行的操作是特殊运算符可以执行的所有操作。宏调用可以扩展为特殊运算符,而函数调用则不能。例如,如果我们有一个词法变量x
,那么(mac x)
可以合理地更新其值。但是(fun x)
不能。
宏被扩展从而消失,之后只有函数调用和特殊运算符。因此,宏不但可以在没有宏的情况下完成任何操作,而且每次都可以在没有宏的情况下完成。但这就像说您可以用高级语言进行的所有操作都可以在汇编器中进行编码一样。从纯理论上来说,是的(我是说,看,编译器实际上是在对所有汇编器进行编码,并提供即时的存在证明),但这并不实际。
答案 7 :(得分:0)
在除lisp之外的示例中,例如elixir,if
控制流语句实际上是一个宏。 if
作为函数实现。但为了获得更清晰,更难忘的语法,它也被实现为宏。
if true do 1+2 end
if true, do: ( 1+2 )
if(true, do: 1+2)
if(true, [do: (1+2)])
if(true, [{:do, 1+2}])
以上所有内容都是等效的。但第一行是if的宏实现,可能会扩展到下面的if函数。
通过创建一个函数并且可以作为宏访问,为您提供了将if
控制流放在另一个函数的参数中的这种很酷的能力,同时保持对其他语言的熟悉。
is_number(if true do 1+2 end)
is_number(if true, do: (1+2))
所以我认为宏可以让你更好地控制语法,从而允许你创建标准函数不能的DSL。