blog.bouzuya.net

2012-05-08 4Clojureを楽しむ(5)〜#()マクロ〜

4Clojureで遊びながら学ぶ。今日は4Clojureから気づいたことを書く。今日は#()リーダーマクロについて書く。

fn, #() はなくても良い

"Write a function"などと書かれていてもfn#()で書きはじめる必要はない。これは4Clojureのためのテクニックである。

例えば、ある数値のシーケンスから奇数だけを取り出すような問題があったとする。

(= (__ [1 2 3 4 5]) [1 3 5])
(= (__ [2 4]) [])
(= (__ [0 1 2]) [1])

(fn [s] (filter odd? s))#(filter odd? %)と解答したくなるかもしれないが、4Clojureではfilter odd?で十分だ。(partial filter odd?)である必要さえない。関数ではなくても良い。入力したものがそのまま設定される。

さほど重要ではないが、4Clojureに取り組むなら知っておいて損はない。

#()は最後に置換する

#(...)は便利なリーダーマクロである。(fn [] ...)よりも短く書けるし、引数もよしなに処理してくれる。しかし大きな弱点がある。それは入れ子にできないことだ。

例えば、ある数値のシーケンス5よりも大きい数だけを取り出すような問題があったとする。

(= (__ (range 10)) [6 7 8 9])
(= (__ (range 5)) [])
(= (__ (range 10 15)) [10 11 12 13 14])

#(filter #(> % 5) %)と書きたくなるが、これはClojureでは動作しない。#()は入れ子にできないからだ。この場合には(fn [s] (filter #(> % 5) s))とする必要がある。(もちろん(partial filter #(> % 5))でも良い。こちらの方が分かりやすいかもしれない。)

これくらいの問題なら見た瞬間に入れ子になりそうだと気づく。しかし複雑な問題だったり、Clojureに慣れていないと複雑な条件を組みたてているうちに#()を入れ子にしてしまうことがままある。

対策としては最初はすべてfnで書いてしまうことだ。最後に提出する前に、あるいはテストをクリアした後に内側のものだけを#()にすれば良い。

その他の注意点

おまけとしてその他の#()の注意点をいくつか書く。まず#()だと分配束縛ができない。ほかにも[x]を返すために[x]と書くことができない。具体例で説明しよう。

フィボナッチ数列の先頭n個を返す問題がある。

(= (__ 1) [1])
(= (__ 3) [1 1 2])
(= (__ 5) [1 1 2 3 5])

はじめぼくは次のように書いていた。

(fn [n] (take n (map second
  (iterate #(vector (second %) (+ (first %) (second %))) [0 1]))))

この解はそんなに悪くない。Clojureらしい解き方だ。[[0 1] [1 1] [1 2] [2 3] [3 5] ...]という遅延シーケンスを生成し、各要素を2番目だけに変換し、先頭n個を取り出す。悪くない。

でももっとうまく書ける。うるさいfirst/secondをなくすことができる。

#(take % (map second (iterate (fn [[x y]] [y (+ x y)]) [0 1])))

考え方は同じだ。違うのは#()fnに置き換えて分配束縛を使うようにしたことだ。(fn [[x y]] ...)とすることでうるさいfirst/secondはなくなった。コアな部分がより分かりやすく読める。また#()だと([...])の形を避けるために#(vector ...)としていたがfnなら[]で書ける。

外側に#()があることは先程の内側のfn#()に書き換えるというルールには反する。しかし、そもそもフィボナッチ数列としては(map second ...)の時点でできあがっている。この問題にとりあえず合わせるためのtake nなので、「一時的な代用として使っている」という観点で#()を見れば、より自然な位置で使用されているように思える。

便利な記法である#()だが、注意点もある。うまく使っていきたい。

60 min.