Clojure宏使用的很好的例子,证明了语言优于主流的优势?

时间:2012-05-03 15:33:52

标签: macros clojure

我正在考虑学习Clojure,但是来自基于c语法的(java,php,c#)命令式语言世界将成为一种挑战,所以人们自然会问自己,这真的值得吗?虽然这样的问题陈述可能非常主观且难以管理,但我仍然会阅读Clojure的一个特定特征(更常见的是lisps),这应该使它成为有史以来最灵活的语言:宏。

你有没有Clojure中宏用法的好例子,出于其他主流语言(考虑C ++,PHP,Perl,Python,Groovy / Java,C#,JavaScript中的任何一种)的目的,需要更不优雅的解决方案/ a很多不必要的抽象/黑客/等等。

6 个答案:

答案 0 :(得分:22)

我发现宏对于定义新的语言功能非常有用。在大多数语言中,您需要等待语言的新版本才能获得新语法 - 在Lisp中,您可以使用宏扩展核心语言并自行添加功能。

例如,Clojure没有命令式的C风格for(i=0 ;i<10; i++)循环,但您可以轻松地添加一个宏:

(defmacro for-loop [[sym init check change :as params] & steps]
  (cond
    (not (vector? params)) 
      (throw (Error. "Binding form must be a vector for for-loop"))
    (not= 4 (count params)) 
      (throw (Error. "Binding form must have exactly 4 arguments in for-loop"))
    :default
      `(loop [~sym ~init value# nil]
         (if ~check
           (let [new-value# (do ~@steps)]
             (recur ~change new-value#))
           value#))))

用法如下:

(for-loop [i 0, (< i 10), (inc i)]
  (println i))

为函数式语言添加命令式循环是否是一个好主意,这是我们应该避免的争论: - )

答案 1 :(得分:22)

在clojure的基础上有很多你没有想到的宏......这是一个好宏的标志,它们让你扩展语言,使生活更轻松。< / b>没有宏,生活将不那么令人兴奋。例如,如果我们没有

(with-out-str (somebody else's code that prints to screen))

然后您需要以您可能无法访问的方式修改其代码。

另一个很好的例子是

(with-open-file [fh (open-a-file-code ...)]
   (do (stuff assured that the file won't leak)))

整个with-something-do模式的宏已经真正添加到了clojure生态系统中。


众所周知的宏观硬币的另一面是,我基本上花了我所有(现在的)专业Clojure时间使用一个非常宏的重型库,因此我花了很多时间解决宏的问题写得好,不是一流的。这个库的作者在下一个版本中将不遗余力地通过宏来提供所有可用的功能,以允许像我这样的人在更高阶函数中使用它们,如mapreduce

当它们让生活更轻松时,宏可以改善世界。当它们是库的唯一接口时,它们会产生相反的效果。 请不要将宏用作接口

通常很难让数据的形状真正正确。如果作为一名图书馆作者,您的数据结构很好,可以说明您如何使用图书馆,那么很可能有一种方法可以重新构建内容,以允许用户以新的和难以想象的方式使用您的图书馆。在这种情况下,所讨论的奇妙库的结构非常好,它允许作者不想要的东西。不幸的是,一个伟大的图书馆受到限制,因为它的界面是一组宏而不是一组功能。 图书馆比它的宏更好,所以他们把它拿回来。这并不是说宏会以任何方式受到责备,只是编程很难,而且它们是另一种可以产生很多效果的工具,所有部分必须一起使用才能很好地工作。

答案 2 :(得分:14)

我有时会使用更为深奥的宏用例:编写简洁易读的代码,这些代码也经过了全面优化。这是一个简单的例子:

(defmacro str* [& ss] (apply str (map eval ss)))

这样做是在编译时连接字符串(当然,它们必须是编译时常量)。 Clojure中的常规字符串连接函数是str,所以在紧密循环代码中,我有一个长字符串,我想分成几个字符串文字,我只是将星号添加到str和将运行时串联更改为编译时。用法:

(str* "I want to write some very lenghty string, most often it will be a complex"
      " SQL query. I'd hate if it meant allocating the string all over every time"
      " this is executed.")

另一个不太重要的例子:

(defmacro jprint [& xs] `(doto *out* ~@(for [x xs] `(.append ~x))))

&表示它接受可变数量的参数(varargs,可变参数函数)。在Clojure中,可变参数函数调用使用堆分配的集合来传递参数(就像在Java中一样,它使用数组)。这不是最优,但如果我使用上面的宏,那么就没有函数调用。我这样用它:

(jprint \" (json-escape item) \")

它编译成三个PrintWriter.append的调用(基本上是一个展开的循环)。

最后,我想向您展示一些更加截然不同的东西。您可以使用宏来帮助您定义类似函数的clas,从而消除浩大数量的样板。以这个熟悉的例子为例:在HTTP客户端库中,我们需要为每个HTTP方法提供单独的函数。每个函数定义都非常复杂,因为它有四个重载签名。此外,每个函数都涉及Apache HttpClient库中的不同请求类,但其他所有HTTP方法都完全相同。看看我需要处理多少代码。

(defmacro- def-http-method [name]
  `(defn ~name
     ([~'url ~'headers ~'opts ~'body]
        (handle (~(symbol (str "Http" (s/capitalize name) ".")) ~'url) ~'headers ~'opts ~'body))
     ([~'url ~'headers ~'opts] (~name ~'url ~'headers ~'opts nil))
     ([~'url ~'headers] (~name ~'url ~'headers nil nil))
     ([~'url] (~name ~'url nil nil nil))))

(doseq [m ['GET 'POST 'PUT 'DELETE 'OPTIONS 'HEAD]]
  (eval `(def-http-method ~m)))

答案 3 :(得分:6)

在没有宏的情况下很难重新创建的有用宏的一个简单示例是doto。它评估其第一个参数,然后评估以下形式,将评估结果作为第一个参数插入。这可能听起来不多,但是......

doto这个:

(let [tmpObject (produceObject)]
 (do
   (.setBackground tmpObject GREEN)
   (.setThis tmpObject foo)
   (.setThat tmpObject bar)
   (.outputTo tmpObject objectSink)))

成为:

(doto (produceObject)
   (.setBackground GREEN)
   (.setThis foo)
   (.setThat bar)
   (.outputTo objectSink))

重要的是doto不是魔术 - 你可以使用语言的标准功能(重新)自己构建它。

答案 4 :(得分:6)

我正在处理的项目中的一些实际代码 - 我想要使用未装箱的int嵌套for-esque循环。

(defmacro dofor
  [[i begin end step & rest] & body]
  (when step
    `(let [end# (long ~end)
           step# (long ~step)
           comp# (if (< step# 0)
                   >
                   <)]
       (loop [~i ~begin]
         (when (comp# ~i end#)
           ~@(if rest
               `((dofor ~rest ~@body))
               body)
           (recur (unchecked-add ~i step#)))))))

一样使用
(dofor [i 2 6 2
        j i 6 1]
  (println i j))

打印出来

2 2
2 3
2 4
2 5
4 4
4 5

它编译成非常接近原始loop / recur的东西,这是我最初手工编写的,因此基本上没有运行时性能损失,与等效

不同
(doseq [i (range 2 6 2)
        j (range i 6 1)]
  (println i j))

我认为生成的代码与java等价物相比更有利:

for (int i = 2; i < 6; i+=2) {
    for (int j = i; j < 6; j++) {
        System.out.println(i+" "+j);
    }
}

答案 5 :(得分:3)

宏是Clojure的一部分,但恕我直言不相信它们是你应该或不应该学习Clojure的原因。数据不变性,处理并发状态的良好构造,以及它是JVM语言并且可以利用Java代码的事实有三个原因。如果您找不到其他学习Clojure的理由,请考虑一个函数式编程语言可能会对您如何处理任何语言的问题产生积极影响这一事实。

要查看宏,我建议您从Clojure的线程宏开始:分别是线程优先和线程最后->->>;访问this page和许多various blogs that discuss Clojure

祝你好运,玩得开心。