SRFI-1のiotaっぽいものをCommon Lispで書き直してみた(その2)

id:eel3:20090125:1232892677 で自作iotaをCommon Lispで書き直したまま放置状態だったのを思い出したので、勉強を兼ねて修正することにした。

まずドキュメント文字列を追加することにした。中身はインチキ英語。SRFIは仕様というかドキュメントなので、「SRFI-1から移植した」という表現はおかしく感じるのだけど*1、私の力量では良い修正案が思いつかない。

; 再帰版
(defun iota (count &optional (start 0) (step 1))
  "iota is a function ported from Scheme SRFI-1."
  (if (<= count 0)
      '()
      (cons start (iota (- count 1) (+ start step) step))))

; 末尾再帰(反復)版
(defun iota (count &optional (start 0) (step 1))
  "iota is a function ported from Scheme SRFI-1."
  (labels ((iota-iter (count num list)
             (if (<= count 0)
                 (reverse list)
                 (iota-iter (- count 1) (+ num step) (cons num list)))))
    (iota-iter count start '())))

; 末尾再帰(反復)版の trace対応バージョン
; (trace iota-iter) してからiotaを呼べばOK
(defun iota-iter (count start step list)
  (if (<= count 0)
      (reverse list)
      (iota-iter (- count 1) (+ start step) step (cons start list))))

(defun iota (count &optional (start 0) (step 1))
  "iota is a function ported from Scheme SRFI-1."
  (iota-iter count start step '()))

次に破壊的にリストを操作する関数を使ってみることにした。consとreverseは非破壊的な関数なので毎回新たなリストが生成されるのだけど、iotaの実装としてはconsやreverseで操作した結果は必要だけど操作前のリストは不要なので、破壊的に操作してしまっても問題ない。

破壊的な関数に変更した方が少しは経済的になるだろうし、ガベージコレクションの心配も減るだろう。ということでconsをpushに、reverseをnreverseに変更してみた。

; 再帰版
(defun iota (count &optional (start 0) (step 1))
  "iota is a function ported from Scheme SRFI-1."
  (if (<= count 0)
      '()
      (let (nums)
        (setf nums (iota (- count 1) (+ start step) step))
        (push start nums))))

; 末尾再帰(反復)版
(defun iota (count &optional (start 0) (step 1))
  "iota is a function ported from Scheme SRFI-1."
  (labels ((iota-iter (count num list)
             (if (<= count 0)
                 (nreverse list)
                 (iota-iter (- count 1) (+ num step) (push num list)))))
    (iota-iter count start '())))

; 末尾再帰(反復)版の trace対応バージョン
; (trace iota-iter) してからiotaを呼べばOK
(defun iota-iter (count start step list)
  (if (<= count 0)
      (nreverse list)
      (iota-iter (- count 1) (+ start step) step (push start list))))

(defun iota (count &optional (start 0) (step 1))
  "iota is a function ported from Scheme SRFI-1."
  (iota-iter count start step '()))

再帰版は少し工夫が必要。単純に以前のコードのconsをpushに変更しただけでは評価に失敗してしまい、動かない。pushはマクロなのだけど、どうもマクロ展開時に評価に失敗するようなコードが生成されてしまうらしい*2。今回は一時的にレキシカル変数を挟み込むことで回避した。

*1:SRFIが特定のアプリケーションやライブラリ等の「ソースコードを伴うもの」だったなら、「移植した」という表現でも違和感は感じないと思う。

*2:GNU CLISP 2.41では(setf iota)が定義されていないので評価に失敗。(setf iota)についてはこのページが参考になると思うけど、対処方法が分からない。xyzzy 0.2.2.235では、再帰呼び出ししているiotaのあたりでsetfフォームの展開に失敗。どちらもsetfがらみの問題というか、実は同じ問題かもしれない。