Clojureで使って便利なマクロたち: .. doto ->> ->

この記事は Clojrure Advent Calendar 2011の参加記事です

(この記事はTokyo.clj#15で紹介した内容と同じです)

Clojureは従来の他のLisp系言語よりカッコを減らそうとしていたり、
オブジェクト指向ライブラリとの親和性を高めるための工夫が随所に見られます。
そういうものの中で今回はClojure特有の便利なマクロを4つ紹介します。

clojure.core/..

System.getProperties().get("os.name")

のようなメソッドチェーンを書くときに便利なマクロです。

.. を使わない場合

(.toLowerCase
 (.get
  (System/getProperties)
  "os.name"))
  • 入れ子怖い
  • メソッドと引数がはなればなれになる
  • 処理の順番に読めない


.. を使う場合

(.. System
    getProperties
    (get "os.name")
    toLowerCase)
  • 入れ子にならない
  • メソッドと引数がセットで保たれる
  • 処理の順番に読める


実際にはこの例はあまりイケてなくて、

(.toLowerCase (System/getProperty "os.name"))

と書く方が良いです。

使用例
(defn getCurrentDir []
  "get working directory"
  (.. (java.io.File. ".")
      getAbsoluteFile
      getParent))

……この例もあまりイケてないですね。

(System/getProperty "user.dir")

と書く方が良いです。

使いどころだけわかっていただければと思います。

clojure.core/doto

JPanel panel = new MyPanel();
panel.setFocusable(focusable);
panel.setForeground(fgcolor);
panel.setBackground(bgcolor);
panel.addKeyListener(panel);
panel.addConpomentListener(panel);

のようなことを書くときに便利なマクロです。

doto を使わない場合

(let [h (java.util.HashMap.)]
  (.put h "a" 1)
  (.put h "b" 2)
  (.put h "c" 3)
  h)
;; => #<HashMap {b=2, c=3, a=1}>
  • 一時変数に名前をつける必要がある
  • 第二引数に一時変数が来る同じパターンの繰り返し
  • 最後に値を返す必要がある
  • 値を返さない式が並んでいる……副作用怖い……


doto を使う場合

(doto (new java.util.HashMap)
  (.put "a" 1)
  (.put "b" 2)
  (.put "c" 3))
;; => #<HashMap {b=2, c=3, a=1}>
  • 第二引数に同じものが来るパターンを抽象化する
  • 一時変数に名前をつけなくて良い
  • 第一引数のオブジェクトが値として返る
  • "doto" が副作用のマーカーになる

clojure.core/->>

関数呼出の入れ子を(処理の積み重ね)を平坦化するマクロです。

(cons 'd (cons 'c (cons 'b (cons 'a ()))))
;; => (d c b a)

のような、最後の引数に前の処理の結果が来るパターンを

(->> ()
     (cons 'a)
     (cons 'b)
     (cons 'c)
     (cons 'd))
;; => (d c b a)

のように書くことができます。

このマクロを使えば入れ子の段数が増えませんし、処理が上から順番に読めるようになります。

clojure.core/->

関数呼出の入れ子を平坦化するマクロです。

(conj (conj (conj (conj () 'a) 'b) 'c) 'd)
;; => (d c b a)

のような、第一引数に前の処理の結果が来るパターンを

(-> ()
    (conj 'a)
    (conj 'b)
    (conj 'c)
    (conj 'd))

のように書くことができます。

doto と似ていますが、dotoでは結果が捨てられるのに対して、

  • > は 前の処理の結果が次の処理に渡るところが違います。

余談

ところで、4clojure をやっていると無名関数をよく作る羽目になります。

  • > と ->> はよく使うのですが、最初の引数をパラメータ化して関数として使うことが多くなってきます。

すると関数を返すバージョンをデフォルトで用意しておいて欲しくなってきました。

構文上の変更もすぐ実装できるのがLispのいいところ。ためしに実装して使ってみることにしました。

(defmacro => [& body]
  "same as #(-> % body ...)"
  `(fn [arg#]
     (-> arg# ~@body)))

(defmacro =>> [& body]
  "same as #(->> % body ...)"
  `(fn [arg#]
     (->> arg# ~@body)))

作ってみたけど実プログラムではあまり使いどころがありませんでした。
こんなゴルフのためだけのマクロなんてデフォルトで入ってなくて良かったです。