letとshadowing 代入と束縛

 僕が始めてF#のソースを見た時、最初にドン引きした記述がこの
let x = 1
 という表記でした。これを見たとき頭に浮かんだのは、僕が中学のとき触れた日立の「BasicMaster Jr.」というパソコンのBASICでした。この頃のBASICには、代入文にletを書く必要があったのです。その後、NECのPC-8801でのN88-BASICではletを必要としなくなったため、代入がletで始まるというのはまさに「ここまで戻らないといけないのか・・・・」という衝撃だったのです。
 実際、BASICから始めてPASCAL、C、VB、C++、Javaという言語遍歴の僕にとって、関数型言語というのは「そこまで戻らないといけないもの」であったわけですが、このlet文の衝撃はその巻き戻しのスタート地点ともいえるものでした。正直、関数型言語になじむために必要な「頭の転換」に比べれば、構造化言語からオブジェクト指向への転換はほとんど誤差の範囲ではなかったかと思えるほどです。
 ここでは、F#(やOCaml)に目を向けたときに誰もが必ず感じる「束縛って何?」「代入と違うの?」という疑問を解消できるような説明を書いてみたいと思います。

代入とは

 実際のところ、代入と束縛を全く別のものとして区別して考える必要はそれほどありません。ただ、C言語の仕組みを知っている人は、それをイメージしてしまうと本質的な違いが分からないままになってしまうので、その違いだけ意識してもらえばいいでしょう。
 C言語、というかC++もJavaもBasicも、値と代入のイメージは以下のようなものです。
  • 変数宣言をして、特定の型の値がすっぽり収まるだけの領域をメモリ上に確保する。
  • その『領域』に名前(コンパイルするまで)を付ける。
  • 言語によっては、その領域にデフォルト値を設定する。
  • 何かの式を評価した値、プログラム中の固定値、他の変数の値などをその領域にコピーしたりしてプログラムの動作に活用する。
  • 値を返る必要がある場合は、その『領域』の中の値を変更する。
 言語やその型によっては、値が入る領域を確保しているように見せかけて、実は値が入る領域へのポインタが入る領域しか確保しないものがあるので注意が必要ですが、まぁだいたいこのようなものだと思います。

束縛とは

 対して、F#やOCamlなどの「束縛」とは、以下のようなイメージになります。
  • 値が、計算結果や入力の結果として何らかの形でまず先に「ある」。たぶんメモリ上にあるが、どのようにあるかを気にする必要はあまりない。値は存在する時点で型をすでに持っている。
  • その値に対し、名前を付ける。名前はもちろん内部的にはコンパイラの名前空間のもので、文字列というわけではない。
  • 名前とその値(へのポインタ)とをペアにして、名前リストの先頭に追加する。この名前リストは不変リストで、一度値を加えたら削除することも変更することもできない。ただし、リストの特徴「どの要素を先頭として見立てても、それを先頭とするリストとして解釈できる」を持っている。
  • ある名前を持つ値が必要になった場合は、リストの先頭(今自分がそこが先頭だと思っている場所)から順にリスト末尾まで名前で値を検索し、最初に見つかったものをその値とする。
  • 別の値に同じ名前をlet束縛すると、それ以降の同じ名前の変数があっても参照できなくなる。
 違いがイメージできたでしょうか。「まず値が先にある」「値に名前を付ける」「別の値に同じ名前を付けると、先に付けたほうの名前が見えなくなる」の3点を押さえてください。束縛には、「値を入れる」とか「値の入れ物(領域)」という概念がありません。値は「ある」もので、「別の値で書き換える」ことはできません。別の値もまた「別にある別の値」でしかなく、両方の値に同じ名前を付けると、先に付けたほうの名前が「見えなくなる」というだけです。これを「シャドウイング(shadowing)」と言いますが、F#において、値が変わっているように見えるのは実は値を上書きしているわけではなく、別の値に同じ名前を付けているだけなのです。

(執筆中)
最終更新:2012年01月20日 09:02