増減できるカウンタの作成

久しぶりにscheme自体で遊んでみました。ふと、オブジェクトの状態に対するアクセサを多値で返すという風にしていくとどうなるだろうと思ったのでやってみました。

クロージャの利用

よくクロージャの実例としてカウンタの作成が引き合いにだされます。
例えば以下のような感じですね。

(define (counter$ i)
  (lambda () (inc! i 1)))

(define c (counter$ 10))
(c) ; => 11
(c) ; => 12
(c) ; => 13

ここではcounter$で引数として渡したiを閉じ込めたクロージャを返しています。返されたクロージャの束縛を実行すると、それがあたかもカウンタのように動作します。これがクラスからオブジェクトを生成することに似ていると言われたりします。

ただ、一方でクロージャが返す束縛が持つ機能は値の増加だけです。通常のカウンタなら増加だけでなく他の機能も持たせたいところです。例えばこのような機能などです。

  • 値の減少(inc!に対するdec!のような)
  • 値の再設定

メッセージパッシング方式

複数の機能を持たせる時には、メッセージパッシングを利用するという表現で以下のようなコードが示されることが多かったりします。

(define (counter$ i)
  (lambda (message . args)
    (case message
      [(show) i]
      [(up) (inc! i)]
      [(down) (dec! i)]
      [(set) (let1 v (car args) (set! i v))]
      [else (errorf "COUNTER: method `~a' is not found" message)])))

(define c (counter$ 10))
(c 'show) ; => 10
(c 'up) ; => 11
(c 'down) ; => 10

(c 'set -1) ; => -1
(c 'up) ; => 0

;; (c 'foo) ; => *** ERROR: COUNTER: method  foo is not found
;; Stack Trace:
;; _______________________________________

確かに(c 'up)などすると値が増加し、downで減少させることができてますね。cは複数の機能を持っています。

マクロによるラッピング

例えば、このメッセージパッシングスタイルをより簡便に使うためにマクロでラッピングするという方法を説明したりします。あまり綺麗な構文ではないですが、例えば以下のようにdefine-objectというマクロを定義したりします。

(define-syntax define-object
  (syntax-rules ()
    [(_ :name name :params params :args args method-clause ...)
     (define name
       (lambda params
         (lambda (message . args)
           (case message
             method-clause ...
             [else (errorf "method `~a' is not found" message)]))))]))

(define-object
  :name counter$ 
  :params (i) 
  :args  args
  [(up) (inc! i)]
  [(down) (dec! i)]
  [(set) (let1 v (car args) (set! i v))])

(define c (counter$ 10)) ; => c
(c 'set 100) ; => 100
(c 'up) ; => 101
                    

define-objectのおかげで生成されたクロージャが何をするものなのかわかりやすくなっています。ただ、処理の大本は上のものと何ら変わりがありません。

ふと考えてみると...

このようにメッセージパッシングにしなければいけない理由としては、クロージャが返す値が単一でしかないという制約があるからでした。例えば、冒頭で作成したcounter$のクロージャを返す機能は、set,up,downのどれが良いのでしょう?どれかひとつに選ぶことができません。

今まではこのあたりで思考が止まっていたんですが、よく考えればschemeには多値があるわけです。それなら全ての機能を多値で返してしまえば良いのではないでしょうか?というわけで実験です。

多値によるカウンタの作成(set,up,downを持つ)

(define (counter$ i)
  (values (lambda () (inc! i))
          (lambda () (dec! i))
          (lambda (v) (set! i v))))

(define-values (up down set) (counter$ 10))
(set 10) ; => 10
(up) ; => 11
(up) ; => 12
(down) ; => 11

当然、多値により返された手続きはcounter$に渡した引数を共有しているのですから、それらを用いて状態に触れることが可能ですね。今まで気づきませんでした。

もちろん、複数の機能を返せればどうやっても良いわけですから、alistで返したりハッシュテーブルで返したりしてみても良いのかもしれません。(結局、ディスパッチの処理を書かなければならないところが一緒なのですけれど)多値で返す方法が他と違う唯一のところは、メソッド呼び出し時にディスパッチのコストがかからないという点かもしれません。
あー、多値の方は新しいメソッドを追加する方法がないですね。

多値によるカウンタの作成(メソッドを追加する方法を考えてみる)

新しいメソッドを追加するメソッドを用意してみてはどうでしょう?多値の2番目の値がメソッドを追加するメソッドを目指して作った手続きです。

(define (counter$ i)
  (values (lambda () (inc! i))
          (lambda (fn) (lambda args (apply fn i args)))))

(define-values (up add$) (counter$ 10))
(up) ; => 15
(define down (add$ (lambda (x) (dec! x))))
(down) ; => 14
(down) ; => 14

ダメですね。add$を利用して作ったdownは大本のcounter$の中にあるiの束縛とは違う束縛を参照しています。add$で作成した際のiと等しい値を持つ別の束縛xを持っているだけに過ぎないみたいです。