为什么要区分函数和宏?

14
为什么Common Lisp中存在函数/宏的二分法?
如果允许同一个名称同时代表宏(在编译/求值时优先)和函数(例如可与mapcar一起使用),会有什么逻辑问题?
例如,将second定义为宏和函数都可以使用。
(setf (second x) 42)

(mapcar #'second L)

不需要创建任何setf巧妙方法。

当然,明显的是宏可以做更多的事情而函数不能,所以这个比喻不完整(我并不认为每个宏都应该是一个函数),但为什么要通过使两者共享一个命名空间来禁止它,当它可能是有用的呢?

我希望我没有冒犯任何人,但我并不认为“为什么要这样做?”的回答真的相关...我想知道的是为什么这是一个坏主意。因为没有找到好的用途就强加限制,在我看来有点傲慢(有种假定完美的远见)。

或者允许这样做存在实际问题吗?


如果你想两全其美的话,可以使用编译器宏。寻找define-compiler-macro。 - Dirk
你能给出一个使用场景,说明这会有什么用处吗? - Karoly Horvath
@Dirk。是的...这也是一个有趣的问题,如果你有宏,为什么还需要编译器宏?普通的宏(可能在调试期间每次都被扩展的选项)似乎已经足够了。 - 6502
@gareth:确实,该文档中有一段非常相关于我的问题的内容...“DEFMACRO不能用于此目的,因为它需要使用符号函数单元格,这将防止在编译环境中激活功能定义。”。基本上说,您可能需要编译器宏来提高效率,因为宏不能同时也是函数(这就是我质疑的点)。 - 6502
3
了解标准化过程的限制是有帮助的。如果Lisp供应商不同意实施标准,那么制定一个优雅的标准就没有任何好处。因此,为了制定一个被广泛采用的成功标准,需要做出许多妥协。 - Gareth Rees
显示剩余2条评论
4个回答

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提供了与Scheme相关的宏研究方面的一些指针:Macros

那么Common Lisp呢

Common Lisp提供了可以被存储、传递等的一级函数,并为它们提供词法范围的命名(DEFUNFLETLABELSFUNCTIONLAMBDA)。

Common Lisp提供了全局宏(DEFMACRO)和局部宏(MACROLET)。

Common Lisp提供了全局编译器宏DEFINE-COMPILER-MACRO)。

使用编译器宏,可以为符号提供函数或宏,还可以提供编译器宏。 Lisp系统可以决定优先考虑编译器宏而不是宏或函数。 它也可以完全忽略它们。 这个机制通


只是澄清一下关于第二个“second”的问题。如果second被定义为像(defmacro second (x) (list 'cadr x))这样的宏,那么没有必要使用(defun (setf second) ...)来使其工作,因为setf已经展开了宏,但是在使用mapcar时就不可能使用(mapcar #'second ...)这样的方式,而需要使用类似(mapcar (lambda (x) (second x)) ...)这样的技巧。关于(funcall #'foo 1 2)(foo 1 2)不同的问题,这意味着如果想让它们不同,它们就可以不同。这其中有什么坏处吗? - 6502
@6502:但这在一般情况下并没有什么帮助。现在,您需要通过宏定义一个位置(!),而不是使用SETF机制进行注册。为什么宏扩展要成为一个位置?它可能是,也可能不是。您现在需要记录哪些是扩展为位置的内容,哪些不是。如果我想让这个表单成为一个位置,并且有一个不同的宏扩展呢? - Rainer Joswig
抱歉,我无法理解为什么在函数和宏具有相同名称的情况下可能性会使语言更加复杂。如果您感到困惑,只需不使用它即可。关于second由于是宏而自动设置f-able又只是一种可能性...没有任何东西会阻止您为其定义自定义setf处理程序(在CL中,setf CAN扩展宏但“仅在耗尽除扩展为调用名为(setf reader)的函数之外的所有其他可能性之后”)。 - 6502
@6502:那些只是“可能性”的功能也是无用的。已经很难确定一个表单的作用了,现在你提出要增加更多的复杂性,而没有一个好的想法如何将其整合到语言中。这不仅仅是我的问题,还有可能使用它的库的人。 - Rainer Joswig
简单回顾一下,您认为这是因为宏和函数具有相同的命名空间(不像变量、结构体、标签、编译器宏等,每个都有自己独立的命名空间),所以才会出现这种情况吗?您能指出一份描述这种讨论的文档吗?在那之前,我认为解释为什么存在函数/宏二分法的最好理由是历史原因。我已经查看了您提供的文档,但没有找到相关实例(大多数都是关于强制卫生习惯的)。 - 6502
显示剩余3条评论

10
我认为Common Lisp的两个命名空间(函数和值),而不是三个(宏,函数和值),是历史偶然性。
早期的Lisp(在1960年代)以不同的方式表示函数和值:将值作为运行时堆栈上的绑定,将函数作为附加到符号表中的符号属性。这种实现上的差异导致了当Common Lisp在1980年代被标准化时规定了两个命名空间。请参见Richard Gabriel的论文Technical Issues of Separation in Function Cells and Value Cells,以解释这个决定。
许多Lisp实现将宏(及其祖先FEXPRs,不评估其参数的函数)存储在符号表中,方式与函数相同。如果指定了第三个命名空间(用于宏),这将对这些实现造成不便,并且会对许多程序造成向后兼容性问题。
有关FEXPRs、宏和其他特殊形式的历史,请参见Kent Pitman的论文Special Forms in Lisp
(注:我的Kent Pitman网站无法使用,因此我通过archive.org链接到了这些论文。)

谢谢提供的链接(对我很有用)。我以前从未听说过这些宏前体......实际上,我一直想知道为什么需要“特殊”运算符以及它们为什么被称为必需品。 - 6502

2

因为在不同的上下文中,相同的名称会代表两个不同的对象,这会使程序难以理解。这种情况会让程序变得不必要地复杂。


这似乎不是整个故事,因为你的论点似乎适用于函数和值,而这两个东西确实有不同的命名空间。 - Gareth Rees
通过宏,您可以引入新的语法(您可以控制哪些参数必须被评估,哪些不必)。而函数则由语言决定其行为。 - Dirk
我并不是说每个宏都应该同时也是一个函数,但我想知道为什么我不能拥有同名的宏和函数(可以使用funcall调用)。 - 6502
在Common Lisp中,符号list可以是函数中的变量/参数,该函数还调用(list ...),因此(list list)是有意义的。通过语法/上下文消除歧义的多个命名空间的反对意见早已过时。 - Kaz

1
我的TXR Lisp方言允许一个符号同时是宏和函数。此外,某些特殊操作符也由函数支持。
我对设计进行了一些思考,并且没有遇到任何问题。它运行非常好,概念上也很清晰。
Common Lisp之所以是现在这个样子,是出于历史原因。
以下是该系统的简要介绍:
  • 当使用defmacro为符号X定义全局宏时,符号X不会变成fboundp。相反,复合函数名(macro X)会变成fboundp

  • 然后,名称(macro X)symbol-functiontrace和其他情况所知道。(symbol-function '(macro X))检索出两个参数的扩展器函数,其中一个是形式,另一个是环境。

  • 可以使用(defun (macro X) (form env) ...)编写宏。

  • 没有编译器宏;常规宏完成编译器宏的工作。

  • 常规宏可以返回未扩展的形式,以指示它正在拒绝扩展。如果词法macrolet拒绝扩展,则机会转到更外层的macrolet,以此类推,直到全局defmacro。如果全局defmacro拒绝扩展,则该表单被认为已扩展,因此必须是函数调用或特殊形式。

  • 如果我们同时有一个名为X的函数和宏,我们可以使用(call (fun X) ...)(call 'X ...)调用函数定义,或者使用Lisp-1风格的dwim求值器(dwim X ...),几乎总是通过其[]语法糖使用,如[X ...]

  • 为了完整起见,提供了函数mboundpmmakunboundsymbol-macro,它们是fboundpfmakunboundsymbol-function的宏类似物。

  • 特殊运算符orandif和其他一些也具有函数定义,因此可能存在像[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 的宏定义实现的。那样做并不好。主要原因是这会增加太多负担。程序员可能希望对于某个给定的函数,有一个与实现赋值语义无关的宏定义!
此外,如果 (second x) 实现了位置语义,那么当它没有嵌入到赋值操作中时,即不需要语义时,会发生什么?基本上,要满足所有要求,需要设计一种编写宏的方案,其复杂性将等于或超过处理位置的现有逻辑。
事实上,TXR Lisp 具有一种特殊类型的宏,称为“位置宏”。只有当形式用作位置时,才将其识别为位置宏调用。但是,位置宏本身并不实现位置语义;它们只进行简单的重写。位置宏必须扩展到被识别为位置的形式。
示例:指定当 (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扩展为不是位置的东西,事情就会失败:
4> (define-place-macro foo (x) ^(bar ,x))
foo
5> (macroexpand '(foo a))
(foo a)
6> (macroexpand '(set (foo a) 42))
** (bar a) is not an assignable place

我最终决定放弃使用我的玩具,转而使用一种非常简单且对我来说相当有效的方法... 当setf将一个表达式视为位置时,它会检查是否存在名为set-x的函数或宏,其中x是表达式的第一个元素,并将其重写为该函数或宏的调用,同时将值作为最后一个参数添加进去。因此,通过定义一个常规的函数/宏set-aref,你可以使(setf (aref x y) z)扩展为(set-aref x y z)。我发现这种方法比Common Lisp中的setf机制更简单。 - 6502
@6502 CL也有这个功能,只是函数的名称是一个列表,而不是一个计算出来的符号名称。如果setf找不到(foo ...)形式(无论它是什么)的扩展器,那么它将尝试调用函数(setf foo),就像你所做的那样。但当然它也有所有的扩展机制。如果你只有setter函数回退,那么它就是setf所做的一部分。 - Kaz
是的...我知道。那是我发现需要的唯一部分(一旦你将函数和宏放在不同的命名空间中)。实际上,我的当前方法甚至更愚蠢...只有一个命名空间,但如果表达式的第一个元素是一个符号,比如(foo x),那么首先会检查是否存在名为m/foo的函数,如果存在,则进行宏展开,否则编译器将把它视为(funcall f/foo x) - 6502

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接