letとletrecが必要なのはなぜか

確かに。letとletrecの2種類あるのはなんで?

letとletrecの違い

そもそもletrecのrecは再帰のこと。なので下のように再帰する定義を書くことができる。

(letrec (
  (fact (^n (if (zero? n) 1 (* n (fact (- n 1)))))))
  (fact 10))

逆にletは非再帰なのでこうは書けない。 代わりに

(let ((x 1))
  (let ((x (+ x 1)))
    x))

のように以前の定義をshadowingすることができる。 そしてこれが非再帰のletが必要な理由みたい。

どこでshadowingが必要か

ある identifier を、その値を使いつつ再定義する際に必要です {中略} リファレンス参照した上で、結果を同じ identifier に束縛しています。{中略}間違って元のリファレンスセルを参照できなくなりますからより安全。let が再帰がデフォルトだとするとこういう定義は書けません。どうしても別の identifier を使わなければならないので、元のリファレンスセルも参照できたままになってしまう。名前が面倒だし、リファレンスを隠せないので、不便かつ不安全

let が再帰でない理由というかメリット - camlspotter’s blog

簡単な例をSchemeで書くと

(let* ((hoge-lst (iota 10))
       (hoge-vec (list->vector hoge-lst))))

を下のように書けた方が嬉しいということかな。

(let* ((hoge (iota 10))
       (hoge (list->vector hoge))))

まあ、本質的に必要なのはletとletrecだけで、それを統合できない(したくない)理由はOCamlのそれとほとんど同じ。 もうひとつScheme特有な事情を挙げると、マクロがあるために「変数を確実にシャドウする手段」が必要ってことがある。 (aif <test> <then> <else>) で、を評価して真だったらその値を暗黙の変数itに束縛して<then>を評価する、なんて非健全なマクロを書いた場合、その展開は束縛フォームを挿入するが (簡略のためlegacy macroな表記で):

`(let ((it ,<test>))
   (if it ,<then> ,<else>))

<test>の中にitが含まれていたら、それは確実に外側のitを参照してもらわないと困る。「再帰参照があったらletrecとみなす」みたいに勝手に切り替わっちゃうとまずい。

Island Life - let地獄

最後に

ほとんどが引用になってしまったけど、以上でletとletrecの2種類が必要であることは分かった。

けど、「Scheme=再帰」なイメージなのでletrecの方がletより長いのにはまだ違和感がある。伝統的にそうなっているものを変えろと言うほどではないけど…

でも、もし名前を変えるならこれがしっくり来るかな。

参考