[ 目次, 前節, 次節, 索引 ]

3.1.3 代入を取り入れた代価



すでに見たようにset!演算により局所状態を持つオブジェクトがモデル化出来る. しかしこの利点はただではない. われわれのプログラム言語は最早, 1.1.5節で説明した手続き作用の置換えモデルを使っては解釈出来ない. その上「素敵な」数学的性質を持った単純なモデルで, プログラム言語のオブジェクトや代入を扱う適切な枠組となり得るものはない.

   代入を使わないうちは, 同じ引数での同じ手続きの二回の評価は同じ結果を生じ,手続きは数学的関数を計算すると見ることが出来た. 本書の初めの二章でやったように, 代入を使わないプログラミングは, 従って 関数型プログラミング(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)
15
make-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))
D1D2は同じだろうか. D1D2は同じ計算上の振舞いをする---それぞれが25から入力を引く手続きである---から, 受け入れられる答はイエスである. 実際D1D2に, 任意の計算で結果を変えることなく取り替えられる.

   これをmake-simplified-withdrawで二度呼び出したのと比べよう:

(define W1 (make-simplified-withdraw 25))

(define W2 (make-simplified-withdraw 25))
W1W2は同じだろうか. 次の一連の対話で見るように, W1W2の呼出しは異る効果を持つから, 確実に違う:
(W1 20)
5

(W1 20)
-15

(W2 20)
5
W1W2は, 両方が同じ式(make-simplified-withdraw 25)の評価で作り出されたという意味では「等しい」が, 任意の式で, W1W2に, 式の評価の結果を変えることなく置き換えられるということはない.

   式の評価の結果を変えることなく, 式の中で「等しいものは等しいもので置き換えられる」という概念の成り立つ言語を 参照透明(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-accpeter-accと「同じもの」に定義した. 実効的にはPeterとPaulは共同の銀行口座を持ち, Peterが peter-accから払い出すとPaulはpaul-accのお金が減ったのを知るだろう. この似ているが違う二つの状況は, 計算モデルを構築するのに混乱をもたらす. 特に共有口座では, 二つの異る名前(peter-accpaul-acc) を持つ一つのオブジェクト(銀行口座)があるのは, 非常に紛わしい; プログラムでpaul-accが変えられる場所をすべて探そうとすれば, peter-accを変えているものも探さなければならないことを覚えている必要がある.10

   上の「同一」と「変化」の注意に関して, PeterとPaulが, 自分の口座を見ることだけ出来, 残高を変更する演算が実行出来ないなら, 二つの口座が違うという論点は価値がない. 一般にデータオブジェクトを変更しないうちは, 合成データオブジェクトは, 部品の集積に過ぎないと見ることが出来る. 例えば有理数は分子と分母が与えられれば決る. しかし合成データオブジェクトにはその構成要素とは違う「自己同一性」があるので, この見方は変化がある時には正当ではない. 銀行口座は, 払出しをして残高を変えても, 「同じ」銀行口座である. 逆にわれわれは, 同じ状態情報を持つ, 二つの別の銀行口座を持つことが出来る. この複雑さは, プログラム言語から来たのではなく, 銀行口座のオブジェクトとしての認識から来たのである. われわれは, 例えば分子だけ変えてもまだ「同じ」有理数だといわないように, 有理数が自己同一性を持った変化するものとは思っていない.

命令型プログラムの落し穴
関数型プログラミングと反対に, 代入を多用するプログラミングは, 命令型プログラミング(imperative programming)という. 計算モデルの複雑性を高める上に, 命令型の流儀で書いたプログラムには, 関数型プログラムには起り得ぬ虫を入れ易い. 例えば1.2.1節の反復的な階乗プログラムを考えよう:
(define (factorial n)
  (define (iter product counter)
    (if (> counter n)
        product
        (iter (* counter product)
              (+ counter 1))))
  (iter 1 1))
内部の反復ループで引数を渡す代りに, 変数productcounterの値を更新するのに明示的代入を使い, より命令型の流儀にすることが出来る:

(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の解を修正してよい.

問題 3.8


1.1.3節で評価モデルを定義した時, 式の評価の第一歩はその部分式を評価することだといった. しかし部分式を評価する順(例えば左から右か右から左か) は規定しなかった. 代入を取り入れると, 手続きへの引数が評価される順は結果に違いを生じるかも知れない. 単純な手続きfを定義し, (+ (f 0) (f 1))が, +の引数を左から右へ評価すると0を返し, 右から左へ評価すると1を返すようにせよ.



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)はこの議論を公表する.) また別に初心者にとり, 一歩一歩の代入は, 手続き呼出しより, 視覚化が容易だという見方を反映しているらしい. 理由はともあれ, プログラミングを複雑にし, 重要な点を見え難くする「この変数の設定をあの前にすべきか, 後にすべきか」問題は初心者プログラマに, しばしば負担をかけている.

[ 目次, 前節, 次節, 索引 ]