2010年2月24日水曜日

再帰結果は束縛可能

あんま語られないんですけど、例えばこう言うコードがあるとします。

(define (multirember a lat)
(cond ((null? lat) '())
((eq? a (car lat))(multirember a (cdr lat)))
(else (cons (car lat)
(multirember a (cdr lat))))))

これは非常に綺麗なコードです。
実行結果は次のようになりますね。

> (multirember 'a '(a b c d a b c d))
(b c d b c d)
>

ところで、もう一度上のコードを見てみます。

(define (multirember a lat)
(cond ((null? lat) '())
((eq? a (car lat)) (multirember a (cdr lat))) ;1
(else (cons (car lat)
(multirember a (cdr lat)))))) ;2

まず特徴として、

  1. 分岐の木が
    cond
    により3本に分かれている。

  2. (multirember a (cdr lat))
    と言うのは実は書くと長い。


の二つがあります。
実は、この2番目が再帰のポイントなんで、「記述を重複するな」と言う言い方には無理があるんですけど、要するに記述のショートカットが出来るか否か、ってのがポイントではあるんです。
んで、実は、あまり知られてないんですが、再帰結果ってのは変数に束縛出来て使いまわしが可能なんです。
コード的には次のようになりますね。

(define (multirember a lat)
(if (null? lat)
'()
;; (car lat)も二回出てくるんでショートフォームitとして束縛
(let ((it (car lat))
;; 再帰部分もrecurと言う変数に束縛してしまう。
(recur (multirember a (cdr lat))))
(if (eq? a it) ;itの使いまわし
recur ;recurの使いまわし
(cons it recur))))) ;itとrecurの使いまわし

これでも全く同じように動作します。

> (multirember 'a '(a b c d a b c d))
(b c d b c d)
>

まあ、実際はあんま関係ないんですが、一応理論上は後者の方が若干効率が良い筈です。
と言うのも、Lisp系言語ほどコーディングスタイルが字面通り効率を表す言語は無いんじゃないか、と思われるから、です。
前者だと、例えば
(car lat)
が一々書かれていると、ホントにその場で計算される為、その計算が文字通り二回行われます。反面、2番目のコードだと、再帰に付き1回しか計算されず、その結果は即時、変数itに束縛され「使いまわされ」ます。従って若干効率が上がるんです。
再帰部分も同じですね。結局「結果」が欲しいだけですし、その計算自体は全く同じ、なんで、実はコード内に律儀に2回書く必要はないのです。

この様に、分岐が3段階以上に分かれて、かつ、再帰部分のコード自体は「全く同じ」場合は、試しに局所変数として束縛しちゃうのがお薦めです。

注:ちなみにcall/ccを用いて、

(define (multirember a lat)
(call/cc
(lambda (k)
(let ((it (car (if (null? lat)
(k '())
lat)))
(recur (multirember a (cdr lat))))
(if (eq? a it)
recur
(cons it recur))))))

と言う書き方も可能です。
元々、'()的な記述が必要なのは、Schemeが述語に関して#t#fを返す、と言うメンド臭い仕様になってるから、ですが、上記の方法だと、一応コード本体の分岐の木は二本で済みます。
ANSI Common Lispだと、

(defun multirember (a lat)
(and lat
(let ((it (car lat))
(recur (multirember a (cdr lat))))
(if (eq a it)
recur
(cons it recur)))))

ともうちょっとシンプルに書けますね。

0 件のコメント:

コメントを投稿