Clojure,宏能否完成函数无法完成的任务?

10
我正在学习Clojure宏,想知道为什么我们不能只使用函数进行元编程。
据我所知,宏和函数的区别在于宏的参数不会被评估,而是作为数据结构和符号传递,返回值在调用宏的地方被评估。宏在阅读器和求值器之间充当代理,可以以任意方式转换形式,然后执行评估。它们可能在内部使用所有语言特性,包括函数、特殊形式、文字、递归、其他宏等。
函数则相反,调用前先对参数进行评估,但在返回时不进行评估。但宏和函数的映射关系让我想知道,难道我们不能使用将函数引用(即形)作为参数,转换形式,在函数内部进行评估,最后返回其值的函数作为宏吗?这是否会在逻辑上产生相同的结果?当然,这样做很不方便,但从理论上讲,每个可能的宏都有一个等效的函数吗?
下面是一个简单的中缀宏。
(defmacro infix
  "translate infix notation to clojure form"
  [form]
  (list (second form) (first form) (last form)))

(infix (6 + 6)) ;-> 12

这里是使用函数的相同逻辑:

(defn infix-fn
  "infix using a function"
  [form]
  ((eval (second form)) (eval (first form)) (eval (last form))))

(infix-fn '(6 + 6)) ;-> 12

那么,这种看法是否适用于所有情况,还是有一些特殊情况,宏无法胜任?最终,宏只是对函数调用的一种语法糖吗?


3
请注意,宏会在宏展开时(通常在编译之前)递归地进行展开,而不是在运行时展开。编译后的代码将与您手动编写展开代码一样,因此不会产生性能损失。还要记住,eval 在一个空的词法环境中评估表达式。(let [x 10] (infix-fn '(x + 6))) => CompilerException ... Unable to resolve symbol: x - jkiiski
这些是我没有想到的好观点。 - Tuomas Toivonen
1个回答

16

在回答问题之前,如果我先阅读问题会有所帮助。

你的中缀函数只能与文字直接使用时才有效:

(let [m 3, n 22] (infix-fn '(m + n))
CompilerException java.lang.RuntimeException: 
Unable to resolve symbol: m in this context ...

这是@jkinski所指出的后果:当eval起作用时,m已经消失了。
宏能做函数不能做的事情吗?
是的。但是如果你可以用函数来实现,通常应该使用函数。
宏适用于以下情况:
- 延迟求值; - 捕获表达式; - 重新组织语法;
这些都是函数无法做到的。
延迟求值
考虑一下(来自《Programming Clojure》一书,作者是Halloway和Bedra)
(defmacro unless [test then]
  (list 'if (list 'not test) then)))

...一个if-not的部分克隆。让我们用它来定义

(defn safe-div [num denom]
  (unless (zero? denom) (/ num denom)))

...这将防止除以零,返回nil:
(safe-div 10 0)
=> nil

如果我们试图将其定义为一个函数:

(defn unless [test then]
  (if (not test) then))

...然后

(safe-div 10 0)
ArithmeticException Divide by zero ...

潜在的结果被评估为unlessthen参数,在unless体忽略它之前。

捕获表单和重新组织语法

假设Clojure没有case形式。这里是一个粗略而简易的替代方案:

(defmacro my-case [expr & stuff]
  (let [thunk (fn [form] `(fn [] ~form))
        pairs (partition 2 stuff)
        default (if (-> stuff count odd?)
                  (-> stuff last thunk)
                  '(constantly nil))
        [ks vs] (apply map list pairs)
        the-map (zipmap ks (map thunk vs))]
    (list (list the-map expr default))))

这个函数会解析键(ks)和对应的表达式(vs),将后者包装成无参数的fn形式,然后构建一个从前者到后者的映射,并返回一个调用查找到的函数的形式。
细节并不重要,重要的是它是可行的。
当Guido van Rossum提议在Python中添加一个case语句时,委员会拒绝了他。所以Python没有case语句。如果Rich不想要一个case语句,但我想要一个,我可以有一个。
只是为了好玩,让我们使用宏来构建一个可以接受的if形式的克隆。这在函数式编程圈子里无疑是一个陈词滥调,但对我来说却很意外。我一直认为if是惰性求值的一个不可约简的原语。
一个简单的方法是借助my-case宏来实现。
(defmacro if-like
  ([test then] `(if-like ~test ~then nil))
  ([test then else]
   `(my-case ~test
     false ~else
     nil ~else
     ~then)))

这个太啰嗦和慢了,而且它使用堆栈并且丢失了recur,它被埋在闭包中。然而...
(defn fact [n]
  (if-like (pos? n)
    (* (fact (dec n)) n)
    1))

(map fact (range 10))
=> (1 1 2 6 24 120 720 5040 40320 362880)

...它能运行,或多或少。
请亲爱的读者,指出我代码中的任何错误。

在这个例子中,惰性求值也可以通过将then作为lambda表达式传递给unless函数来实现。是否存在一些情况,即使使用lambda也无法做到这一点? - Tuomas Toivonen
@TuomasToivonen 我认为lambda函数总是可以做到的。正如你所知,一个函数无法推迟其参数的评估。你肯定可以始终提供作为lambda捕获的未评估形式的参数。我们甚至可以使用lambda创建一个类似于if的宏:看,没有特殊形式。事实上,我现在就要这样做 :)。 - Thumbnail

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