Island Life

< 誤差が生じるとき | Self-callの書き換え >

2016/06/17

スロットの初期化をチェックする

Gaucheのオブジェクトシステムでは、オブジェクトの作成時にスロットを初期化しないで おくことができる。初期化されないスロットはunboundという状態になる。次の例では、 スロットaには初期値 (:init-valueもしくは:init-form)が 設定されていないので、make:a で初期値を渡さない限り、 作られるオブジェクトのaスロットはunboundになる。

gosh> (define-class A ()
        ((a :init-keyword :a) 
         (b :init-keyword :b :init-value 1)))
A
gosh> (make A)
#<A 0xb280e0>
gosh> ,d
#<A 0xb280e0> is an instance of class A
slots:
  a         : #<unbound>
  b         : 1

describeでは #<unbound> と表示されているが、#<unbound> という 値が入っているわけではない。unboundはあくまでスロットの状態であって、 値を取り出そうとするとエラーになる。

gosh> (~ (make A)'a)
*** ERROR: slot a of object of class #<class A> is unbound
Stack Trace:

この仕様はCLOSに準じたものだ。

unbound slotは、インスタンス初期化中に、複数のメソッド間で情報をやりとりするのに使える。 スロットの初期化(makeに渡された初期化キーワードの処理と、:init-value:init-formの処理) はベースクラスの initializeメソッドで処理されるので、自分のinitializeメソッド中で スーパークラスのメソッドを呼んだ後にスロットがboundかどうかチェックすることで、 独自の初期化処理を書ける。特に、既に初期化された他のスロットに依存する計算を初期化時に行える。 次の例では、上で定義したクラスAについて、初期化キーワード:amakeに渡されなかった場合に、bスロットの値に基づいた初期化を行う。

gosh> (define-method initialize ((obj A) initargs)
        (next-method)
        (unless (slot-bound? obj 'a)
          (set! (~ obj 'a) (+ (~ obj 'b) 1))))
#<generic initialize (18)>
gosh> (make A :b 10)
#<A 0xb655c0>
gosh> ,d
#<A 0xb655c0> is an instance of class A
slots:
  a         : 11
  b         : 10
gosh> (make A :a 1 :b 10)
#<A 0xb72c60>
gosh> ,d
#<A 0xb72c60> is an instance of class A
slots:
  a         : 1
  b         : 10

現実のプログラムでのunboundスロットの使いどころってほぼこれくらいで、 unbound slotを抱えたままのインスタンスをずっと持ち歩いて嬉しいことというのは 多分ほとんどない。unboundスロットへのアクセスはメソッドslot-unboundで インターセプトできるので、 それを利用したトリックはあり得るけど、特殊な用途と言えるだろう。 むしろ、初期化キーワードを間違えていたりして 初期化したつもりがされてない、なんてバグの方に足を取られることがある。 (関連して、makeに渡す初期化キーワードに無効なものがあったらエラーに できないか、という話もあるのだけれど、初期化キーワードの解釈は allocateinitializeメソッドの定義次第でどうにでもできるので 機械的にはじくのは難しい)。

それなら、初期化が済んだ時点でスロットをチェックしてunboundなものをはじいて しまったらどうか。MOPを使ってmakeにメソッドをつければ 全ての初期化が終わった後のタイミングで検査ができる。

(define-class <ensure-slot-initialization-meta> (<class>) ())

(define-method make ((class <ensure-slot-initialization-meta>) . args)
  (rlet1 instance (next-method)
    (dolist [s (class-slots class)]
      (unless (slot-bound? instance (slot-definition-name s))
        (errorf "Slot ~s of object ~s is not initialized"
                (slot-definition-name s) instance)))))

こんな感じ。

gosh> (define-class B () 
        ((a :init-keyword :a)
         (b :init-keyword :b :init-value 1))
        :metaclass <ensure-slot-initialization-meta>)
B
gosh> (make B :a 1 :b 10)
#<B 0xbb4ef0>
gosh> ,d
#<B 0xbb4ef0> is an instance of class B
slots:
  a         : 1
  b         : 10
gosh> (make B)
*** ERROR: Slot a of object #<B 0xbbcc20> is not initialized
Stack Trace:

これなら初期化キーワードを間違えたケースも(それによって意図したスロットが unboundになりさえすれば)捕まえられる。

さて、<ensure-slot-initialization-meta>は MOPライブラリにしようと思って書いたのだけれど、 かなり本質的な機能のような気がするし、むしろ標準の<class>:disallow-unbound-slot みたいなオプションをつけてしまう方が 良いような気がしてきた。どうしようかなあ。

Tags: Programming, Gauche

Post a comment

Name: