今回はオブジェクト指向の簡単な例題として、ベクタを自動的に拡張する「可変長ベクタ (variable length vector) 」を作ってみましょう。Scheme のベクタは生成するときに大きさを指定しますが、その後で大きさを変更することはできません。可変長ベクタはデータを追加するとき、満杯の場合は自動的にベクタを拡張します。可変長ベクタ (配列) は Perl, Python, Ruby などでは標準でサポートされている機能です。
クラス名は <vlvector> とします。今回は簡単な例題ということで、ベクタの末尾にデータを追加するとき、ベクタが満杯ならば容量を 2 倍に増やすことにしましょう。ただし、ベクタの大きさを自動的に減らすことはしません。そのかわり、ベクタの大きさを変更するメソッドを用意することにします。
可変長ベクタクラス <vlvector> のメソッドを表 1 に示します。
メソッド | 機能 |
---|---|
vlvector-ref v n | n 番目の要素を参照する |
vlvector-set! v n x | n 番目の要素を x に書き換える |
vlvector-push! v x | ベクタの末尾にデータ x を追加する |
vlvector-pop! v | ベクタの末尾からデータを取り出す |
vlvector-clear! v | ベクタを空にする |
vlvector-empty? v | ベクタが空ならば #t を返す |
vlvector-length v | ベクタの要素数を返す |
vlvector-size v | ベクタの大きさ (容量) を返す |
vlvector-resize v size | ベクタの大きさを size に変更する |
fill-pointer v | フィルポインタの値を返す |
fill-pointer-set! v n | フィルポインタの値を n に書き換える |
引数 v はクラス <vlvector> のインスタンスで、引数 n と size は整数値です。プログラムのポイントはフィルポインタ (fill-pointer) の使い方です。ベクタの中で 0 番目から fill-pointer - 1 番目までの要素を有効なデータとして扱います。つまり、fill-pointer でデータの要素数を管理するわけです。
vlvector-push! で末尾にデータを追加する場合、fill-pointer の位置にデータを書き込み、fill-pointer の値を +1 します。vlvector-pop! で末尾からデータを取り出す場合、fill-pointer の値を -1 してから、fill-pointer の位置にある要素を返します。これはスタックの動作と同じになります。
データを追加するとき、fill-pointer の値とベクタの大きさを比較します。同じ値の場合、ベクタは満杯なので容量を 2 倍に増やします。Scheme の場合、ベクタの大きさを変更することはできないので、新しいベクタを生成して元のベクタからデータをコピーします。
容量の変更はメソッド vlvector-resize で行います。ベクタを縮小することもできますが、fill-pointer よりも小さくすることはできません。
fill-pointer の値はメソッド fill-pointer-set! で変更することができます。範囲は 0 からベクタの大きさまでです。値が範囲外の場合はエラーを送出します。
あとは、クラス <sequence> を Mix-in します。<sequence> のメソッドはベクタの 0 番目から fill-pointer - 1 番目までの要素に対して働きますが、メソッド vlvector-ref と vlvector-set! はベクタ全体にアクセスできるものとします。なお、これらの仕様は Common Lisp の「配列 (array) 」を参考にしました。
それではプログラムを作りましょう。最初にクラスを定義します。
リスト 1 : 可変長ベクタクラスの定義 ; メタクラス (define-class <vlvector-meta> (<class>) ()) ; クラス (define-class <vlvector> (<sequence>) ((buff :accessor get-buff :init-keyword :buff) (size :accessor get-size :init-value *min-size* :init-keyword :size) (nums :accessor get-nums :init-value 0)) :metaclass <vlvector-meta>)
まず、<sequence> で必要になるメタクラス <vlvector-meta> を定義します。それから、クラス <vlvector> で <sequence> を Mix-in します。スロット buff にベクタを、size にベクタの大きさを、nums に要素数を格納します。この nums が fill-pointer になります。最後に :metaclass に <vlvector-meta> を指定します。
スロット buff の初期化は総称関数 initialize で行います。次のリストを見てください。
リスト 2 : インスタンスの初期化 ; 初期化 (define-method initialize ((obj <vlvector>) initargs) (next-method) (if (< (get-size obj) *min-size*) (set! (get-size obj) *min-size*)) (set! (get-buff obj) (make-vector (get-size obj))))
まず next-method でスーパークラスの initialize を呼び出します。それから、スロット size の値をチェックします。もしも、size がグローバル変数 *min-size* (4) よりも小さい場合は、その値を *min-size* に書き換えます。あとは大きさ size のベクタを make-vector で生成して buff にセットするだけです。
次はフィルポインタを操作するメソッドを作ります。
リスト 3 : フィルポインタの操作 ; フィルポインタを求める (define-method fill-pointer ((v <vlvector>)) (get-nums v)) ; フィルポインタの変更 (define-method fill-pointer-set! ((v <vlvector>) (n <integer>)) (if (or (negative? n) (< (get-size v) n)) (error "fill-pointer-set! : out of range")) (set! (get-nums v) n)) ; 汎変数 (set! (setter fill-pointer) fill-pointer-set!)
メソッド fill-pointer は簡単です。メソッド get-nums でスロット nums の値を求めるだけです。メソッド fill-pointer-set! はスロット num の値を引数 n に書き換えるだけです。このとき、n の値が範囲内 (0 から size まで) であることを確認します。範囲外であればエラーを送出します。
フィルポインタは set! で変更できると便利です。Gauche では汎変数を簡単に定義することができます。これには setter を使います。
setter proc
関数 setter は引数 proc の setter 手続きを返します。Gauche の場合、setter 手続きは値を更新する関数のことです。たとえば、setter 手続きを update とすると、update は (proc args ...) が参照した値を次の形式で更新します。
update args ... value
簡単な例を示しましょう。
gosh> (setter car) #<subr set-car!> gosh> (setter vector-ref) #<subr vector-set!> gosh> (setter list-ref) #f gosh> (define a '(1 2 3 4 5)) a gosh> (set! (car a) 10) #<undef> gosh> a (10 2 3 4 5)
car の setter 手続きは set-car! なので、(set! (car a) 10) は (set-car! a 10) に変換されて、リストの先頭要素が 10 に変更されます。また、vector-ref の setter 手続きは vector-set! ですが、list-ref の setter 手続きは定義されていません。
setter 手続きの設定にも汎変数が使えるので簡単です。(set! (setter proc) update) とすれば、proc の setter 手続きを update に設定することができます。fill-pointer の場合も、(set! (setter fill-pointer) fill-pointer-set!) とすれば、fill-pointer の setter 手続きを fill-pointer-set! にすることができます。
次はベクタの大きさを変更するメソッド vlvector-resize を作ります。
リスト 4 : ベクタサイズの変更 (define-method vlvector-resize ((v <vlvector>) (new-size <integer>)) (if (< new-size (get-nums v)) (error "vlvector-resize : not enough new-size")) (if (< new-size *min-size*) (set! new-size *min-size*)) (let ((new-buff (make-vector new-size))) (let loop ((i (- (get-nums v) 1))) (cond ((<= 0 i) (vector-set! new-buff i (vector-ref (get-buff v) i)) (loop (- i 1))))) (set! (get-buff v) new-buff) (set! (get-size v) new-size)))
引数 new-size が新しいベクタの大きさです。new-size が要素数 nums よりも小さい場合はエラーを送出します。new-size が *min-size* よりも小さい場合は、new-size を *min-size* に変更します。あとは、大きさ new-size の新しいベクタを生成して、そこに buff からデータをコピーするだけです。
次はメソッド vlvector-push! と vlvector-pop! を作ります。
リスト 5 : データの追加と取り出し ; 追加 (define-method vlvector-push! ((v <vlvector>) value) (if (= (get-size v) (get-nums v)) (vlvector-resize v (* (get-size v) 2))) (vector-set! (get-buff v) (get-nums v) value) (inc! (get-nums v))) ; 取り出し (define-method vlvector-pop! ((v <vlvector>)) (if (vlvector-empty? v) (error "vlvector-pop! : <vlvector> is empty")) (dec! (get-nums v)) (vector-ref (get-buff v) (get-nums v)))
vlvector-push! は簡単です。まず、ベクタが満杯かチェックし、そうであれば vlvector-resize を呼び出してベクタの大きさを 2 倍に拡張します。そして、nums の位置にデータ value をセットしてから nums の値を +1 します。
vlvector-pop! は vevector-empty? を呼び出してベクタが空かチェックし、空の場合はエラーを送出します。そして、nums の値を -1 してから、その位置にある要素を取り出して返します。
次は <sequence> で使うイテレータとビルダーを作ります。
リスト 6 : <sequence> 用メソッド ; イテレータ (define-method call-with-iterator ((v <vlvector>) proc . opts) (let ((n (get-keyword :start opts 0))) (proc (lambda () (eqv? n (get-nums v))) (lambda () (if (eqv? n (get-nums v)) #f (begin0 (vector-ref (get-buff v) n) (inc! n))))))) ; ビルダー (define-method call-with-builder ((class <vlvector-meta>) proc . opts) (let ((v (make <vlvector>))) (proc (lambda (val) (vlvector-push! v val)) (lambda () v)))) ; 参照 (define-method referencer ((avec <vlvector>)) vlvector-ref) ; 更新 (define-method modifier ((avec <vlvector>)) vlvector-set!) ; オーバーライド (define-method size-of ((v <vlvector>)) (vlvector-length v)) (define-method lazy-size-of ((v <vlvector>)) (vlvector-length v))
ベクタの有効範囲は 0 番目から nums - 1 番目までです。call-with-iterator は変数 n の位置にある要素を取り出して返すだけなので簡単です。call-with-builder も make で <vlvector> のインスタンスを生成して、vlvector-push! で要素を追加していくだけです。
<vlvector> の要素数は簡単に求めることができるので、メソッド size-of と lazy-size-of をオーバーライドして、vlvector-length の値を返すようにします。デフォルトの動作は、call-with-iterator を呼び出してコレクションの大きさを求めています。
後のメソッドは簡単なので説明は割愛いたします。詳細はプログラムリスト2をお読みください。
それでは簡単な実行例を示します。
gosh> (use vlvector) #<undef> gosh> (use gauche.sequence) #<undef> gosh> (define a (make <vlvector>)) a gosh> (vlvector-length a) 0 gosh> (vlvector-size a) 4 gosh> (vlvector-empty? a) #t gosh> (dotimes (x 10) (vlvector-push! a x)) #t gosh> (vlvector-length a) 10 gosh> (vlvector-size a) 16 gosh> (vlvector-empty? a) #f gosh> (for-each (lambda (x) (format #t "~D " x)) a) 0 1 2 3 4 5 6 7 8 9 #t gosh> (map (lambda (x) (* x x)) a) (0 1 4 9 16 25 36 49 64 81) gosh> (define b (map-to <vlvector> (lambda (x) (* x x)) a)) b gosh> (for-each (lambda (x) (format #t "~D " x)) b) 0 1 4 9 16 25 36 49 64 81 #t gosh> (size-of b) 10 gosh> (dotimes (x 10) (format #t "~D " (vlvector-pop! b))) 81 64 49 36 25 16 9 4 1 0 #t gosh> (size-of b) 0 gosh> (vlvector-empty? b) #t gosh> (set! (fill-pointer b) 10) #<undef> gosh> (size-of b) 10 gosh> (vlvector-empty? b) #f gosh> (dotimes (x 10) (format #t "~D " (vlvector-pop! b))) 81 64 49 36 25 16 9 4 1 0 #t
正常に動作しているようです。興味のある方はいろいろ試してみてください。
; ; vlvector.scm : 可変長ベクタクラス (variable-length vector) ; ; Copyright (C) 2010 Makoto Hiroi ; (define-module vlvector (use gauche.sequence) (export <vlvector> vlvector-ref vlvector-set! vlvector-push! vlvector-pop! vlvector-empty? vlvector-clear! vlvector-length vlvector-size fill-pointer fill-pointer-set! vlvector-resize call-with-iterator call-with-builder referencer modifier)) (select-module vlvector) ;;; 定数 (define *min-size* 4) ;;; クラス定義 ; メタクラス (define-class <vlvector-meta> (<class>) ()) ; クラス (define-class <vlvector> (<sequence>) ((buff :accessor get-buff :init-keyword :buff) (size :accessor get-size :init-value *min-size* :init-keyword :size) (nums :accessor get-nums :init-value 0)) :metaclass <vlvector-meta>) ; 初期化 (define-method initialize ((obj <vlvector>) initargs) (next-method) (if (< (get-size obj) *min-size*) (set! (get-size obj) *min-size*)) (set! (get-buff obj) (make-vector (get-size obj)))) ;;; メソッドの定義 ; 空にする (define-method vlvector-clear! ((v <vlvector>)) (set! (get-nums v) 0)) ; 空か? (define-method vlvector-empty? ((v <vlvector>)) (zero? (get-nums v))) ; フィルポインタを求める (define-method fill-pointer ((v <vlvector>)) (get-nums v)) ; フィルポインタの変更 (define-method fill-pointer-set! ((v <vlvector>) (n <integer>)) (if (or (negative? n) (< (get-size v) n)) (error "fill-pointer-set! : out of range")) (set! (get-nums v) n)) ; 汎変数 (set! (setter fill-pointer) fill-pointer-set!) ; 要素数を求める (define-method vlvector-length ((v <vlvector>)) (get-nums v)) ; 大きさを求める (define-method vlvector-size ((v <vlvector>)) (get-size v)) ; 参照 (ベクタ全体にアクセスできる) (define-method vlvector-ref ((v <vlvector>) (n <integer>)) (vector-ref (get-buff v) n)) ; 更新 (ベクタ全体にアクセスできる) (define-method vlvector-set! ((v <vlvector>) (n <integer>) value) (vector-set! (get-buff v) n value)) ; 大きさの変更 (define-method vlvector-resize ((v <vlvector>) (new-size <integer>)) (if (< new-size (get-nums v)) (error "vlvector-resize : not enough new-size")) (if (< new-size *min-size*) (set! new-size *min-size*)) (let ((new-buff (make-vector new-size))) (let loop ((i (- (get-nums v) 1))) (cond ((<= 0 i) (vector-set! new-buff i (vector-ref (get-buff v) i)) (loop (- i 1))))) (set! (get-buff v) new-buff) (set! (get-size v) new-size))) ; 追加 (define-method vlvector-push! ((v <vlvector>) value) (if (= (get-size v) (get-nums v)) (vlvector-resize v (* (get-size v) 2))) (vector-set! (get-buff v) (get-nums v) value) (inc! (get-nums v))) ; 取り出し (define-method vlvector-pop! ((v <vlvector>)) (if (vlvector-empty? v) (error "vlvector-pop! : <vlvector> is empty")) (dec! (get-nums v)) (vector-ref (get-buff v) (get-nums v))) ;;; イテレータ (fill-pointer までが有効) (define-method call-with-iterator ((v <vlvector>) proc . opts) (let ((n (get-keyword :start opts 0))) (proc (lambda () (eqv? n (get-nums v))) (lambda () (if (eqv? n (get-nums v)) #f (begin0 (vector-ref (get-buff v) n) (inc! n))))))) ; ビルダー (define-method call-with-builder ((class <vlvector-meta>) proc . opts) (let ((avec (make <vlvector>))) (proc (lambda (val) (vlvector-push! avec val)) (lambda () avec)))) ; 参照 (define-method referencer ((avec <vlvector>)) vlvector-ref) ; 更新 (define-method modifier ((avec <vlvector>)) vlvector-set!) ; オーバーライド (define-method size-of ((v <vlvector>)) (vlvector-length v)) (define-method lazy-size-of ((v <vlvector>)) (vlvector-length v)) (provide "vlvector")