【发布时间】:2011-04-09 16:56:03
【问题描述】:
defn 和 defmacro 有什么区别?函数和宏有什么区别?
【问题讨论】:
标签: clojure
defn 和 defmacro 有什么区别?函数和宏有什么区别?
【问题讨论】:
标签: clojure
其他答案对此进行了深入介绍,因此我将尽可能简洁地进行介绍。我将不胜感激编辑/cmets 如何更简洁地编写它,同时保持清晰:
一个函数将值转换为其他值。(reduce + (map inc [1 2 3])) => 9
a 宏将代码转换为其他代码。(-> x a b c) => (c (b (a x))))
【讨论】:
(defn -> [val & fns] (reduce #(%2 %1) val fns))?
宏就像有一个学徒程序员,你可以写笔记:
有时,如果我尝试调试某些东西,我喜欢更改类似的东西
(* 3 2)
变成这样:
(let [a (* 3 2)] (println "dbg: (* 3 2) = " a) a)
它的工作方式相同,除了它打印出它的表达式 刚刚评估,它的值,以及返回值作为结果 整个表达。这意味着我可以在检查中间值时让我的代码不受干扰。
这可能非常有用,但它既费时又容易输入错误。你可以想象 将此类任务委派给您的学徒!
您可以编写编译器为您做这些事情,而不是雇用学徒。
;;debugging parts of expressions
(defmacro dbg[x] `(let [x# ~x] (println "dbg:" '~x "=" x#) x#))
现在试试:
(* 4 (dbg (* 3 2)))
它实际上为您对代码进行了文本转换,虽然 作为一台计算机,它为其变量选择不可读的名称,而不是我会选择的“a”。
我们可以问它对给定的表达式会做什么:
(macroexpand '(dbg (* 3 2)))
这就是它的答案,所以你可以看到它确实在为你重写代码:
(let* [x__1698__auto__ (* 3 2)]
(clojure.core/println "dbg:" (quote (* 3 2)) "=" x__1698__auto__)
x__1698__auto__)
尝试写一个函数 dbgf 做同样的事情,你会遇到问题,因为 (dbgf (* 3 2)) -> (dbgf 6) 在调用 dbgf 之前,所以无论 dbgf 做什么,它都可以'不恢复它需要打印出来的表达式。
我相信您可以想出很多方法来解决这个问题,例如运行时评估或传入字符串。尝试使用 defn 而不是 defmacro 编写 dbg。这将是让自己相信宏是语言中的好东西的好方法。一旦你得到它的工作, 尝试在具有副作用和值的表达式上使用它,例如
(dbg (print "hi"))
事实上,拥有宏非常好,我们已经准备好使用 LISP 的 (括号 ((syntax))) 来获取它们。 (虽然我必须说我宁愿喜欢它本身(但后来(我)有点奇怪(在头脑中))。
C 也有宏,它们的工作方式大致相同,但它们总是出错,为了使它们正确,您需要在程序中放入如此多的括号,使其看起来像 LISP!
实际上建议您不要使用 C 的宏,因为它们很容易出错,尽管我已经看到那些真正知道自己在做什么的人使用它们时效果很好。
LISP 宏非常有效,语言本身就是由它们构建的,如果您查看本身用 Clojure 编写的 Clojure 源文件,就会发现这一点。
基础语言非常简单,易于实现,然后使用宏构建复杂的上层结构。
我希望这会有所帮助。它比我通常的回答要长,因为你问了一个很深的问题。祝你好运。
【讨论】:
defn 定义一个函数,defmacro 定义一个宏。
函数和宏的区别在于,在函数调用中,首先计算函数的参数,然后使用参数计算函数体。
另一方面,宏描述了从一段代码到另一段代码的转换。任何评估都在转换之后进行。
这意味着参数可能会被计算多次或根本不计算。例如or 是一个宏。如果or 的第一个参数为假,则永远不会计算第二个参数。如果or 是一个函数,这将是不可能的,因为总是在函数运行之前计算参数。
这样做的另一个结果是宏的参数在扩展宏之前不必是有效的表达式。例如,您可以定义一个宏 mymacro,使 (mymacro (12 23 +)) 扩展为 (+ 23 12),因此即使 (12 23 +) 本身是无意义的,这也将起作用。您不能对函数执行此操作,因为 (12 23 +) 会在函数运行之前被评估并导致错误。
一个小例子来说明区别:
(defmacro twice [e] `(do ~e ~e))
(twice (println "foo"))
宏twice 获取列表(println "foo") 作为参数。然后将其转换为列表(do (println "foo") (println "foo"))。这个新代码就是被执行的。
(defn twice [e] `(do ~e ~e))
(twice (println "foo"))
这里println "foo" 立即被评估。由于println 返回nil,因此以nil 作为参数调用了两次。 twice 现在生成列表 (do nil nil) 并将其作为结果返回。请注意,这里的(do nil nil) 不被评估为代码,它只是被视为一个列表。
【讨论】:
没有听起来刻薄,一个创建一个函数,而另一个创建一个宏。在 Common Lisp 中(我假设这也适用于 clojure),宏在函数的实际编译之前被扩展。所以,懒猫:
(defmacro lazy-cat
"Expands to code which yields a lazy sequence of the concatenation
of the supplied colls. Each coll expr is not evaluated until it is
needed.
(lazy-cat xs ys zs) === (concat (lazy-seq xs) (lazy-seq ys) (lazy-seq zs))"
{:added "1.0"}
[& colls]
`(concat ~@(map #(list `lazy-seq %) colls)))
实际上会扩展为
`(concat ~@(map #(list `lazy-seq %) colls)))
其中lazy-seq随后将进一步扩展为
(list 'new 'clojure.lang.LazySeq (list* '^{:once true} fn* [] body)))
所有在实际处理传递给他们的数据之前。
Practical Common Lisp: Chapter 8 上有一个非常可爱的故事可以帮助解释差异(后面是一些信息丰富的示例)
【讨论】:
defn 定义一个函数,defmacro 定义一个宏。
宏就像一个函数,但将它的参数(如果它们是表达式作为数据),然后处理它们并返回数据(作为代码的符号列表),然后评估该返回代码。所以它用另一个代码替换了一个代码(在编译时)。
【讨论】: