a[i][j] を (a i j) と書きたい.

definline を使って Clojure でも配列を Java のように簡潔に扱えるようにします.

関数のような配列

Clojure で配列を操作するコードは たとえば,二次元配列にアクセスする場合,Java なら a[i][j] で済むところが Clojure だと:

(aget ^longs (aget ^objects a i) j)

となります.Java と比べて長く,さらに読みにくいです.

ここで,Clojure のコレクションが関数であることを思い出しましょう. たとえば,ベクタなら:

user> (def v [1 2 3 4])
#'user/v
user> (v 3)
4

のように,インデックスをキーとして要素にアクセスすることができます. これを配列にも適用して, Java の a[i][j](a i j) と書けるようにしましょうというのがこの記事の趣旨です. 配列への代入 a[i][j] = x は,(a= i j x) と書くことにします.

やってはいけない

これを実現する方法として関数 a, a= を定義するのが簡単です.

user> (def -a (make-array Long/TYPE 1000 1000))
#'user/-a
user> (defn a [i j] (aget ^longs (aget ^objects -a i) j))
#'user/a'
user> (defn a= [i j ^long x] (aset ^longs (aget ^objects -a i) j x))
#'user/a='

しかし,この方法はとても遅いのでやってはいけません. 試しに,配列 -a の全要素を inc する関数 inc-loop を書いてみると,

user> (defn inc-loop []
        (loop [i 0 j i]
           (when (< i 1000)
             (if (< j 1000)
                 (do
                  (a= i j (inc (a i j)))
                  (recur i (inc j)))
                 (recur (inc i) 0)))))
#'user/inc-loop''
user> (time (dotimes [n 1000] (inc-loop)))
"Elapsed time: 113763.894 msecs"
nil

すっきり書けるのはいいのですが,とても遅いです. 素の aget/aset 版と比べてどれほど遅いかというと,

user> (defn inc-loop
          []
        (loop [i 0 j 0]
           (when (< i 1000)
             (if (< j 1000)
                 (do
                  (aset ^longs (aget ^objects -a i) j (inc (aget ^longs (aget ^objects -a i)j)))
                  (recur i (inc j)))
                 (recur (inc i) 0)))))
#'user/inc-loop
user> (time (dotimes [n 1000] (inc-loop)))
"Elapsed time: 6320.196 msecs"
nil

関数 a, a= 版は 20 倍も遅いです.

インライン化する

a, a= が遅かったのはメソッド呼び出しのオーバーヘッドです. これを無くすには関数をインライン化すればよく,ちょうど Clojure には definline というマクロが用意されています.早速インライン化してみましょう:

user> (definline a
          [i j]
        (let [larray (gensym "larray")]
          `(let [~larray (aget ~(with-meta `-a {:tag 'objects}) ~i)]
             (aget ~(with-meta larray {:tag 'longs}) ~j))))
#'user/a
user> (definline a=
          [i j x]
        (let [larray (gensym "larray")]
          `(let [~larray (aget ~(with-meta `-a {:tag 'objects}) ~i)]
             (aset ~(with-meta larray {:tag 'longs}) ~j (long ~x)))))
#'user/a=

これらインライン化された a, a= を使って inc-loop を実行してみます:

user> (defn inc-loop []
        (loop [i 0 j i]
           (when (< i 1000)
             (if (< j 1000)
                 (do
                  (a= i j (inc (a i j)))
                  (recur i (inc j)))
                 (recur (inc i) 0)))))
#'user/inc-loop
user> (time (dotimes [n 1000] (inc-loop)))
"Elapsed time: 6255.931 msecs"
nil

簡潔さはそのままに,素の aget/aset と同じ速度を得られました.

おわりに

今回は definline を使って配列操作を簡潔に書く方法を紹介しました. この方法を使うと,コレクションと同様に,配列を関数のように使うことができます.

今気づいたのですが, (a i j) という記述は,Java の a[i][j] と比べて括弧が少ないですね. 配列操作の記述が冗長なのは Clojure の弱点の一つだと思うのですが,これで, Java と同等か,括弧が少ない分 Java より楽しく書けそうです.

マクロはむしゃくしゃして書いてものなので改善の余地があるかもしれません. 気になったところがあればコメント欄にて添削してくださると嬉しいです.

blog comments powered by Disqus
  1. bgnori-technology reblogged this from tnoda-clojure
  2. tnoda-clojure posted this