制約の拡散(問題3-33〜37)

「3.3.5 制約の拡散」の節は、ディジタル回路のような「遅延」は絡んでいないものの、未定義コネクタに値をセットした後、その値が制約に従って伝播していく様子などに見られる、コネクタや制約の仕組みが面白いと感じました。
問題3-33は、averagerを定義する問題です。3つの未定義コネクタの内、2つのコネクタに値がセットされれば、最後の1つの値も算出されるような制約です。

(define (averager a b c)
  (let ((u (make-connector))
        (v (make-connector)))
    (adder a b u)
    (multiplier u v c)
    (constant 0.5 v)
    'ok))

(define A (make-connector))
(define B (make-connector))
(define C (make-connector))
(averager A B C)

(probe "A temp" A)
(probe "B temp" B)
(probe "ave temp" C)

(set-value! A 30 'takuya)
=>Probe: A temp = 30

(set-value! B 25 'takuya)
=>Probe: B temp = 25
=>Probe: ave temp = 27.5

(forget-value! B 'takuya)
=>Probe: B temp = ?
=>Probe: ave temp = ?

(set-value! C 20 'takuya)
=>Probe: ave temp = 20
=>Probe: B temp = 10.0

問題3-34と35では、squarerを定義します。Louisの実装だと、二乗を求める場合は上手くいきますが、平方根を求める場合が上手くいきません。multiplierに渡される3つのコネクタの内、2つのコネクタに値がセットされなければ、全てのコネクタに値が伝播することはありませんが、Louisは(multiplier a a b)とやっていて、bだけに値をセットしても、2つあるaに値が伝播しないことになります(そもそも、平方根を求めるというロジックそのものが、どこにも定義されていません)。そのため、Benが指摘しているような実装を施す必要があります。

(define (squarer a b)
  (define (process-new-value)
    (if (has-value? b)
        (if (< (get-value b) 0)
            (error "square less than 0" (get-value b))
            (set-value! a
                        (sqrt (get-value b))
                        me))
        (if (has-value? a)
            (set-value! b
                        (square (get-value a))
                        me))))
  (define (process-forget-value)
    (forget-value! a me)
    (forget-value! b 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" request))))
  (connect a me)
  (connect b me)
  me)

この制約を使って、円の半径と円の面積の関係をネットワーク化してみます。

(define (circle h a)
  (let ((p (make-connector))
        (x (make-connector)))
    (squarer h x)
    (multiplier x p a)
    (constant 3.14 p)
    'ok))

(define h (make-connector))
(define a (make-connector))
(circle h a)

(probe "h" h)
(probe "a" a)

(set-value! h 2 'takuya)
=>Probe: h = 2
=>Probe: a = 12.56

(forget-value! h 'takuya)
=>Probe: h = ?
=>Probe: a = ?

(set-value! a 314 'takuya)
=>Probe: a = 314
=>Probe: h = 10.000000000139897

問題3-36は、あるコネクタに値がセットされた時、その情報がどのように伝播するのかを理解するための問題です。adderとかmultiplierとかの制約自体は、その制約に関わっているコネクタ自身が情報を持っています(make-connector手続き内のconstraintsリスト)。for-each-exceptが呼び出されると、そのコネクタの値を変更したsetter(ユーザー自身の時もあれば、他の制約の場合もある)、値が変わったことを伝えるためのinform-about-value手続きと、このコネクタが保持している個々の制約が渡され、処理が開始されます。それぞれの制約に対してinform-about-valueを適用しますが、この処理が起動するきっかけとなったsetter制約については、処理を飛ばしています。このようにすることで、無限ループに陥ることなく、つながっているコネクタへ変更内容が伝播される仕組みになっているようです。
問題3-37は、calsius-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))

これを上手く動かすためには、以下のような補助手続きが必要です。

(define (c+ x y)
  (let ((z (make-connector)))
    (adder x y z)
    z))

(define (c- z y)
  (let ((x (make-connector)))
    (adder x y z)
    x))

(define (c* x y)
  (let ((z (make-connector)))
    (multiplier x y z)
    z))

(define (c/ z y)
  (let ((x (make-connector)))
    (multiplier x y z)
    x))

(define (cv value)
  (let ((x (make-connector)))
    (constant value x)
    x))
(probe "Celsius temp" C)
(probe "Fahrenheit temp" F)

(set-value! C 25 'takuya)
=>Probe: Celsius temp = 25
=>Probe: Fahrenheit temp = 77

(forget-value! C 'takuya)
=>Probe: Celsius temp = ?
=>Probe: Fahrenheit temp = ?

(set-value! F 212 'takuya)
=>Probe: Fahrenheit temp = 212
=>Probe: Celsius temp = 100

こちらの方がシンプルでわかりやすいような気がします。また、注釈33にも非常に興味深い指摘がありました。まだまだ理解できていないので、後でじっくり読んでみたいと思っています。