Programmer's Note

コード読み書きの備忘録。

mapのループをforで表現する

前回(mapと再帰で木構造を扱う - Programmer's Note)は、mapと再帰木構造を扱う処理を書いたが、mapはシーケンスを順になめていく高階関数であって、いわゆるコレクションを扱うfor文と変わらない。 Clojureにもforマクロは用意されていて、かなり表現力が高い。

前回のscale-tree関数は、

(defn scale-tree [tree factor]
  (map (fn [sub-tree]
         (if (coll? sub-tree)
           (scale-tree sub-tree factor)
           (* sub-tree factor)))
       tree))

だが、これをforを使って書き直すと以下になる。

(defn scale-tree [tree factor]
  (for [item tree]
    (if (coll? item)
      (scale-tree item factor)
      (* item factor))))

処理内容がだいぶ読みやすくなった。mapは汎用性が高く利点はあるが、無名関数の定義(fn [..] ..)が入る分コードが煩雑になり読みにくくなる。 Schemeなどだと無名関数はいわゆるlambda関数なので、表記が(lambda (...) ..)でさらに読みにくい。

forの場合は、扱うコレクションと各要素を代入する変数が一緒に、要素に対する処理の手前に置かれるので、コードを追いやすい。

プログラムの可読性はシンタックスがもろに効いてくることを実感させられる。

さて、SICPにはmapを使った以下のような例(練習問題)が出てくる。

(defn unique-pairs [n]
  (reduce concat
          (map 
            (fn [i]
              (map (fn [j] (list i j))
                   (range 1 i)))
            (range 1 (inc n)))))

これは、1 <= j < i <= nのiとjのペアの集合を作る関数だ。 実行結果は以下のようになる。

(unique-pairs 6)
;-> ((2 1) (3 1) (3 2) (4 1) (4 2) (4 3) (5 1) (5 2) (5 3) (5 4) (6 1) (6 2) (6 3) (6 4) (6 5))

このコードはmapを二重に使っていて読みにくいが、forを使うとすっきりと表現できる。

(defn unique-pairs [n]
  (reduce concat
          (for [i (range 1 (inc n))]
            (for [j (range 1 i)]
              (list i j)))))

これにとどまらず、Clojureforマクロはかなり便利で、二重ループはもっとすっきり表現できる。

(defn unique-pairs [n]
  (for [i (range 1 (inc n))
        j (range 1 i)]
    (list i j)))

なんと、これだけで済んでしまった。 Clojure見事なり。