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

3.3.5 制約の拡散



計算機プログラムは伝統的に, 前もって指定した引数に演算を実行し望みの出力を生じるという一方向計算になっていた. 他方, われわれは多くの量の間の関係を使ってシステムをモデル化することもよくやる. 例えば機械構造の数学的モデルを見ると, 金属棒のたわみdは, 棒に加わる力F, 棒の長さL, 横断面積Aおよび弾性係数Eについて方程式

d A E = F L

の関係があるという情報があったりする. こういう方程式は一方向性ではない. 任意の四つの量があれば, それを使って五番目が計算出来る. しかし方程式を伝統的な計算機言語に翻訳しようとすると, 他の四つを使って計算する量を一つ選ばなければならない. 断面積Aを計算するプログラムは, Aの計算もdの計算も同じ方程式から出たのに, たわみdの計算には使えない.31

   本節では関係そのものを使って仕事が出来るような言語の設計を眺めよう. この言語の基本要素は, 量の間にはある関係が成り立つことを述べる 基本制約(primitive constraints)である. 例えば(adder a b c) は, 量a, bおよびcは方程式a + b = cで関係づけられていることを規定し, (multiplier x y z)は制約xy = zを表し, (constant 3.14 x)xの値は3.14でなければならないといっている.

   われわれの言語は, 基本制約を組み合せ, 更に複雑な関係を表す手段を提供する. 制約は 制約ネットワーク(constraint networks)を構成して組み合せる. 制約は コネクタ(connectors)で接続する. コネクタは一つか複数の制約に関る値を「保持する」オブジェクトである. 例えばカ氏[Fahrenheit]の温度Fとセ氏[Celsius]の温度Cの間には

9C = 5(F - 32)

の関係があることを知っている. こういう関係は, 基本加算器, 乗算器と定数の制約から出来ているネットワークと考えることが出来る(図3.28). この図の左には, m1, m2およびpでラベルした三つの端子を持つ乗算器があり, それらは乗算器を次のようにネットワークの他の部分へ接続している: m1端子は, セ氏の温度を保持するコネクタCに結合されている. m2端子はコネクタwに結合され, それはまた9を保持する定数箱に結合されている. 乗算箱がm1とm2の積だと制約を与えているp端子は, もう一つの乗算箱のp端子に結合され, そちらのm2は定数5に, m1は和の一つの項へ繋いである.



図3.28 制約ネットワークで表した関係9C = 5(F - 32)

   こういうネットワークの計算は次のように進行する: (利用者または, それに結合している制約箱により)コネクタに値が与えられると, (自分を起した制約以外の)関係あるすべての制約を起し, 値を持ったことを知らせる. 起された各制約箱は, コネクタを見回し, コネクタの値を決めるのに十分な情報があるかを見る. そうであれば, 箱はコネクタを設定し, そのコネクタは, 関係する制約をすべて起す. これを繰り返す. 例えばセ氏とカ氏の間の変換では, w, xおよびyは, それぞれ定数箱9, 5および32により直ちに設定される. これらのコネクタは乗算器と加算器を起すが, それらは進むには十分な情報がないことを知る. 利用者(またはネットワークの他の部分)がCをある値(例えば25)に設定すると, 左端の乗算器は起され, uを25 ... 9 = 225に設定する. するとuは二番目の乗算器を起すが, それはvを45に設定し, vは加算器を起し, それはFを77に設定する.

制約システムの使い方
上で説明した温度の計算をするために制約システムを使うには, まず構成子make-connectorを呼び出して二つのコネクタCFを作り出し, CFを適当なネットワークに結合する:
(define C (make-connector))
(define F (make-connector))
(celsius-fahrenheit-converter C F)
ok
ネットワークを作り出す手続きは次のとおり:

(define (celsius-fahrenheit-converter c f)
  (let ((u (make-connector))
        (v (make-connector))
        (w (make-connector))
        (x (make-connector))
        (y (make-connector)))
    (multiplier c w u)
    (multiplier v x u)
    (adder v y f)
    (constant 9 w)
    (constant 5 x)
    (constant 32 y)
    'ok))
この手続きが内部のコネクタu, v, w, xおよびyを作り出し, それらを基本制約の構成子adder, multiplierおよびconstantを使って図3.28に示すように結合する. 3.3.4節のディジタル回路シミュレータでのように, 基本要素の組合せを手続きを使って表すと, 言語に自動的に合成オブジェクトを抽象化する手段を提供する.

   ネットワークの動作を見るため, 3.3.4節で回線を監視するのに使ったのと似たprobe手続きを使い, コネクタCFにプローブを置く. コネクタにプローブを置くと, コネクタに値が与えられる度にメッセージが印字される:

(probe "Celsius temp" C)
(probe "Fahrenheit temp" F)
次にCの値を25に設定する. (set-value!の第三引数はこの指令がuserから来たことを告げる.)
(set-value! C 25 'user)
Probe: Celsius temp = 25
Probe: Fahrenheit temp = 77
done
Cのプローブは起きて値を報告する. Cはまた上に述べたように, その値をネットワーク中に拡散させる. こうしてFが77に設定され, Fのプローブが報告する.

   ところでFを新しい値, 例えば212に設定して見ることが出来る.

(set-value! F 212 'user)
Error! Contradiction (77 212)
コネクタは矛盾を検出したと文句をいう: その値は77で, だれかがそれを212 に設定しようとした. 本当にネットワークの再利用したければCに昔の値を忘れるようにいう:
(forget-value! C 'user)
Probe: Celsius temp = ?
Probe: Fahrenheit temp = ?
done
Cは元の値を設定したuserがその値を取り除こうとしているのを知り, Cはプローブが示すようにその値を失うことに同意し, この事実をネットワークの残りに知らせる. この情報はそのうちFまで拡散し, 自分の値が77であると信じ続ける理由がなくなったことを知る. そこでFはプローブの示すようにその値を捨てる.

   今やFには値がないので, それを212に設定するのは自由だ:

(set-value! F 212 'user)
Probe: Fahrenheit temp = 212
Probe: Celsius temp = 100
done
新しい値はネットワークを拡散し, Cに100の値をとらせ, それがCのプローブに記録される. 全く同じネットワークがFからCを計算するのにもCからFを計算するのにも使えることに注意しよう. 計算のこの無方向性が制約に基づいたシステムの著しい特徴である.
制約システムの実装
制約システムは, 3.3.4節のディジタル回路シミュレータと非常に似た方法で, 局所状態を持つ手続きオブジェクトにより実装してある. 制約システムの基本オブジェクトはいくらか複雑だが, 次第書きや論理遅延の心配がないため, システム全体はずっと単純だ.

   コネクタの基本演算は次の通り:

(has-value? connector)
コネクタが値を持つかどうかを告げる.

(get-value connector)
コネクタの現在の値を返す.

(set-value! connector⟩  ⟨new-value⟩  ⟨informant)
通知者はコネクタに新しい値を設定するよう要求していることを示す.

(forget-value! connector⟩  ⟨retractor)
撤回者がその値を忘れるように要求していることをコネクタに告げる.

(connect connector⟩  ⟨new-constraint)
コネクタに新しい制約に関るよう告げる.

   コネクタは, コネクタが値を持つことを制約に告げる手続き inform-about-valueと, コネクタは値を持たないと制約に告げるinform-about-no-valueを使い, 制約と通信する.

   addera1a2の被加算コネクタとsumコネクタの間に加算制約を構成する. 加算器は局所状態(次の手続きme)を持つ手続きとして実装される:


(define (adder a1 a2 sum)
  (define (process-new-value)
    (cond ((and (has-value? a1) (has-value? a2))
           (set-value! sum
                       (+ (get-value a1) (get-value a2))
                       me))
          ((and (has-value? a1) (has-value? sum))
           (set-value! a2
                       (- (get-value sum) (get-value a1))
                       me))
          ((and (has-value? a2) (has-value? sum))
           (set-value! a1
                       (- (get-value sum) (get-value a2))
                       me))))
  (define (process-forget-value)
    (forget-value! sum me)
    (forget-value! a1 me)
    (forget-value! a2 me)
    (process-new-value))
  (define (me request)
    (cond ((eq? request 'I-have-a-value)  
           (process-new-value))
          ((eq? request 'I-lost-my-value) 
           (process-forget-value))
          (else 
           (error "Unknown request -- ADDER" request))))
  (connect a1 me)
  (connect a2 me)
  (connect sum me)
  me)
adderは新しい加算器を指示されたコネクタへ繋ぎ, それを値として返す. 手続きmeは加算器を表し, 局所手続きへの振分けとして働く. 次の「構文インターフェース」(3.3.4節の脚注27参照)は, 振分けと一緒に使う:

(define (inform-about-value constraint)
  (constraint 'I-have-a-value))


(define (inform-about-no-value constraint)
  (constraint 'I-lost-my-value))
加算器の局所手続きprocess-new-valueは, 加算器のコネクタの一つが値を持ったと知らされた時に呼び出される. 加算器はまずa1a2の両方が値を持つか見る. そうであればsumに値を二つの被加数の和に設定するようにいう. set- value!informant引数はmeで, それは加算器オブジェクトそのものである. a1a2の両方が値を持っていなければ, 加算器はa1sumが値を持つことがあるか見る. そうであればa2を両者の差に設定する. 最後にa2sumが値を持てば, 加算器はa1を設定するのに十分な情報を持つ. 加算器がコネクタの一つが値を失ったと知れば, そのコネクタのすべてに値を失うよう要求する. (この加算器が設定した値だけが実際は失われる.) 次にprocess-new-valueを走らせる. この最後のステップの理由は, いくつかのコネクタはまだ値を持っているかも知れず(つまり, コネクタは, この加算器が元々設定したのではない値を持っていたかも知れず,) これらの値は加算器を通して拡散し戻す必要があるかも知れないからだ.

   乗算器も加算器に非常に似ている. 因子のいずれかが0なら, もう一方の因子が分らなくてもproductを0に設定する.


(define (multiplier m1 m2 product)
  (define (process-new-value)
    (cond ((or (and (has-value? m1) (= (get-value m1) 0))
               (and (has-value? m2) (= (get-value m2) 0)))
           (set-value! product 0 me))
          ((and (has-value? m1) (has-value? m2))
           (set-value! product
                       (* (get-value m1) (get-value m2))
                       me))
          ((and (has-value? product) (has-value? m1))
           (set-value! m2
                       (/ (get-value product) (get-value m1))
                       me))
          ((and (has-value? product) (has-value? m2))
           (set-value! m1
                       (/ (get-value product) (get-value m2))
                       me))))
  (define (process-forget-value)
    (forget-value! product me)
    (forget-value! m1 me)
    (forget-value! m2 me)
    (process-new-value))
  (define (me request)
    (cond ((eq? request 'I-have-a-value)
           (process-new-value))
          ((eq? request 'I-lost-my-value)
           (process-forget-value))
          (else
           (error "Unknown request -- MULTIPLIER" request))))
  (connect m1 me)
  (connect m2 me)
  (connect product me)
  me)
constant構成子は指示されたコネクタの値を設定するだけである. 定数箱に送られたI-have-a-valueI-lost-my- valueメッセージはエラーとなる.

(define (constant value connector)
  (define (me request)
    (error "Unknown request -- CONSTANT" request))
  (connect connector me)
  (set-value! connector value me)
  me)
最後にプローブは指示されたコネクタの設定や設定解除についてメッセージを印字する:

(define (probe name connector)
  (define (print-probe value)
    (newline)
    (display "Probe: ")
    (display name)
    (display " = ")
    (display value))
  (define (process-new-value)
    (print-probe (get-value connector)))
  (define (process-forget-value)
    (print-probe "?"))
  (define (me request)
    (cond ((eq? request 'I-have-a-value)
           (process-new-value))
          ((eq? request 'I-lost-my-value)
           (process-forget-value))
          (else
           (error "Unknown request -- PROBE" request))))
  (connect connector me)
  me)
コネクタの表現
コネクタは, コネクタの現在の値であるvalue, コネクタの値を設定したオブジェクトのinformantおよびコネクタが関る制約のリストのconstraintsという局所状態変数を持つ手続きオブジェクトで表現する.

(define (make-connector)
  (let ((value false) (informant false) (constraints '()))
    (define (set-my-value newval setter)
      (cond ((not (has-value? me))
             (set! value newval)
             (set! informant setter)
             (for-each-except setter
                              inform-about-value
                              constraints))
            ((not (= value newval))
             (error "Contradiction" (list value newval)))
            (else 'ignored)))
    (define (forget-my-value retractor)
      (if (eq? retractor informant)
          (begin (set! informant false)
                 (for-each-except retractor
                                  inform-about-no-value
                                  constraints))
          'ignored))
    (define (connect new-constraint)
      (if (not (memq new-constraint constraints))
          (set! constraints 
                (cons new-constraint constraints)))
      (if (has-value? me)
          (inform-about-value new-constraint))
      'done)
    (define (me request)
      (cond ((eq? request 'has-value?)
             (if informant true false))
            ((eq? request 'value) value)
            ((eq? request 'set-value!) set-my-value)
            ((eq? request 'forget) forget-my-value)
            ((eq? request 'connect) connect)
            (else (error "Unknown operation -- CONNECTOR"
                         request))))
    me))

   コネクタの局所手続きset-my-valueはコネクタの値を設定する要求がある時に呼び出される. 現在コネクタが値を持っていなければ, その値を設定し, informantとしてその値の設定を要求した制約を記憶する. 32 次にコネクタは, 値の設定を要求した制約を除き, すべての関りのある制約に知らせる. これは 指示した手続きを, 与えられたものを除き, リストのすべての項目に作用させる, 次の反復子を使って達成する:


(define (for-each-except exception procedure list)
  (define (loop items)
    (cond ((null? items) 'done)
          ((eq? (car items) exception) (loop (cdr items)))
          (else (procedure (car items))
                (loop (cdr items)))))
  (loop list))

   コネクタが値を忘れるよう頼まれると, 局所手続きforget-my-valueを呼び出す. この手続きはまず要求が確かに値を元々設定したのと同じオブジェクトから来ているかを調べる. そうであればコネクタは関りある制約に, 値のなくなったことを知らせる.

   局所手続きconnectは, リストにまだ存在していなければ指示された新しい制約を制約のリストに追加する. 次にコネクタが値を持てば, 新しい制約にこの事実を知らせる.

   コネクタの手続きmeは, 他の内部手続きへの振分けとして役立ち, またコネクタをオブジェクトとして表現する. 次の手続きは振分けの構文インターフェースを提供する:


(define (has-value? connector)
  (connector 'has-value?))


(define (get-value connector)
  (connector 'value))


(define (set-value! connector new-value informant)
  ((connector 'set-value!) new-value informant))


(define (forget-value! connector retractor)
  ((connector 'forget) retractor))


(define (connect connector new-constraint)
  ((connector 'connect) new-constraint))

問題 3.33


基本乗算, 加算および定数の制約を使い, 入力として三つのコネクタa, bおよびcをとり, cの値がabの値の平均であるような制約を達成する手続き averagerを定義せよ.

問題 3.34


Louis Reasonerは, 二つの端子を持ち, 二番目の端子のコネクタbが常に最初の端子aの二乗であるような制約装置である平方器を作ろうとした. 乗算器で作った単純な次の装置を考案した:
(define (squarer a b)
  (multiplier a a b))
この考えには重要な欠点がある. 説明せよ.

問題 3.35


Ben BitdiddleはLouisに, 問題3.34のトラブルを避ける一つの方法は平方器を新しい基本制約として定義することだという. そういう制約を実装する手続きのBenの下書きで欠けている部分を埋めよ:
(define (squarer a b)
  (define (process-new-value)
    (if (has-value? b)
        (if (< (get-value b) 0)
            (error "square less than 0 -- SQUARER" (get-value b))
            ⟨代替部1⟩)
        ⟨代替部2⟩))
  (define (process-forget-value) ⟨本体1⟩)
  (define (me request) ⟨本体2⟩)
  ⟨定義の残り⟩
  me)


問題 3.36


次の一連の式を大域環境で評価するとしよう:
(define a (make-connector))
(define b (make-connector))
(set-value! a 10 'user)
set-value!の評価中の時点で, コネクタの局所手続きの中の次の式が評価された:
(for-each-except setter inform-about-value constraints)
上の式が評価される環境を示す環境図を描け.

問題 3.37


celsius-fahrenheit-converter手続きは,

(define (celsius-fahrenheit-converter x)
  (c+ (c* (c/ (cv 9) (cv 5))
          x)
      (cv 32)))

(define C (make-connector))
(define F (celsius-fahrenheit-converter C))
のような, より式主導の定義の形と比べると, 煩わしい. ここでc+, c*などは算術演算の「制約」版である. 例えばc+は引数として二つのコネクタをとり, これらと加算制約で関係づけられたコネクタを返す:
(define (c+ x y)
  (let ((z (make-connector)))
    (adder x y z)
    z))
上の変換器の例のように合成制約の定義に使えるよう, 似たような手続きc-, c*, c/およびcv(定数値)を定義せよ.33



31 制約拡散は Ivan Sutherland(1963)の信じ難いほどの先見性を持った SKETCHPADに初めて現れた. Smalltalk言語に基づいた美しい制約拡散システムは Xerox Palo Alto研究所で Alan Borning(1977)が開発した. Sussman, Stallmanと Steeleは制約拡散を電気回路解析に応用した(Sussman and Stallman 1975; Sussman and Steele 1980). TK!Solver (Konopasek and Jayaraman 1984)は制約に基づいた強力なモデリング環境である.

32 setterは制約でないかも知れない. 温度の例では, usersetterに使った.

33 式主導の形式は計算の中間の式に名前をつける必要がないから便利である. 多くの言語が合成データを扱おうとすると煩わしくなるように, 制約言語の初めの形式化も煩わしい. 例えば, 変数がベクタを表すとして, 積(a + b) ... (c + d)を計算する時, 指示したベクタの値を設定するが, 値としてはベクタを返さない手続きを使い, 「命令的流儀で」仕事をすることが出来る:

(v-sum a b temp1)
(v-sum c d temp2)
(v-proc temp1 temp2 answer)

一方値としてベクタを返す手続きを使って式を扱い, temp1temp2といちいちいわないで済むこともある.

(define answer (v-prod (v-sum a b) (v-sum c d)))

Lispは手続きの値として合成オブジェクトを返すことが出来るので, 命令形流儀の制約言語をこの問題のような式主導の流儀に変換出来る. Algol, Basic, Pascal(Pascalのポインタ変数を使わない時)のような, 合成オブジェクトの扱いの貧弱な言語では, 合成オブジェクトを扱おうとすると, よく命令形の流儀にはまってしまう. 式主導の書式の利点が使えれば, この節でやったように, システムを命令形の流儀で実装しなければならない理由があるか問うかも知れない. 一つの理由は, 非式主導の制約言語には, コネクタオブジェクトの他に, 制約オブジェクトのハンドル(例えばadder手続きの値)があることだ. このことはコネクタの演算を通して間接的にではなく, 制約と直接通信する新しい演算でシステムを拡張しようと思えば, 有用である. 命令形の実装を使って式主導の流儀の実装をするのは容易だが, その逆は極めて困難である.

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