プログラミング再入門

プログラミングをもう一度ちゃんと勉強する読書ノート

SICP 3 Modularity, Objects, and State

漸く第3章に到達。既にかなりお腹いっぱいな感じですが、これから更に面白くなって行く事に期待。

ノート

モジュラリティは部品(モジュール)としての独立性とかその度合い。物理系をモデル化したシステムを構築するひとつの有効な方法は、モデル化した構造をそのままプログラムの構造として作り込む事。目指すべき所はシステムを拡張する時に限られた範囲の変更だけで済む状態にする事。この章ではオブジェクトとストリーム、遅延評価を扱う。

3.1 Assignment and Local State

代入とlocal stateは日本語にすると局所状態?
オブジェクトの振る舞いがその経緯によって左右される場合、そのオブジェクトは「状態を持っている」と言う。
各々のオブジェクトがそれぞれの局所状態変数(local state variable)を持つ。状態変数の状態を変える為には代入演算子が必要となる。

3.1.1 Local State Variables

局所状態変数。
時間とともに変化する状態モデルの例として銀行口座の残高を取り上げる。
ここまでに出て来た手続きは基本的には同じ引数に対しては同じ結果が変える様に出来ていた。一部の例外を除いて。
withdrawは整数か文字列を返す。CやJavaでは扱えない事もないが普通はやらない手法。
set!とbeginを導入。set!は代入、beginはブロックを形成して中身を順番に実行する。最後の式の値がbegin全体の値となる。
最初の例をDrRacket上で動かす(racketモード)

> (define balance 100)
> (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
> (withdraw 25)
75
> (withdraw 25)
50
> (withdraw 60)
"Insufficient funds"
> (withdraw 15)
35
> 

これではbalanceはどこからでもアクセス出来てしまう。balanceを局所変数にしながら、関数から戻っても永続的に持続させる為にはクロージャを使う。
C++だとコンストラクタとoperator ()だけを定義したクラスが近いが、インスタンスとしてnew-withdrawしか作れない点が異なる。

> (define new-withdraw
    (let ((balance 100))
      (lambda (amount)
        (if (>= balance amount)
            (begin (set! balance (- balance amount))
                   balance)
            "Insufficient funds"))))
> (new-withdraw 25)
75
> (new-withdraw 25)
50
> (new-withdraw 60)
"Insufficient funds"
> (new-withdraw 15)
35
> 

局所変数とset!の組み合わせを使うと1.1.5節の置換えモデル(substitution model)では説明出来なくなる。置き換えモデルとは恐らく手続きを呼び出している部分にその定義本体を展開する様に解釈する(のだと思う)が、new-withdrawを呼び出す毎にその定義をその場所に展開していたら呼び出し毎に一旦balanceは100に戻る事になってしまう。

次のmake-withdrawはletを使った局所変数の代わりに仮引数を局所変数の代わりに使っている。CやJavaでも(?)使えるテクニック。またmake-withdrawはnew-withdrawとちがって手続きとして定義されていて何度でも呼び出せるので、先ほどのoperator()だけを実装したクラスに相当する。make-withdrawを呼び出す事はコンストラクタを呼び出す事と同じ。ただしインスタンス変数に相当する部分が実はmake-withdrawの引数なのでちょっと変ではある。

> (define (make-withdraw balance)
    (lambda (amount)
      (if (>= balance amount)
          (begin (set! balance (- balance amount))
                 balance)
          "Insufficient funds")))
> (define W1 (make-withdraw 100))
> (define W2 (make-withdraw 100))
> (W1 50)
50
> (W2 70)
30
> (W2 40)
"Insufficient funds"
> (W1 40)
10
> 

make-withdrawでは単に呼び出すとそれはwithdraw(引き下ろし)を意味していたが、make-accountではwithdrawとdeposit(入金)も出来る様に、どちらを呼び出すのかを決定する手続きdispatchを返している。予備知識がないと正確に理解するのは難しいが、関数内部のdefineは単にそこに定義が書いてあるだけではなく、defineが解釈された時点の環境も含めた手続きとしてオブジェクトを形成する。なのでmake-accountの結果として返される手続きdispatchはmake-accountを呼び出した時のbalanceを含めた環境を伴っている。この点がC等の関数ポインタとは大きく異なる。

メソッドwithdrawとdipositを呼び出す為にはdispatch(make-accountを呼び出した結果返って来る関数*1)にメッセージとしてシンボルで'withdrawか'dipositを渡す。Smalltalkの「メッセージとメソッドは別」と言う話に繋がっているのかも知れない。ここではdispatchがwithdrawやdipositを呼び出すのではなく、withdrawはdipositの関数*2を返し、dipositを呼び出した側にwithdrawやdipositを直接呼んで貰う形を取っている。

> (define (make-account balance)
    (define (withdraw amount)
      (if (>= balance amount)
          (begin (set! balance (- balance amount))
                 balance)
          "Insufficient funds"))
    (define (deposit amount)
      (set! balance (+ balance amount))
      balance)
    (define (dispatch m)
      (cond ((eq? m 'withdraw) withdraw)
            ((eq? m 'deposit) deposit)
            (else (error "Unknown request -- MAKE-ACCOUNT"
                         m))))
    dispatch)
> (define acc (make-account 100))
> ((acc 'withdraw) 50)
50
> ((acc 'withdraw) 60)
"Insufficient funds"
> ((acc 'deposit) 40)
90
> ((acc 'withdraw) 60)
30
> 
Exercise 3.1

make-accumulatorの引数が初期値(initial value)である事を明確にする為と状態変数を明示的に作る為に、letを使った方式にした。言語のモードは取り敢えずデフォルトのracketのままとする。

#lang racket
(define (make-accumulator initial-value)
  (let ((ammount initial-value))
    (lambda (value-to-add)
      (set! ammount (+ ammount value-to-add))
      ammount)))

動作確認

> (define A (make-accumulator 5))
> (A 10)
15
> (A 10)
25
> (define B (make-accumulator 5))
> (B 5)
10
> (A -5)
20
> (B 3)
13
> 
Exercise 3.2

直接lambdaとも書けるが、問題文にmfと言う名前が出て来ているのでdefineで名前をつけた。

#lang racket
(define (make-monitored f)
  (let ((counter 0))
    (define (mf arg)
      (cond ((eq? arg 'how-many-calls?) counter)
            ((eq? arg 'reset-count) (begin (set! counter 0)
                                           counter))
            (else
             (begin
               (set! counter (+ 1 counter))
               (f arg)))))
    mf))

動作確認

> (define s (make-monitored sqrt))
> (s 100)
10
> (s 'how-many-calls?)
1
> (s 400)
20
> (s 'how-many-calls?)
2
> (s 5)
2.23606797749979
> (s 'how-many-calls?)
3
> (s 'reset-count)
0
> (s 'how-many-calls?)
0
> (s 2)
1.4142135623730951
> (s 'how-many-calls?)
1
> 
Exercise 3.3

make-accountはerrorを呼んでる為、ここではracketモードである必要がある。またwithdrawの様にエラーのケースで単に文字列を返してしまうと文字列を手続きとして呼び出してしまいおかしなエラーになってしまうので。。。
敢えて実際にやってみると

> (define acc (make-account 100 'secret-password))
> ((acc 'some-other-password 'deposit) 50)
. . application: not a procedure;
 expected a procedure that can be applied to arguments
  given: "Incorrect password"
  arguments...:
   50
> 

解決策としてerrorで実行を止める版。

#lang racket
(define (make-account balance saved-password)

  (define (dispatch password m)
    (if (eq? password saved-password)
        (cond ((eq? m 'withdraw) withdraw)
              ((eq? m 'deposit) deposit)
              (else (error "Unknown request -- MAKE-ACCOUNT"
                           m)))
        (error "Incorrect password")))
  dispatch)

動作確認

> (define acc (make-account 100 'secret-password))
> ((acc 'secret-password 'withdraw) 40)
60
> ((acc 'some-other-password 'deposit) 50)
. . Incorrect password
> 

でも、これでは問題文の出力と異なるので、呼び出されても"Incorrect password"とだけ返して何もしない関数を返す事にした。

#lang racket
(define (make-account balance saved-password)

  (define (incorrect-password amount)
    "Incorrect password")
  (define (dispatch password m)
    (if (eq? password saved-password)
        (cond ((eq? m 'withdraw) withdraw)
              ((eq? m 'deposit) deposit)
              (else (error "Unknown request -- MAKE-ACCOUNT"
                           m)))
        incorrect-password))
  dispatch)

動作確認

> (define acc (make-account 100 'secret-password))
> ((acc 'secret-password 'withdraw) 40)
60
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> 
Exercise 3.4

call-the-copsを呼び出すと面白い事が出来ると良いのだが、ここでは呼び出した事を確認出来る様にするのみ。

#lang racket
(define (make-account balance saved-password)
  (let ((incorrect-password-count 0))
    (define (withdraw amount)
      (if (>= balance amount)
          (begin (set! balance (- balance amount))
                 balance)
          "Insufficient funds"))
    (define (deposit amount)
      (set! balance (+ balance amount))
      balance)
    (define (incorrect-password amount)
      "Incorrect password")
    (define (call-the-cops)
      (error "Please wait for a moment..."))
    (define (dispatch password m)
      (if (eq? password saved-password)
          (begin
            (set! incorrect-password-count 0)
            (cond ((eq? m 'withdraw) withdraw)
                  ((eq? m 'deposit) deposit)
                  (else (error "Unknown request -- MAKE-ACCOUNT"
                               m))))
          (begin
            (set! incorrect-password-count (+ 1 incorrect-password-count))
            (if (>= incorrect-password-count 7)
                (call-the-cops)
                incorrect-password))))
    dispatch))

動作確認

> (define acc (make-account 100 'secret-password))
> ((acc 'secret-password 'withdraw) 40)
60
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'secret-password 'deposit) 50)
110
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
"Incorrect password"
> ((acc 'some-other-password 'deposit) 50)
. . Please wait for a moment...
> 

7回目までに正しいパスワードを入れればカウンターはリセットされて、7回連続で間違っているとエラーになる。

*1:C++的には関数ポインタとも捉えられるが本来関数オブジェクトの方が近いか

*2:これも関数オブジェクト