【问题标题】:Good examples of Clojure macros usage which demonstrate advantages of the language over the mainstream?Clojure 宏使用的好例子,展示了该语言相对于主流语言的优势?
【发布时间】:2012-05-13 03:51:02
【问题描述】:

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

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

【问题讨论】:

标签: macros clojure


【解决方案1】:

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

看宏,我建议你从Clojure的线程宏开始:thread-first和thread-last分别->->>;访问this page,以及许多various blogs that discuss Clojure

祝你好运,玩得开心。

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 很好,这就是我要找的那种东西。 (if (have-more you) (please-continue)) ;)
    • 我在 GitHub 上的 clojure-utils 库中的类似宏和函数的集合在稳步增长:github.com/mikera/clojure-utils
    【解决方案3】:

    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 时间都花在使用一个非常庞大的宏库上,因此我花了很多时间 解决宏不'的事实t 作曲很好,不是一流的。这个库的作者将在下一个版本中竭尽全力使所有功能都可用,而无需通过宏来允许像我这样的人在像 mapreduce 这样的高阶函数中使用它们。

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

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

    【讨论】:

    • 该死!我只是按照这些思路写一个答案,所以我只会+1这个:)我认为在 clojure 中,宏是 with-open
    • with-out-str 真的很整洁。谢谢!
    • 你能解释一下你的第二个例子吗?你说的泄漏是什么意思?关于使用宏作为接口,在您看来,您是否认为这只是库的部分作者的一个非常糟糕的决定,还是 clojure 宏只是以这样的方式工作,以至于除了一个人之外的任何人都太容易过度使用它们非常熟练的开发人员?
    • 虽然with-out-str确实是一个很大的方便,但关键不是宏观方面。关键是动态绑定,而*out* 就是这样一个动态绑定的变量。宏只是更容易分隔*out* 反弹到StringWriter 的代码部分。
    【解决方案4】:

    一个有用的宏的简单示例是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 并不神奇——您可以使用该语言的标准特性自己(重新)构建它。

    【讨论】:

    • 所以这基本上就像 JS 和其他几种语言中的“with”语句?
    • @Cray 是的,它是一个“with”,您可以自己实现,而无需深入编译器源代码来更改它识别的语法。
    • 另请注意,doto 适用于任何符合相同语法的东西——方法调用只是一个用例。
    • @Marko True, doto 与普通函数和其他宏同样适用,尽管它可能最常用于在 java 对象上设置大量属性。
    • 我只是认为注意到这一普遍性对 OP 很有帮助。
    【解决方案5】:

    对于我有时使用的宏,还有一个更深奥的用例:编写简洁易读的代码,同时也进行了全面优化。这是一个简单的例子:

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

    &amp; 表示它接受可变数量的参数(可变参数、可变参数函数)。在 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)))
    

    【讨论】:

    • +1 用于一些好的宏用途。我可能会窃取其中的一些想法,尤其是循环展开技巧!
    • 有趣的事实:围绕这个想法构建了整个特殊用途的 Lisp。 GOAL/GOOL 是完全编译的 Lisps,可生成优化的汇编代码。它们是制作某些主机游戏的主要工具。
    【解决方案6】:

    我正在处理的一个项目中的一些实际代码——我想要使用未装箱整数的嵌套 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/recurs 的东西,我最初是手写的,所以基本上没有运行时性能损失,不像等效的

    (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);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-05-22
      • 2010-09-19
      • 1970-01-01
      • 1970-01-01
      • 2014-01-19
      • 1970-01-01
      • 1970-01-01
      • 2017-10-11
      • 1970-01-01
      相关资源
      最近更新 更多