日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

Clojure のちょい便利マクロ

複数のシーケンスを対象に先頭からそれぞれ要素を取って処理したい時どうするか。 map が順当なんだけど渡す処理がごく簡単な場合ならまだしも ちょっと複雑になってくると少し見辛くなる。

例えばこんなの。

(map
  (fn [avar bvar cvar]
    (->> (foo-fn avar)
         (bar-fn bvar)
         (baz-fn cvar)))
    coll-a coll-b coll-c)

;; リーダマクロで多少簡略化できるが・・・
(map
 #(->> (foo-fn %1)
       (bar-fn %2)
       (baz-fn %3))
 coll-a coll-b coll-c)

これは結局渡すコレクションが下に来ているのが見辛さの原因になってる。 だったら上に持ってくる doseq でいいんじゃないかと最初思うがそうもいかない。

(doseq [a [1 2 3]
        b [4 5 6]]
  (println [a b]))
[1 4]
[1 5]
[1 6]
[2 4]
[2 5]
[2 6]
[3 4]
[3 5]
[3 6]
nil

複数シーケンスを渡した時の doseq は、全要素の組み合わせをたどる。 これはこれで良いのだが、各シーケンス要素を平行に取って処理したい時もしばしばある。

だったら最初の map に展開するようなマクロ作っちゃえと言う事でこんなん作った。

(defmacro domap
  "map 版 doseq。複数のシーケンスをバインドさせた時、全ての組み合わせを行うのではなく、
   map と同様に先頭から順に処理し、一番短い所で終了する。残りのアイテムは無視する。"
  [seq-exprs & body]
  `(doall (map (fn ~(vec (take-nth 2 seq-exprs))
                 ~@body)
               ~@(take-nth 2 (rest seq-exprs)))))

doall を入れておかないと REPL 上で副作用結果と map の結果シーケンスが入り混じって 表示される事になる。実際に返すシーケンスに副作用結果値が入るわけではないので それが気にならないなら doall は入れなくても構わない。lazy-seq じゃなくなってしまうしね。 結果シーケンスを捨てても良いなら doall の替わりに dorun という手もある。

使い方がこんな感じ。

(domap [i (range)
        j [3 4 5]]
  (println i j))
0 3
1 4
2 5
(nil nil nil)

ちょっとした小ネタでした。