28

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

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

4

6 回答 6

24

我发现宏对于定义新的语言特性非常有用。在大多数语言中,您需要等待语言的新版本才能获得新的语法 - 在 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))

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

于 2012-05-03T16:04:34.110 回答
23

clojure 的基础中有很多你没有想到的宏......这是一个好的宏的标志,它们让你以让生活更轻松的方式扩展语言。如果没有宏,生活就不会那么令人兴奋了。例如,如果我们没有

(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.

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

通常很难让数据的形状真正正确。如果作为图书馆作者,您的数据结构很好,可以很好地了解您对图书馆使用方式的设想,那么很可能有一种方法可以重新构建事物,以允许用户以新的和难以想象的方式使用您的图书馆。在这种情况下,所讨论的奇妙库的结构确实非常好,它允许作者没有打算做的事情。不幸的是,一个很棒的库受到限制,因为它的接口是一组宏而不是一组函数。图书馆比它的宏好,所以他们把它藏起来了。这并不是说宏有任何责任,只是说编程很难,它们是另一种可以产生多种效果的工具,所有部分必须一起使用才能正常工作。

于 2012-05-03T16:25:59.130 回答
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))))

&意味着它接受可变数量的参数(可变参数、可变参数函数)。在 Clojure 中,可变参数函数调用利用堆分配的集合来传输参数(就像在 Java 中使用数组一样)。这不是很理想,但如果我使用像上面这样的宏,那么就没有函数调用。我这样使用它:

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

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

最后,我想向你们展示一些更完全不同的东西。您可以使用宏来帮助您定义类似函数的类,从而消除大量样板文件。举这个熟悉的例子:在 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)))
于 2012-05-04T08:15:37.610 回答
7

我正在处理的项目中的一些实际代码——我想要使用未装箱整数的嵌套 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

它编译成非常接近我最初手写的 raw loop/ s 的东西,因此基本上没有运行时性能损失,不像等效的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);
    }
}
于 2012-05-09T01:40:09.460 回答
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不是魔法——你可以使用语言的标准特性自己(重新)构建它。

于 2012-05-03T18:17:28.543 回答
3

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

要查看宏,我建议你从 Clojure 的线程宏开始:thread-first 和 thread-last->分别->>为;访问这个页面,以及许多讨论 Clojure 的各种博客

祝好运并玩得开心点。

于 2012-05-03T15:49:43.463 回答