代入を使わないうちは, 同じ引数での同じ手続きの二回の評価は同じ結果を生じ,手続きは数学的関数を計算すると見ることが出来た. 本書の初めの二章でやったように, 代入を使わないプログラミングは, 従って 関数型プログラミング(functional programming)という.
代入が話を難しくしていることを理解するため, 3.1.1節の make-withdraw手続きの, お金の不足のチェックを心配しない簡易版を考えよう:
(define (make-simplified-withdraw balance) (lambda (amount) (set! balance (- balance amount)) balance)) (define W (make-simplified-withdraw 25)) (W 20) 5 (W 10) -5この手続きと次のset!を使わないmake-decrementer手続きを比べて見よう:
(define (make-decrementer balance) (lambda (amount) (- balance amount)))make-decrementerは指定された金額balanceから入力を引く手続きを返す. しかしmake-simplified-withdrawのような, 連続した呼出しに対する積算効果はない:
(define D (make-decrementer 25)) (D 20) 5 (D 10) 15make-decrementerの働きを説明するのに, 置換えモデルを使うことが出来る. 例えば式
((make-decrementer 25) 20)の評価を解析してみよう. まずmake-decrementerの本体の balanceを25で置き換えて組合せの演算子を単純化する. 式は
((lambda (amount) (- 25 amount)) 20)と簡約される. 次にlambda式の本体のamountを20で置き換え, 演算子を作用させる:
(- 25 20)最後の答は5である.
しかし同じ置換え解析をmake-simplified-withdraw
((make-simplified-withdraw 25) 20)に試みると何が起きるか見よう. まずmake-simplified-withdrawの本体のbalanceを25で置き換えて組合せの演算子を単純化する. 式は
((lambda (amount) (set! balance (- 25 amount)) 25) 20)と簡約化される.9 次に lambda式の本体のamountを20で置き換え, 演算子を作用させる:
(set! balance (- 25 20)) 25ここで置換えモデルに執着すれば, 手続き作用の意味は, まずbalance に5をセットし, 式の値として25を返すことだといわなければならないだろう. これは間違った答である. 正しい答を得るためには, balanceの( set!の効果の前の)最初の出現と, balanceの(set!の効果の後の) 二回目の出現を区別しなければならないが, 置換えモデルにはこれは出来ない.
問題はわれわれの言語では, 置換えは究極的には, 記号は本質的に値に対する名前であるとの考えに基づいていることである. しかしset!と, 変数の値は変り得るという考えを取り入れると, 変数は最早単なる名前ではない. 変数は何か値が格納される場所を指し, この場所に格納される値は変化出来るのだ. 3.2節で, 環境がわれわれの言語モデルのこの「場所」の役目を果すのを見よう.
同じ引数でmake-decrementerを二度呼び出し, 二つの手続きを作り出したとしよう:
(define D1 (make-decrementer 25)) (define D2 (make-decrementer 25))D1とD2は同じだろうか. D1とD2は同じ計算上の振舞いをする---それぞれが25から入力を引く手続きである---から, 受け入れられる答はイエスである. 実際D1をD2に, 任意の計算で結果を変えることなく取り替えられる.
これをmake-simplified-withdrawで二度呼び出したのと比べよう:
(define W1 (make-simplified-withdraw 25)) (define W2 (make-simplified-withdraw 25))W1とW2は同じだろうか. 次の一連の対話で見るように, W1 とW2の呼出しは異る効果を持つから, 確実に違う:
(W1 20) 5 (W1 20) -15 (W2 20) 5W1とW2は, 両方が同じ式(make-simplified-withdraw 25)の評価で作り出されたという意味では「等しい」が, 任意の式で, W1をW2に, 式の評価の結果を変えることなく置き換えられるということはない.
式の評価の結果を変えることなく, 式の中で「等しいものは等しいもので置き換えられる」という概念の成り立つ言語を 参照透明(referentially transparent)であるという. 参照透明性はわれわれの計算機言語にset!を入れた時に破られた. そのため, 式をいつ等価な式で置き換えて単純化出来るかを決めることが微妙になってきた. 従って代入を使うプログラムについて, 推論することがずっと難しくなる.
参照透明性を一旦捨てると, 計算オブジェクトが「同じ」であるとは何を意味するかの概念を形式的に捕らえるのは難しくなる. 実はわれわれのプログラムがモデル化している実世界の「同じ」の意味もあまりはっきりしない. 一般に二つの見かけ上同じなオブジェクトが本当に「同じもの」であるかは, 一つのオブジェクトを変えてみて, もう一つのオブジェクトが同じように変っているかを見て決める. しかし「同じ」オブジェクトを二度観測し, オブジェクトのある性質が一回目と二回目の観測で違っているということ以外に, あるオブジェクトが「変った」ことがどうして分るだろうか. つまり何か先験的な 「同一」という概念なしに「変化」を決めることは出来ず, 変化の効果を観測することなしに同一性を決めることは出来ない.
この論点がプログラミングにどう現れるかの例として, PeterとPaulが100ドルある銀行口座を持っている状況を考えよう. これを
(define peter-acc (make-account 100)) (define paul-acc (make-account 100))とモデル化するのと, これを
(define peter-acc (make-account 100)) (define paul-acc peter-acc)とモデル化するのでは, 本質的な相違がある. 初めの状況では, 二つの銀行口座は別々である. Peterの取引きはPaulの口座には影響を及さず, 逆もそうだ. しかし二番目の状況では, paul-accをpeter-accと「同じもの」に定義した. 実効的にはPeterとPaulは共同の銀行口座を持ち, Peterが peter-accから払い出すとPaulはpaul-accのお金が減ったのを知るだろう. この似ているが違う二つの状況は, 計算モデルを構築するのに混乱をもたらす. 特に共有口座では, 二つの異る名前(peter-accとpaul-acc) を持つ一つのオブジェクト(銀行口座)があるのは, 非常に紛わしい; プログラムでpaul-accが変えられる場所をすべて探そうとすれば, peter-accを変えているものも探さなければならないことを覚えている必要がある.10
上の「同一」と「変化」の注意に関して, PeterとPaulが, 自分の口座を見ることだけ出来, 残高を変更する演算が実行出来ないなら, 二つの口座が違うという論点は価値がない. 一般にデータオブジェクトを変更しないうちは, 合成データオブジェクトは, 部品の集積に過ぎないと見ることが出来る. 例えば有理数は分子と分母が与えられれば決る. しかし合成データオブジェクトにはその構成要素とは違う「自己同一性」があるので, この見方は変化がある時には正当ではない. 銀行口座は, 払出しをして残高を変えても, 「同じ」銀行口座である. 逆にわれわれは, 同じ状態情報を持つ, 二つの別の銀行口座を持つことが出来る. この複雑さは, プログラム言語から来たのではなく, 銀行口座のオブジェクトとしての認識から来たのである. われわれは, 例えば分子だけ変えてもまだ「同じ」有理数だといわないように, 有理数が自己同一性を持った変化するものとは思っていない.
(define (factorial n) (define (iter product counter) (if (> counter n) product (iter (* counter product) (+ counter 1)))) (iter 1 1))内部の反復ループで引数を渡す代りに, 変数productとcounterの値を更新するのに明示的代入を使い, より命令型の流儀にすることが出来る:
(define (factorial n) (let ((product 1) (counter 1)) (define (iter) (if (> counter n) product (begin (set! product (* counter product)) (set! counter (+ counter 1)) (iter)))) (iter)))これは, プログラムの生じる結果を変えない. しかし微妙な罠をしかける. 代入の順序をどう決めるか. たまたま書いた通りで正しい. しかし代入を
(set! counter (+ counter 1)) (set! product (* counter product))と逆にすると別の正しくない結果を生じる. 一般に代入を使うプログラムでは, 代入の相対的順序を注意深く考え, 各文が変化した変数の正しい版を使うよう確認しなければならない. こういうことは関数型プログラミングにはない.11
命令型プログラムの複雑さは, 複数のプロセスが並行に走る応用を考えるともっと大変なことになる. この点には3.4節で戻ろう. しかしまだ, シミュレーションの設計のために, 代入を含む式の計算モデルを用意する問題を考慮し, 内部状態を持つオブジェクトの使い方を研究しよう.
問題 3.7
問題3.3で述べたパスワードの修正つきmake-accountで作り出した銀行口座オブジェクトを考えよう. この銀行システムは共同口座を作る機能を要求した. これを行う手続き
make-jointを定義せよ. make-jointは三つの引数をとる. 第一はパスワードで保護された口座. 第二引数はmake-jointが働くためには口座を定義した時のパスワードと合っていなければならない. 第三引数は新しいパスワードである. make-jointは新しいパスワードを使った元の口座への追加のアクセスを作り出す. 例えば, peter-accがパスワード
open-sesameを使った銀行口座なら,
(define paul-acc (make-joint peter-acc 'open-sesame 'rosebud))は名前paul-accとパスワードrosebudを使ってpeter-accでの取引きを許す. この新機能を満すため, 問題3.3の解を修正してよい.
9
set!の中の〈name〉は評価しないので,
set!式の中のbalanceの出現には置換えをしない. 置換えをしたら,
(set! 25 (- 25 amount))とおかしいものになってしまう.
10
一つの計算オブジェクトに複数の名前でアクセスする現象を
別名(aliasing)という. 共同銀行口座は別名の単純な例である. 3.3節では, 部品を共有する「別の」合成データ構造のような, もっと難しい例を見る.
二つの「違う」オブジェクトが違う別名で現れている一つのオブジェクトであるために, オブジェクトの変更が, 「副作用」として, 「別の」オブジェクトも変更することを忘れれば,
プログラムに虫が入りやすい. こういう副作用虫(side-effect bug)は見つけたり調べたりが難しく, 人によってはプログラム言語は, 副作用や別名を許さないように設計すべきだという
(Lampson他 1981, Morris, SchmidtおよびWadler 1980).
11
この見方では, プログラミングの入門講義が非常にしばしば高度の命令型の流儀で教えられるのは皮肉だ. これは1960年代から1970年代を通じて普通だった, 手続きを呼び出すプログラムは, 代入を実行するプログラムより本来低効率であるという信念の痕跡であろう.
(Steele (1977)はこの議論を公表する.) また別に初心者にとり, 一歩一歩の代入は, 手続き呼出しより, 視覚化が容易だという見方を反映しているらしい.
理由はともあれ, プログラミングを複雑にし, 重要な点を見え難くする「この変数の設定をあの前にすべきか, 後にすべきか」問題は初心者プログラマに,
しばしば負担をかけている.