【发布时间】:2020-04-27 23:39:06
【问题描述】:
AND 和 OR 是宏,由于宏不是方案/球拍中的第一类,它们不能作为参数传递给其他函数。部分解决方案是使用 and-map 或 or-map。是否可以编写一个可以采用任意宏并将其转换为函数的函数,以便可以将其作为参数传递给另一个函数?是否有任何语言具有一流的宏?
【问题讨论】:
AND 和 OR 是宏,由于宏不是方案/球拍中的第一类,它们不能作为参数传递给其他函数。部分解决方案是使用 and-map 或 or-map。是否可以编写一个可以采用任意宏并将其转换为函数的函数,以便可以将其作为参数传递给另一个函数?是否有任何语言具有一流的宏?
【问题讨论】:
一般来说,不会。考虑let 已(或可能)作为lambda 之上的宏实现:
(let ((x 1))
(foo x))
可能是一个扩展为的宏
((lambda (x) (foo x)) 1)
现在,将let 转换为函数会是什么样子?显然,这是无稽之谈。它的输入是什么?它的返回值?
很多宏都是这样的。事实上,任何可以常规转换为函数而不会丢失任何功能的宏都是坏宏!这样的宏本来应该是一个函数。
【讨论】:
我同意@amalloy。如果将某些东西写成宏,它可能会做一些函数不能做的事情(例如,引入绑定、更改评估顺序)。因此,即使可能,自动将任意宏转换为函数也是一个非常糟糕的主意。
是否可以编写一个可以接受任意宏并将其转换为函数的函数,以便可以将其作为参数传递给另一个函数?
不,但编写一个 macro 将 some 宏转换为函数是可行的。
#lang racket
(require (for-syntax racket/list))
(define-syntax (->proc stx)
(syntax-case stx ()
[(_ mac #:arity arity)
(with-syntax ([(args ...) (generate-temporaries (range (syntax-e #'arity)))])
#'(λ (args ...) (mac args ...)))]))
((->proc and #:arity 2) 42 12)
(apply (->proc and #:arity 2) '(#f 12))
((->proc and #:arity 2) #f (error 'not-short-circuit))
您可能还对identifier macro 感兴趣,它允许我们在某些上下文中将标识符用作宏并在另一个上下文中起作用。这可以用来创建第一类and/or,当它用作宏时会短路,但可以作为非变压器位置的函数值传递。
关于一级宏的话题,请看https://en.wikipedia.org/wiki/Fexpr。众所周知这是个坏主意。
【讨论】:
不像你想象的那样
要了解原因,这里有一种关于宏的思考方式:宏是一个函数,它获取一些源代码并将其转换为另一种源代码:宏的扩展。换句话说,宏是一个函数,其域和范围是源代码。
一旦源代码完全展开,它就会被提供给评估器或编译器。让我们假设它被馈送到编译器,因为它使问题更容易回答:编译器本身只是一个函数,其域是源代码,其范围是机器的一些指令序列(可能是也可能不是真实的机器)执行。这些指令可能包括诸如“在这些参数上调用此函数”之类的内容。
那么,你要问的是:“在这些参数上调用这个函数”中的“这个函数”可以是某种宏吗?嗯,是的,它可能是,但是无论它要转换的源代码肯定不能是你正在执行的程序的源代码,因为它已经消失了:剩下的就是作为返回值的指令序列编译器。
所以你可能会说:好吧,假设我们不允许编译器:我们现在可以这样做吗?好吧,撇开“不允许编译器”是一种严重的限制不谈,事实上,这是非常古老的 Lisp 方言所做的事情,使用称为 FEXPR 的构造,正如另一个答案中提到的那样。重要的是要意识到 FEXPR 的存在是因为人们还没有发明宏。很快,人们确实发明了宏,尽管 FEXPR 和宏共存了一段时间——主要是因为人们编写了使用 FEXPR 的代码,他们希望继续运行,而且因为在反引号之类的东西出现之前编写宏是一件非常痛苦的事情——FEXPR 死了出去。它们消失了,因为它们在语义上很糟糕:即使按照 1960 年代 Lisps 的标准,它们在语义上也很糟糕。
这里有一个关于 FEXPR 如此可怕的小例子:假设我用一种带有 FEXPR 的语言编写了这个函数:
(define (foo f g x)
(apply f (g x)))
现在:当我拨打 foo 时会发生什么?特别是,如果f 可能是 FEXPR,会发生什么?。好吧,答案是我根本无法编译 foo:我必须等到运行时,然后即时决定要做什么。
当然,这不是这些带有 FEXPR 的旧 Lisps 所做的:他们只是默默地假设 f 是一个普通函数(他们会称之为 EXPR)并相应地编译(是的,甚至非常旧的 Lisp 有编译器)。如果你传递了一个你刚刚丢失的 FEXPR 的东西:要么是东西检测到了,要么它更可能是可怕地摔倒,或者给了你一些垃圾答案。
而这种可怕之处就是发明宏的原因:宏提供了一种语义上健全的方法来处理 Lisp 代码,它允许(最终,这需要很长时间才能真正发生)像编译这样的小细节完全可以实现,代码具有合理的语义和编译代码具有与解释代码相同的语义。事实证明,这些是人们喜欢他们的语言的功能。
顺便说一下,在 Racket 和 Common Lisp 中,宏都是明确的函数。在 Racket 中,它们是对特殊“语法”对象进行操作的函数,因为这是您获得卫生的方式,但在不太卫生的 Common Lisp 中,它们只是对 CL 源代码进行操作的函数,源代码只是简单地制作由列表、符号等组成。
以下是 Racket 中的一个示例:
> (define foo (syntax-rules ()
[(_ x) x]))
> foo
#<procedure:foo>
好的,foo 现在只是一个普通函数。但它是一个域和范围是 Racket 源代码的函数:它需要一个语法对象作为参数并返回另一个:
> (foo 1)
; ?: bad syntax
; in: 1
; [,bt for context]
这是因为1 不是语法对象。
> (foo #'(x 1))
#<syntax:readline-input:5:10 1>
> (syntax-e (foo #'(x 1)))
1
在 CL 中这更容易看到:这是一个宏定义:
(defmacro foo (form) form)
现在我可以获取宏的函数并在一些 CL 源代码上调用它:
> (macro-function 'foo)
#<Function foo 4060000B6C>
> (funcall (macro-function 'foo) '(x 1) nil)
1
在 Racket 和 CL 中,宏实际上是一流的(或者,在 Racket 的情况下,我认为几乎是一流的):它们是对源代码进行操作的函数,而源代码本身就是一流的——类:您可以编写 Racket 和 CL 程序,它们以任意方式构造和操作源代码:这就是这些语言中的宏。
在 Racket 的情况下,我说“几乎是一流的”,因为我看不到在 Racket 中检索位于用 define-syntax &c 定义的宏后面的函数的方法。
【讨论】:
syntax-local-value 检索宏的转换器函数,但这并不是决定一流的,至少不是我认为一流的方式
apply 或 @987654337 @ 只是假设它的参数不是 fexpr。我认为使 Racket 的宏成为一流的事情是,如果您可以在运行时访问它们作为函数的定义,那么您可以像在 CL 中那样梳理宏扩展,而不仅仅是拥有 expand 或其他任何东西一切。
我在 Scheme 中创建了类似的东西,它是返回 lambda 的宏,它使用 eval 来执行宏:
(define-macro (macron m)
(let ((x (gensym)))
`(lambda (,x)
(eval `(,',m ,@,x)))))
示例用法:
;; normal eval
(define x (map (lambda (x)
(eval `(lambda ,@x)))
'(((x) (display x)) ((y) (+ y y)))))
;; using macron macro
(define x (map (macron lambda)
'(((x) (display x)) ((y) (+ y y)))))
和x 在这两种情况下都是两个函数的列表。
另一个例子:
(define-macro (+++ . args)
`(+ ,@args))
((macron +++) '(1 2 3))
【讨论】: