为什么Common Lisp中存在函数/宏二分法?
允许同一名称同时代表一个宏(在编译/评估时在函数位置找到优先级)和一个函数(例如用mapcar
可用)时会出现什么逻辑问题?
例如,将second
定义为宏和函数将允许使用
(setf (second x) 42)
和
(mapcar #'second L)
无需创建任何setf
诡计。
当然很明显,宏可以做的不仅仅是函数,所以类比不能完整(我当然不认为每个宏也是一个函数),但为什么禁止它呢?通过在可能有用的时候共享一个命名空间?
我希望我不会冒犯任何人,但我真的没有找到“为什么这样做?”回应真的很相关...我在寻找为什么这是一个坏主意。施加任意限制,因为没有很好的用途是IMO有点傲慢(有点假设完美的远见)。
或者允许它存在实际问题吗?
答案 0 :(得分:13)
宏和函数是两个非常不同的东西:
宏正在使用源(!!!)代码并生成新的源代码(!!!)
函数是参数化代码块。
现在我们可以从几个角度来看这个问题,例如:
a)我们如何设计一种语言,其中功能和宏可以清晰识别,并且在我们的源代码中看起来不同,所以我们(人类)可以很容易地看到什么是什么?
或
b)我们如何以最有用的方式混合宏和函数,并拥有控制其行为的最有用的规则?对于用户来说,使用宏或函数应该没有区别。
我们真的需要说服自己b)是要走的路,我们想要使用一种语言,其中宏和函数的使用看起来是一样的,并且按照类似的原则工作。乘船和汽车。他们看起来不一样,他们的用例大多不同,他们运送人 - 我们现在应该确保他们的交通规则大部分是相同的,我们应该使它们不同,还是我们应该为他们的特殊用途设计规则?
对于函数,我们遇到的问题包括:定义函数,函数范围,函数的生命周期,传递函数,返回函数,调用函数,函数的阴影,函数的扩展,删除函数的定义,编译和功能解释,......
如果我们要让宏看起来与函数大致相似,我们需要解决大部分或全部上述问题。
在您的示例中,您提到了一个SETF表单。 SETF是一个宏,它在宏扩展时分析封闭的表单并为setter生成代码。这与SECOND
是否为宏无关。在这种情况下,将SECOND
作为宏将无济于事。
那么,问题的例子是什么?
(defmacro foo (a b)
(if (and (numberp b) (zerop b))
a
`(- ,a ,b)))
(defun bar (x list)
(mapcar #'foo (list x x x x) '(1 2 3 4)))
那该怎么办?直观地看起来很容易:在列表上映射FOO
。但事实并非如此。当Common Lisp被设计出来时,我猜,不清楚应该做什么以及它应该如何工作。如果FOO
是一个函数,那么很明显:Common Lisp将来自Scheme的思想置于词法范围的第一类函数之后并将其集成到语言中。
但是一流的宏?在Common Lisp的设计之后,一系列研究进入了这个问题并进行了调查。但是在Common Lisp的设计时,没有广泛使用的一流宏,也没有设计方法的经验。 Common Lisp正在标准化当时已知的内容以及用户认为开发所需的语言(对象系统CLOS是一种新颖的,基于早期使用类似对象系统的经验)的软件。 Common Lisp并不是为了拥有理论上最令人愉悦的Lisp方言而设计的 - 它被设计成具有强大的Lisp,可以高效地实现软件。
我们可以解决这个问题并说,传递宏是不可能的。开发人员必须提供一个名称相同的函数,我们会传递它。
但是(funcall #'foo 1 2)
和(foo 1 2)
会调用不同的机器吗?在第一种情况下,函数foo
,在第二种情况下,我们使用宏foo
为我们生成代码?真?我们(作为人类程序员)是否想要这个?我认为不是 - 看起来它使编程变得更加复杂。
从实用的角度来看:宏及其背后的机制已经足够复杂,以至于大多数程序员在实际代码中处理它时遇到了困难。它们使得调试和代码理解对于人类来说更加困难。从表面上看,宏使代码更容易阅读,但价格是需要理解代码扩展过程和结果。 寻找进一步将宏集成到语言设计中的方法并非易事。
readscheme.org对宏相关研究提出了一些建议。方案:Macros
Common Lisp
怎么样? Common Lisp提供的函数可以是第一类(存储,传递,...)和词法范围命名(DEFUN
,FLET
,LABELS
,{{ 1}},FUNCTION
)。
Common Lisp提供全局宏(LAMBDA
)和本地宏(DEFMACRO
)。
Common Lisp提供全局compiler macros(DEFINE-COMPILER-MACRO)。
使用编译器宏,可以为符号和编译器宏提供函数或宏。 Lisp系统可以决定在宏或函数上优先使用编译器宏。它也可以完全忽略它们。此机制主要用于用户编程特定的优化。因此,它没有解决任何与宏相关的问题,但提供了一种编程全局优化的实用方法。
答案 1 :(得分:10)
我认为Common Lisp的两个名称空间(函数和值),而不是三个(宏,函数和值),是一种历史偶然性。
Early Lisps(在20世纪60年代)以不同的方式表示函数和值:值作为运行时堆栈上的绑定,以及作为附加到符号表中的符号的属性的函数。当Common Lisp在20世纪80年代被标准化时,这种实现的差异导致了两个命名空间的规范。有关此决定的解释,请参阅Richard Gabriel的论文Technical Issues of Separation in Function Cells and Value Cells。
宏(及其祖先,FEXPRs,不评估其参数的函数)以与函数相同的方式存储在符号表中的许多Lisp实现中。如果已经指定了第三个命名空间(用于宏),那么这些实现会很不方便,并且会导致许多程序出现向后兼容性问题。
有关FEXPR,宏和其他特殊形式的历史,请参阅Kent Pitman的论文Special Forms in Lisp。
(注意:Kent Pitman的网站不适合我,因此我通过archive.org链接到论文。)
答案 2 :(得分:2)
因为完全相同的名称将代表两个不同的对象,具体取决于上下文。它使程序不必要地难以理解。
答案 3 :(得分:1)
我的TXR Lisp方言允许符号同时成为宏和函数。而且,某些特殊运算符还具有功能支持。
我对设计进行了一些思考,但没有遇到任何问题。它效果很好,从概念上讲也很干净。
由于历史原因,常见的Lisp就是这样。
这里是系统的简要概述:
为带有X
的符号defmacro
定义了全局宏时,符号X
不会变为fboundp
。相反,变成fboundp
的是复合函数名称(macro X)
。
然后(macro X)
,symbol-function
和其他情况下都知道名称trace
。 (symbol-function '(macro X))
检索采用形式和环境的两参数扩展器功能。
可以使用(defun (macro X) (form env) ...)
编写宏。
没有编译器宏;常规宏完成了编译器宏的工作。
常规宏可以返回未扩展形式,以表示它正在拒绝扩展。如果词汇macrolet
拒绝扩展,则机会将转到词汇更外部的macrolet
,依此类推,直到全局defmacro
。如果全局defmacro
拒绝扩展,则该形式被视为扩展,因此必然是函数调用或特殊形式。
如果我们同时拥有一个称为X
的函数和宏,则可以使用(call (fun X) ...)
或(call 'X ...)
调用函数定义,或者使用Lisp-1-style { {1}}评估程序dwim
几乎总是通过其(dwim X ...)
语法糖作为[]
使用。
出于某种完整性,提供了功能[X ...]
,mboundp
和mmakunbound
,它们是symbol-macro
,fboundp
的宏类似物和fmakunbound
。
特殊运算符symbol-function
,or
,and
和其他一些运算符也具有函数定义,因此可以使用类似if
的代码。
示例:对[mapcar or '(nil 2 t) '(1 0 3)] -> (1 2 t)
进行固定折叠:
sqrt
但是,没有通过1> (sqrt 4.0)
2.0
2> (defmacro sqrt (x :env e :form f)
(if (constantp x e)
(sqrt x)
f))
** warning: (expr-2:1) defmacro: defining sqrt, which is also a built-in defun
sqrt
3> (sqrt 4.0)
2.0
4> (macroexpand '(sqrt 4.0))
2.0
5> (macroexpand '(sqrt x))
(sqrt x)
的宏定义来实现(set (second x) 42)
。那不会很好。主要原因是这将是太多负担。对于给定的功能,程序员可能希望拥有一个与实现分配语义无关的宏定义!
此外,如果second
实现了场所语义,那么当它不嵌入在赋值操作中时会发生什么,从而根本不需要语义?基本上,要满足所有要求,就需要制定一种方案来编写宏,该宏的复杂性将等于或超过用于处理位置的现有逻辑。
实际上,TXR Lisp具有一种称为“位置宏”的特殊宏。表单仅用作场所时,才被识别为场所宏调用。但是,位置宏本身并不实现位置语义。他们只是简单地重写。位置宏必须向下扩展为可识别为位置的形式。
示例:指定(second x)
在用作场所时的行为类似于(foo x)
:
(car x)
如果1> (define-place-macro foo (x) ^(car ,x))
foo
2> (macroexpand '(foo a)) ;; not a macro!
(foo a)
3> (macroexpand '(set (foo a) 42)) ;; just a place macro
(sys:rplaca a 42)
扩展到某个地方以外的地方,则失败:
foo