defn和defmacro有什么区别?

Bel*_*lun 32 clojure

defn和defmacro有什么区别?函数和宏有什么区别?

sep*_*p2k 59

defn定义一个函数,defmacro定义一个宏.

函数和宏之间的区别在于,在函数调用中,首先计算函数的参数,然后使用参数计算函数体.

另一方面,宏描述了从一段代码到另一段代码的转换.任何评估都在转换后进行.

这意味着可以多次评估参数或根本不评估参数.作为一个例子or是一个宏.如果第一个参数or为false,则永远不会计算第二个参数.如果or是函数,则这是不可能的,因为在函数运行之前始终会计算参数.

这样做的另一个结果是,在扩展宏之前,宏的参数不必是有效的表达式.例如,你可以定义一个宏mymacro,使得(mymacro (12 23 +))扩展到(+ 23 12),所以这将工作,即使(12 23 +)对自己将是无稽之谈.您无法使用函数执行此操作,因为(12 23 +)在函数运行之前会对其进行求值并导致错误.

一个小例子来说明差异:

(defmacro twice [e] `(do ~e ~e))
(twice (println "foo"))
Run Code Online (Sandbox Code Playgroud)

twice将列表(println "foo")作为参数.然后它将其转换为列表(do (println "foo") (println "foo")).这个新代码是执行的.

(defn twice [e] `(do ~e ~e))
(twice (println "foo"))
Run Code Online (Sandbox Code Playgroud)

println "foo"是立即评估的.由于println返回nil,因此调用两次nil作为其参数.twice现在生成列表(do nil nil)并返回结果.请注意,这里(do nil nil)不作为代码进行评估,它只是作为列表处理.

  • @Nicolas:我不同意(关于其他所有事情,我的意思是).使用常量参数调用的非递归函数通常也会在编译时作为优化进行求值,但它们仍然没有宏所具有的任何属性.另外,完全可以编写一个完全解释的clojure实现(因此没有编译时),并且它的行为仍然与官方的clojure实现完全相同.即在编译时发生的宏扩展是一个实现细节. (3认同)
  • @Belun:最简单的例子:( defmacro两次[e]`(do~e~e))(两次(println"foo"))将打印foo两次. (2认同)

Art*_*ldt 37

其他答案深入讨论了这个问题,所以我会尽量简洁地介绍它.我会很感激编辑/评论如何更清晰地写它,同时保持清晰:

  • 一个函数变换值转换为其他的值.
    (reduce + (map inc [1 2 3])) => 9

  • 一个变换代码到其他代码.
    (-> x a b c) => (c (b (a x))))

  • 对不起,2年太晚了,但我真的很喜欢这个答案!超级简洁,美妙地解释. (6认同)
  • 好的,使第一个变得更加复杂以平衡所有内容.. :) (2认同)

Joh*_*den 12

宏就像有一个学徒程序员,你可以写笔记:

有时,如果我正在尝试调试某些东西,我喜欢改变类似的东西

(* 3 2)
Run Code Online (Sandbox Code Playgroud)

进入这样的事情:

(let [a (* 3 2)] (println "dbg: (* 3 2) = " a) a)
Run Code Online (Sandbox Code Playgroud)

它的工作方式相同,只是它打印出刚刚评估的表达式及其值,并将值作为整个表达式的结果返回.这意味着我可以在检查中间值时保持代码不受干扰.

这可能非常有用,但它很耗时且容易出错.您可能会想到将这些任务委托给您的学徒!

您可以编程编译器为您做这些事情,而不是雇用学徒.

;;debugging parts of expressions
(defmacro dbg[x] `(let [x# ~x] (println "dbg:" '~x "=" x#) x#))
Run Code Online (Sandbox Code Playgroud)

现在尝试:

(* 4 (dbg (* 3 2)))
Run Code Online (Sandbox Code Playgroud)

它实际上对你的代码进行了文本转换,虽然它是一台计算机,它为其变量选择了不可读的名称,而不是我选择的"a".

我们可以问它对给定表达式会做什么:

(macroexpand '(dbg (* 3 2)))
Run Code Online (Sandbox Code Playgroud)

这是它的答案,所以你可以看到它真的在为你重写代码:

(let* [x__1698__auto__ (* 3 2)]
      (clojure.core/println "dbg:" (quote (* 3 2)) "=" x__1698__auto__)
      x__1698__auto__)
Run Code Online (Sandbox Code Playgroud)

尝试写一个做同样事情的函数dbgf,你会遇到问题,因为(dbgf(*3 2)) - >(dbgf 6)在调用dbgf之前,所以无论dbgf做什么,它都无法恢复需要打印的表达式.

我相信你可以想到很多方法,比如运行时评估或传入一个字符串.尝试使用defn而不是defmacro编写dbg.这将是一个很好的方式来说服自己,宏是一种语言中的好东西.一旦你有了它的工作,尝试在具有副作用和值的表达式上使用它,比如

(dbg (print "hi"))
Run Code Online (Sandbox Code Playgroud)

实际上,我们已经准备好使用LISP的(包围((语法)))以获得它们.(虽然我必须说我非常喜欢它(但后来(我)有点奇怪(在头脑中)).

C也有宏,它们的工作方式大致相同,但它们总是出错,为了使它们正确,你需要在程序中加入如此多的括号,使其看起来像LISP!

你实际上建议不要使用C的宏,因为它们容易出错,尽管我已经看到它们已经被那些真正了解它们正在做什么的人们所效用.

LISP宏是非常有效的,语言本身就是由它们构建的,你会注意到你是否看到了自己用Clojure编写的Clojure源文件.

基本语言非常简单,因此易于实现,然后使用宏构建复杂的上层结构.

我希望这会有所帮助.这比我平常的答案要长,因为你问了一个很深的问题.祝好运.

  • C宏与_不像Lisp宏.C宏在_text_上运行,即在构成源代码的字符串上运行,而Lisp宏在_abstract语法树上运行,即程序结构. (7认同)