読者です 読者をやめる 読者になる 読者になる

internal defineのすすめ

末尾再帰という考えを知って間もない頃、関数の内部で関数を定義することにまだ違和感を持っていたので、named letを知ってからしばらくは末尾再帰で書けるものは何でもnamed letで書いていた。

internal defineに馴れてnamed let狂いの時期が過ぎた頃からちょっと大きな(と言ってもたかが知れてて、まぁ200行とか300行とかの)プログラムが急に書けるようになったのを覚えている。

named letは簡単なループのようなものを書くときには便利だけど、少し処理が複雑になってくるとすぐに書きにくくなってくるように感じる。internal defineの(末尾再帰的な処理を書くときにnamed letに対して持つ)優位性は2つくらいのことに起因するんじゃないかと思う。


まず、初期値の設定を遅らせることができること。named letでは、

(define (sum n)
  (let loop ((n n) (count 0))
    (if (= n 0)
        count
        (loop (- n 1) (+ count n))))

のようなコードを書くことになるけど、nやcountといった変数の隣に初期値が書いてあることで、「まず初期値があって、それに変化を繰り返し起こさせる」というか、値のインクリメンタルな更新の積み重ねが最終的な成果物を生む、という思考に陥りがちだと思う。

上の単純な例では分かりにくいかもしれないけれど、より複雑な再帰のコードを書こう(読もう)と思ったら、「直前の状態から次状態へどう更新するのか」という数学的帰納法的な思考方法が必要になってくるんじゃないだろか。そのためにはひとまず初期値が決まっていることは忘れていた方が考えやすい。気がする。

(define (sum n)
  (define (loop n count)
    (if (= n 0)   ;; まぁ本体のコードは変わらないけどね(^_^;
        count
        (loop (- n 1) (+ count n))))
  (loop n 0))


そして2つめは、入れ子になったループを内部から書けること。

(define (loop1 ...)   ;; ほとんど意味のない疑似コード
  (define (loop2 ...)
    ... (loop1 ...) ...  )
  ... (loop2 ...) ...  )

といったコードで、loop1内でloop2を呼び出し(tail call的な意味で。以下同様)、loop2は自分自身を再帰的に呼び、ときどきloop1を呼んで返るような二重のループがあったとする。単にfor文を入れ子にしてもbreakやcontinueだけでは書けないようなちょっと複雑な部類のループ。

このとき、最初に書くことになるのは外側のループであるloop1ではなくて内側のループのloop2だ。入れ子のループでは最も内側のループ内での処理が1番本質的な部分であることが多いだろうから、ここを始めに書けるのは一足飛びに処理の核心に触れられることと同じだろう。また、loop2を定義しているときには「loop2がどういう条件下で呼び出されるべきか」について考えなくてよく、境界条件について考えるのはloop1の定義を書くときまで先延ばしにすることもできる。

もちろん、これはボトムアップに攻めるかトップダウンに攻めるかといったアプローチの仕方によっては一概に利点とは言えないかもしれない。けれど、「どうやって書いたらいいんだ?」っていうような手探り状態でコードを書いてるようなときには、とりあえずいろんなものを後回しにしておいていきなり本質部分が書ける状態に入れた方が嬉しいでしょう。


んー、何が言いたいのかよく分からなくなってきた。文章が長くなってくるとどんどんダレてくるし、増長した文になるのがよくないッスね。とりあえず結論は、internal defineいいよ。たぶん。