日曜プログラミング

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

On Lisp -> Clojure へ移植: 10.4~10.6

10.4 反復

p96

(defmacro forever [& body]
  `(loop []
     ~@body
     (recur)))

図 53 単純な反復用マクロ.while, for は既出なので till のみ。 Clojure の loop には go や return はないが、条件分岐と recur を組み合わせたら マクロ定義時だけは同様の事が可能。「定義時だけ」と書いたのは Common Lisp のように do 派生マクロから go や return を呼ぶ事はできないため。

(defmacro till [test & body]
  `(loop []
     (when ~test
       ~@body
       (recur))))

p97

図 54 のマクロ定義へ行きたい所だが、内部で使っている map0-n は On Lisp で定義した 関数らしく、ここでは少し戻って図 9(p39) で示されている定義の Clojure 版を先に書く。

(defn mapa-b
  ([f a b]
     (map f (range a (inc b))))
  ([f a b step]
     (map f (range a (+ b step) step))))

(defn map0-n [f n]
  (mapa-b f 0 n))

(defn map1-n [f n]
  (mapa-b f 1 n))

(defn map-> [f start test-f succ-f]
  (loop [i start
         result nil]
    (if (test-f i)
      (reverse result)
      (recur (succ-f i) (cons (f i) result)))))

(defn mapa-b [f a b & [step]]
  (map-> f
         a
         (fn [x] (> x b))
         (fn [x] (+ x step))))

ClojureCommon Lisp そのままのオプショナル引数サポートは無いものの この場合なら引数の多重定義でいける。こうして書いてみて思うが map-> と mapa-b は通常の map 以上に loop に近い気がする。

ともあれ内部関数定義はできたので図 54 のマクロ定義へ戻る。 Clojure/名前空間修飾の区切りに使われるのでマクロ名は略称でない名前に変更している。

まずは忠実な移植をしてみた。

(defmacro do-tuples-open [parms source & body]
  (if parms
    (let [src (gensym)]
      `(let [~src ~source]
         (map (fn ~parms ~@body)
              ~@(map0-n (fn [n]
                          `(nthnext ~src ~n))
                        (dec (count parms))))))))

が、REPL での結果が print の戻り値 nil も含んだような結果になってしまう。

(do-tuples-open [x y] '(a b c d)
  (print (list x y)))
;=> ((a b)(b c)nil (c d)nil nil)

Clojure なら部分リストを作るのは標準で partition が使えるので、 doseq と組み合わせて書き換えてみた。

(defmacro do-tuples-open [parms source & body]
  (if parms
    (let [src (gensym)]
      `(let [~src ~source
             body-fn# (fn ~parms ~@body)]
         (doseq [x# (partition (count '~parms) 1 ~src)]
           (apply body-fn# x#))))))

(count '~parms) の部分はマクロ展開後の評価でシンボルないよと言われるので アンクォート後にクォートした上でカウントしてやる必要がある。 関数の仮引数リストとして渡す場合はシンボル解決が行われない為問題ない*1

テスト。上手く行く。

(do-tuples-open [x y] '(a b c d)
  (print (list x y)))
;=> (a b)(b c)(c d)
;   nil

と、思ったが、よくよく考えてみれば REPL 側での 関数の戻り値と標準出力とで出力順序が不定なだけなんじゃなかろうかと思い直し確かめてみる。

(def result (do-tuples-open [x y] '(a b c d)
               (print (list x y))))
;=> (a b)(b c)(c d)
;   #'user/result
result
;=> nil

やっぱそうでした。

気をとり直し、do-tuples-close へ。・・・ちょっとネスト深すぎやしませんかコレ。 理解がまだ追いつかないがまずは忠実に移植してみる。

(defn dt-args [len rest src]
  (map0-n (fn [m]
            (map1-n (fn [n]
                      (let [x (+ m n)]
                        (if (>= x len)
                          `(nth ~src ~(- x len))
                          `(nth ~rest ~(dec x)))))
                    len))
          (- len 2)))

(defmacro do-tuples-close [parms source & body]
  (if parms
    (with-gensyms [src rest bodfn]
      (let [len (count parms)]
        `(let [~src ~source]
           (when (nthrest ~src ~(dec len))
             (letfn [(~bodfn ~parms ~@body)]
               (loop [~rest ~src]
                 (when (not (nthnext ~rest ~(dec len)))
                   ~@(map (fn [args] `(~bodfn ~@args))
                          (dt-args len rest src))
                   nil)
                 (~bodfn ~@(map1-n (fn [n] `(nth ~rest ~(dec n)))
                                   len))
                 (recur (next ~rest))))))))))

それで動かしてみるが、IndexOutOfBoundsException になる。

(do-tuples-close [x y] '(a b c d)
  (print (list x y)))
;=> IndexOutOfBoundsException   clojure.lang.RT.nthFrom (RT.java:795)

;   (a b)(b c)(c d)(d a)

nth 辺りを何とかすればいいとは思うものの、それ以前に理解がイマイチ追いつかないw 気が向いたら do-tuples-open のように書き直してみる事にして次へ。

10.5 複数の値に渡る反復

多値に対応したこれまた結構複雑なマクロが示されてるが、 シーケンスなんかで返して受け側で let なりの部分で destructuring してやりゃいいんじゃねと 言う気がしてならなく、モチベが上がらんので飛ばす。

shiroさんの解説辺りを読むと性能が要求される場面で適用する意味があるかもとの事だけど、うーん。

10.6 マクロの必要性

ここも関数でも書ける場合があると説明があり、出てるサンプルも相当小さいので飛ばす。 終わってみると 10.4 くらいしか書いてないな。

第 10 章 古典的なマクロ

はここまで。

*1:仮引数なのだから当然っちゃ当然