M.Hiroi's Home Page

お気楽 Scheme プログラミング入門

オブジェクト指向編 : 可変長ベクタ

Copyright (C) 2010 Makoto Hiroi
All rights reserved.

はじめに

今回はオブジェクト指向の簡単な例題として、ベクタを自動的に拡張する「可変長ベクタ (variable length vector) 」を作ってみましょう。Scheme のベクタは生成するときに大きさを指定しますが、その後で大きさを変更することはできません。可変長ベクタはデータを追加するとき、満杯の場合は自動的にベクタを拡張します。可変長ベクタ (配列) は Perl, Python, Ruby などでは標準でサポートされている機能です。

●可変長ベクタの仕様

クラス名は <vlvector> とします。今回は簡単な例題ということで、ベクタの末尾にデータを追加するとき、ベクタが満杯ならば容量を 2 倍に増やすことにしましょう。ただし、ベクタの大きさを自動的に減らすことはしません。そのかわり、ベクタの大きさを変更するメソッドを用意することにします。

可変長ベクタクラス <vlvector> のメソッドを表 1 に示します。

表 1 : 可変長ベクタクラスのメソッド
メソッド機能
vlvector-ref v nn 番目の要素を参照する
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

正常に動作しているようです。興味のある方はいろいろ試してみてください。


●プログラムリスト2

;
; 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")

初版 2010 年 4 月 17 日