前回は双方向リストクラス <dlist> に <sequence> を Mix-in しました。プログラム (dlist.scm) の中には、内部でしか使わない作業用の関数など、外部から呼び出されると困る関数やメソッドなどがあります。また、Gauche や CLOS の場合、スロットやメソッドのアクセス権を設定する機能はありません。必要な関数 (メソッド) や変数 (シンボル) だけを外部に公開し、内部で使用するものを隠蔽する機能があると便利です。
このような場合、Gauche では「モジュール (module) 」を利用することができます。モジュールを簡単に説明すると、ある機能を実現するためのプログラムの集まりや構造のことです。たとえば双方向リストの場合、クラス (データ構造) の定義と基本的な関数 (メソッド) が複数ありますが、それらをひとつにまとめてモジュールとして考えることができます。今回はモジュールの基本的な使い方について簡単に説明します。
プログラムを作っていると、ほかのプログラムで作った関数が利用できるのではないか、といった場面に出会うことがあります。このような場合、自分で作成した関数をライブラリとしてまとめておくと、簡単に再利用することができて便利です。もともと、このような場合に使われる機能がモジュールです。Gauche のライブラリはモジュールを使って整理されています。
ライブラリの作成で問題になるのが「名前の衝突」です。複数のライブラリを使うときに、ライブラリ内部で使用する関数や変数に同じ名前のものが存在すると、そのライブラリは正常に動作しないでしょう。この名前の衝突を避けるためにモジュールを使います。次の例を見てください。
gosh> (current-module) #<module user> gosh> (define a 10) a gosh> a 10 gosh> (define-module foo) #<undef> gosh> (select-module foo) #<undef> gosh> a *** ERROR: unbound variable: a gosh> (define a 20) a gosh> a 20 gosh> (select-module user) #<undef> gosh> a 10
特殊形式 current-module は現時点で使用しているモジュール (カレントモジュール) を返します。Gauche を起動すると、カレントモジュールは user というあらかじめ用意されているモジュールになります。REPL で定義する変数や関数は user に登録されます。したがって、最初 a に 10 を代入しましたが、この変数はモジュール user 内に定義されます。
define-module はモジュールを定義する特殊形式です。あとで詳しく説明しますが、ここでは foo という名前のモジュールを定義しています。select-module はカレントモジュールを切り替える特殊形式です。Gauche はカレントモジュールの中からグローバル変数の値を探します。これ以降、REPL で定義されるグローバル変数はモジュール foo での値になります。
カレントモジュールを fooに切り替えた場合、グローバル変数 a はまだ定義されていないので値は未束縛です。次に、変数 a の値を 20 にセットします。この値はモジュール foo でのみ有効で、カレントモジュールを user に切り替えると、グローバル変数 a の値は 10 になります。このように、モジュールによってグローバル変数 a の値は区別されるのです。
define-module はモジュールを定義するだけではなく、その中で S 式を評価することもできます。
define-module module-name body ...
body はそのモジュールの中で評価されるので、その結果がグローバル変数を定義するものであれば、その値はそのモジュールに属することになります。モジュールで定義されているグローバル変数の値は、select-module でモジュールを切り替えなくても特殊形式 with-module を使って求めることができます。
with-module module-name body ...
with-module はカレントモジュールを module-name に切り替えて body を順番に評価します。そして、最後に評価した S 式の結果を返します。
簡単な例を示しましょう。
gosh> (define-module foo (define a 10)) a gosh> a *** ERROR: unbound variable: a gosh> (with-module foo a) 10 gosh> (with-module foo (+ 10 a)) 20
モジュール foo でグローバル変数 a を定義しました。モジュール user では定義されていません。with-module でモジュール foo を指定して S 式を評価すると、そこで定義されている変数 a の値にアクセスすることができます。
もちろん、モジュールで関数を定義して呼び出すこともできます。
gosh> (define-module bar (define (baz) (print "bar baz!"))) baz gosh> (with-module bar (baz)) bar baz! #<undef>
このように、with-module を使うと他のモジュールで定義された変数や関数にアクセスすることができますが、公開したくない関数や変数までアクセスが可能になってしまいます。通常は、次に説明する import と export を使います。
Gauche には、ほかのモジュールで定義された関数や変数を取り込む機能「インポート (import) 」が用意されています。ただし、このためにはモジュール側でも関数や変数を公開するための準備が必要です。これを「エキスポート (export) 」といいます。
ここで、Gauche の中では同じ名前のシンボルは一つしかないことに注意してください。たとえば、モジュール user のシンボル a とモジュール foo のシンボル a は同一のものなので、eq? で比較すると #t になります。
gosh> (define a 10) a gosh> a 10 gosh> (define-module foo (define a 20)) a gosh> (with-module foo a) 20 gosh> (eq? 'a (with-module foo 'a)) #t
Gauche のモジュールは、そこに属する新しいシンボルを作成するのではありません。モジュールはシンボルに束縛されたグローバルな値を保持する環境、と考えるとわかりやすいと思います。モジュールを切り替えると環境も切り替わるので、同じシンボルであってもモジュールによってグローバル変数の値が異なるわけです。
Gauche の場合、exprot と import は特殊形式として定義されています。
export symbol ... import module-name ...
export はモジュール内の symbol を公開します。import はモジュール module-name の export で公開された symbol をカレントモジュールに受け入れます。つまり、module-name の中で定義された symbol の束縛 (グローバル変数の値) が見えるようになります。なお、import の引数は複数のモジュールを指定することができます。これで、export されたグローバル変数の値にアクセスすることができます。
簡単な例を示しましょう。
gosh> (define-module foo (export a) (define a 10)) a gosh> (define-module bar (export b) (define b 20)) b gosh> a *** ERROR: unbound variable: a gosh> (import foo) #<undef> gosh> a 10 gosh> b *** ERROR: unbound variable: b gosh> (import bar) #<undef> gosh> b 20
モジュール foo はグローバル変数 a を export とし、bar はグローバル変数 b を export としています。カレントモジュール user で、グローバル変数 a は未束縛ですが、foo を import とすると foo での束縛が見えるようになるので、a の値は 10 になります。同様に、bar を import するとグローバル変数 b の値は 20 になります。
Gauche の場合、通常はひとつのファイルでひとつのモジュールを定義します。ひとつのモジュールを複数のファイルに分割することもできますが、よほど大きなプログラムでなければ、複数のファイルに分割する必要はないと思います。Gauche の場合、モジュール名とファイル名は同じにしておきます。たとえば、モジュール名が foo であればファイル名は foo.scm となります。
モジュールの骨格をリスト 1 に示します。
リスト 1 : モジュールの骨格 (define-module module-name (use ...) (export ...)) (select-module module-name) ・・・モジュール本体・・・ (provide "module-name")
最初に define-module でモジュールを定義します。ここで、使用するモジュールを use などでロードし、公開する変数や関数 (メソッド) を export で指定します。define-module の中でモジュール本体を記述することもできますが、Gauche のユーザリファレンスでは非推薦となっています。
次に、select-module でカレントモジュールを module-name に切り替えて、モジュール本体を記述します。そして、最後に provide で module-name を文字列で指定します。
provide module-name
provide は use でモジュールをロードするために必要となります。provide で指定されたモジュールは一度ロードされると、それ以降 use や require などで再ロードされることはありません。
モジュールのロードは、プログラムファイルのロードと同じです。Gauche の場合、load や require でプログラムをロードすることができますが、import の処理は行われません。マクロ use を使うと、自動的に import の処理も行ってくれるので便利です。use はグローバル変数 *load-path* に格納されているディレクトリからモジュールを探すので、作成したモジュールはそこに置いておくか、*load-path* にモジュールがあるディレクトリのパスを追加してください。
なお、モジュールをロードするとき、モジュール内でカレントモジュールを切り替えますが、ロードしたあとカレントモジュールは元の状態に戻ります。モジュール内でカレントモジュールを元に戻す操作は必要ありません。
簡単な例題として、前回作成した双方向リスト dlist.scm をモジュールにしたものを プログラムリスト に示します。プログラムの修正は簡単なので説明は割愛いたします。興味のある方は実際にいろいろ試してみてください。
; ; dlist.scm : 双方向リスト (モジュール版) ; ; Copyright (C) 2010 Makoto Hiroi ; (define-module dlist (use gauche.sequence) (export <dlist> make-dlist dlist? dlist-ref dlist-set! dlist-insert! dlist-delete! dlist-fold dlist-for-each dlist-length dlist-clear dlist-empty? list->dlist dlist->list call-with-iterator call-with-builder referencer modifier )) (select-module dlist) ; コレクション用メタクラス (define-class <dlist-meta> (<class>) ()) ; セルの定義 (define-class <cell> () ((item :accessor cell-item :init-value #f :init-keyword :item) (prev :accessor cell-prev :init-value #f :init-keyword :prev) (next :accessor cell-next :init-value #f :init-keyword :next))) ; 空リストを作る (define (make-empty) (let ((cp (make <cell>))) (set! (cell-prev cp) cp) (set! (cell-next cp) cp) cp)) ; 双方向リストの定義 (define-class <dlist> (<sequence>) ((top :accessor dlist-top :init-form (make-empty))) :metaclass <dlist-meta>) ; 双方リストの生成 (define (make-dlist) (make <dlist>)) ; 双方向リスト? (define (dlist? x) (eq? (class-of x) <dlist>)) ; n 番目のセルを返す (作業用関数) (define (cell-nth d n next) (let loop ((i -1) (cp (dlist-top d))) (cond ((and (<= 0 i) (eq? (dlist-top d) cp)) (error "cell-nth --- oops!")) ((= n i) cp) (else (loop (+ i 1) (next cp)))))) ; 参照 (define-method dlist-ref ((d <dlist>) (n <integer>)) (cell-item (if (negative? n) (cell-nth d (abs (+ n 1)) cell-prev) (cell-nth d n cell-next)))) ; 書き換え (define-method dlist-set! ((d <dlist>) (n <integer>) value) (set! (cell-item (if (negative? n) (cell-nth d (abs (+ n 1)) cell-prev) (cell-nth d n cell-next))) value)) ; 挿入 (define-method dlist-insert! ((d <dlist>) (n <integer>) value) (define (cell-insert! n next prev) (let* ((p (cell-nth d (- n 1) next)) (q (next p)) (cp (make <cell> :item value))) (set! (next cp) q) (set! (prev cp) p) (set! (prev q) cp) (set! (next p) cp))) ; (if (negative? n) (cell-insert! (abs (+ n 1)) cell-prev cell-next) (cell-insert! n cell-next cell-prev))) ; 削除 (define-method dlist-delete! ((d <dlist>) (n <integer>)) (define (cell-delete! n next prev) (let* ((cp (cell-nth d n next)) (p (prev cp)) (q (next cp))) (set! (next p) q) (set! (prev q) p) (cell-item cp))) ; (if (negative? n) (cell-delete! (abs (+ n 1)) cell-prev cell-next) (cell-delete! n cell-next cell-prev))) ; 畳み込み (define-method dlist-fold ((d <dlist>) func init . args) (let ((next (if (get-keyword :from-end args #f) cell-prev cell-next))) (let loop ((cp (next (dlist-top d))) (a init)) (if (eq? cp (dlist-top d)) a (loop (next cp) (if (eq? next cell-prev) (func (cell-item cp) a) (func a (cell-item cp)))))))) ; サイズ (define-method dlist-length ((d <dlist>)) (dlist-fold d (lambda (x y) (+ x 1)) 0)) ; クリア (define-method dlist-clear ((d <dlist>)) (let ((cp (dlist-top d))) (set! (cell-next cp) cp) (set! (cell-prev cp) cp))) ; 空リストか? (define-method dlist-empty? ((d <dlist>)) (let ((cp (dlist-top d))) (eq? cp (cell-next cp)))) ; 変換 (define-method list->dlist ((xs <list>)) (let ((d (make <dlist>))) (for-each (lambda (x) (dlist-insert! d -1 x)) xs) d)) ; (define-method dlist->list ((d <dlist>)) (dlist-fold d (lambda (x y) (cons x y)) '() :from-end #t)) ; 巡回 (define-method dlist-for-each ((d <dlist>) func . opts) (if (get-keyword :from-end opts #f) (dlist-fold d (lambda (x y) (func x)) #f :from-end #t) (dlist-fold d (lambda (x y) (func y)) #f))) ; <collection> 用 : イテレータ (define-method call-with-iterator ((coll <dlist>) proc . opts) (let ((cp (cell-nth coll (get-keyword :start opts 0) cell-next))) (proc (lambda () (eq? cp (dlist-top coll))) (lambda () (if (eq? cp (dlist-top coll)) #f (begin0 (cell-item cp) (set! cp (cell-next cp)))))))) ; ビルダー (define-method call-with-builder ((class <dlist-meta>) proc . opts) (let ((dlist (make <dlist>))) (proc (lambda (val) (dlist-insert! dlist -1 val)) (lambda () dlist)))) ;;; <sequence> 用 (define-method referencer ((dlist <dlist>)) dlist-ref) (define-method modifier ((dlist <dlist>)) dlist-set!) (provide "dlist")
Gauche の場合、インスタンス中のスロットは同じクラスのインスタンスでも別々のメモリ領域に割り当てられます。たとえば、クラス <foo> にスロット a, b がある場合、make でインスタンス x1, x2 を生成すると、x1 と x2 のスロット a, b は異なるメモリ領域に割り当てられます。Gauche や CLOS では、これを「局所スロット」といいます。オブジェクト指向プログラミングの場合、インスタンスは個々のオブジェクトを表しているので、スロットがインスタンスごとに別々のメモリ領域に割り当てられるのは当然といえるでしょう。
ところが、プログラムによっては、同じクラスのインスタンスで共通の変数や定数を使いたい場合があります。つまり、インスタンスごとにスロットを用意するのではなく、クラス単位でスロットを用意するのです。Gauche や CLOS では、これを「共有スロット」といいます。他のオブジェクト指向言語では「クラス変数」に相当する機能です。今回は共有スロットについて説明します。
共有スロットの設定は、スロットオプション :allocation にキーワード :class を指定します。:allocation の指定がない場合、もしくはキーワード :instance を指定すると、スロットは共有されず「局所スロット」になります。
簡単な例を示しましょう。次のリストを見てください。
リスト 1 : 共有スロットの定義 (define-class <foo> () ((a :accessor foo-a :init-keyword :a :allocation :class) (b :accessor foo-b :init-value 1 :init-keyword :b)))
クラス <foo> にはスロット a, b がありますが、スロット a が共有スロットで、スロット b が局所スロットになります。局所スロット b はインスタンスごとにメモリ領域が割り当てられますが、共有スロット a のメモリ領域はクラスでひとつしかありません。次の例を見てください。
gosh> (define x1 (make <foo> :a 0 :b 10)) x1 gosh> (define x2 (make <foo> :b 20)) x2 gosh> (foo-a x1) 0 gosh> (foo-b x1) 10 gosh> (foo-a x2) 0 gosh> (foo-b x2) 20 gosh> (set! (foo-a x1) 100) 100 gosh> (foo-a x1) 100 gosh> (foo-a x2) 100
最初に、インスタンス x1 を生成します。ここでスロット a を 0 に、b を 10 に初期化します。次にインスタンス x2 を生成し、スロット b を 20 に初期化します。スロット a は共有スロットなので、a の値は x1 を生成したときの値 0 になります。当然ですが、(foo-a x1) と (foo-a x2) は同じ値になり、(foo-b x1) と (foo-b x2) は異なる値になります。また、(setf (foo-a x1) 100) のように共有スロットの値を書き換えると、(foo-a x2) の値は書き換えた値 100 になります。このように、スロットが共有されていることがわかります。
また Guache の場合、共有スロットは次の関数を使うとインスタンスではなくクラスオブジェクトからでもアクセスすることができます。
class-slot-ref class slot-name class-slot-set! class slot-name obj class-slot-bound? class slot-name obj
簡単な使用例を示します。
gosh> (define-class <foo> () ((a :init-value 1 :allocation :class))) <foo> gosh> (class-slot-bound? <foo> 'a) #t gosh> (class-slot-ref <foo> 'a) 1 gosh> (class-slot-set! <foo> 'a 10) 10 gosh> (class-slot-ref <foo> 'a) 10
ところで、Gauche や CLOS の継承はスロットやメソッドだけではなく :allocation オプションも継承されることに注意してください。次のリストを見てください。
リスト 2 : 共有スロットの継承 (define-class <foo> () ((a :accessor foo-a :init-keyword :a :allocation :class) (b :accessor foo-b :init-value 1 :init-keyword :b))) (define-class <foo1> (<foo>) ((c :accessor foo-c :init-value 2 :init-keyword :c)))
クラス <foo> を継承してクラス <foo1> を定義します。<foo1> のスロットは a, b, c の 3 つになりますが、スロット a は共有スロットになります。このとき、スロット a は <foo1> だけはなく、<foo> と <foo1> の共有スロットになります。簡単な例を示しましょう。
gosh> (define x3 (make <foo1>)) x3 gosh> (foo-a x3) 100 gosh> (foo-b x3) 1 gosh> (foo-c x3) 2 gosh> (set! (foo-a x3) 200) 200 gosh> (foo-a x1) 200 gosh> (foo-a x2) 200 gosh> (foo-a x3) 200
クラス <foo> のインスタンスが変数 x1, x2 にセットされている状態で、クラス <foo1> のインスタンスを生成して変数 x3 にセットします。x3 のスロット a は共有スロットなので、x1, x2 と同じ値になります。スロット b, c は局所スロットなので、:init-value の値で初期化されます。ここで、(set! (foo-a x3) 200) とスロット a の値を 200 に書き換えると、(foo-a x1) と (foo-a x2) の値は 200 になります。スロット a はクラス <foo> と <foo1> で共有されていることがわかります。
それでは、サブクラスにスーパークラスと同じ名前の共有スロットを定義したらどうなるのでしょうか。次のリストを見てください。
リスト 3 : 同名の共有スロットがある場合 (define-class <foo> () ((a :accessor foo-a :init-keyword :a :allocation :class) (b :accessor foo-b :init-value 1 :init-keyword :b))) (define-class <foo2> (<foo>) ((a :accessor foo2-a :init-keyword :a :allocation :class) (c :accessor foo2-c :init-value 3 :init-keyword :c)))
クラス <foo> を継承してクラス <foo2> を定義します。<foo2> でも共有スロット a を定義していることに注意してください。この場合、<foo> のスロット a と <foo2> のスロット a は共有されません。つまり、<foo> のスロット a は <foo> の共有スロットであり、<foo2> のスロット a は <foo2> の共有スロットになるのです。次の例を見てください。
gosh> (define x1 (make <foo> :a 100)) x1 gosh> (define x2 (make <foo2> :a 200)) x2 gosh> (foo-a x1) 100 gosh> (foo2-a x2) 200 gosh> (foo-a x2) 200
<foo> のインスタンスを生成して変数 x1 にセットします。このとき、共有スロット a を 100 に初期化しています。次に <foo2> のインスタンスを生成して変数 x2 にセットします。共有スロット a は 200 に初期化されていることに注意してください。そして、インスタンス x1 と x2 のスロット a の値を求めるてみると 100 と 200 になります。
このように、<foo> と <foo2> のスロット a は共有されません。<foo2> のインスタンス中のスロット a は <foo2> の共有スロットであり、(foo-a x2) としても <foo> の共有スロット a にアクセスすることはできません。つまり、<foo> の共有スロット a はサブクラス <foo2> の共有スロット a に「隠蔽(シャドウ)」されるわけです。
今度は、スーパークラスの共有スロットと同じ名前の局所スロットがある場合を考えてみます。この場合、:allocation オプションはサブクラスの指定が優先されます。次のリストを見てください。
リスト 4 : 局所スロットと共有スロットの衝突 (1) (define-class <foo1> (<foo>) ((c :accessor foo-c :init-value 2 :init-keyword :c))) (define-class <foo3> (<foo>) ((a :accessor foo3-a :init-keyword :a :allocation :instance) (c :accessor foo3-c :init-value 3 :init-keyword :c)))
クラス <foo> を継承してクラス <foo3> を定義します。<foo> のスロット a は共有スロットですが、<foo3> のスロット a は局所スロットであることに注意してください。この場合、クラス <foo> のインスタンスのスロット a は共有スロットになりますが、クラス <foo3> のインスタンスのスロット a は局所スロットになります。簡単な例を示しましょう。
gosh> (define x1 (make <foo> :a 10 :b 20)) x1 gosh> (define x2 (make <foo> :b 30)) x2 gosh> (define y1 (make <foo3> :a 100 :c 200)) y1 gosh> (define y2 (make <foo3> :a 300 :c 400)) y2 gosh> (foo-a x1) 10 gosh> (foo-a x2) 10 gosh> (foo3-a y1) 100 gosh> (foo3-a y2) 300 gosh> (foo-a y1) 100 gosh> (foo-a y2) 300
クラス <foo> のインスタンスを生成して変数 x1, x2 にセットし、クラス <foo3> のインスタンスを生成して変数 y1, y2 にセットします。<foo> のスロット a は共有スロットなので、(foo-a x1) と (foo-a x2) は同じ値 (10) になります。ところが、<foo3> のスロット a は局所スロットになるので、make で指定した値に初期化されます。したがって、(foo-a y1) は 100 になり、(foo-a y2) は 300 になります。
逆に、スーパークラスのスロット a が局所スロットで、サブクラスのスロット a が共有スロットの場合、サブクラスのスロット a は共有スロットになります。次のリストを見てください。
リスト 5 : 局所スロットと共有スロットの衝突 (2) (define-class <bar> () ((a :accessor bar-a :init-value 0 :init-keyword :a) (b :accessor bar-b :init-value 1 :init-keyword :b))) (define-class <bar1> (<bar>) ((a :accessor bar1-a :init-keyword :a :allocation :class) (c :accessor bar1-c :init-value 2 :init-keyword :c)))
クラス <bar> のスロット a, b は局所スロットです。<bar> を継承してクラス <bar1> を定義します。このとき、スロット a を共有スロットとして定義します。<bar> のインスタンス中のスロット a は局所スロットになりますが、<bar1> のスロット a は共有スロットになります。簡単な実行例を示しましょう。
gosh> (define x1 (make <bar> :a 10 :b 20)) x1 gosh> (define x2 (make <bar> :a 30 :b 40)) x2 gosh> (define y1 (make <bar1> :a 100 :b 200 :c 300)) y1 gosh> (define y2 (make <bar1> :b 400 :c 500)) y2 gosh> (bar-a x1) 10 gosh> (bar-a x2) 30 gosh> (bar-a y1) 100 gosh> (bar-a y2) 100 gosh> (bar1-a y1) 100 gosh> (bar1-a y2) 100
クラス <bar> のインスタンス x1, x2 のスロット a は局所スロットで、<bar1> のインスタンス y1, y2 のスロット a は共有スロットになっていることがわかります。ようするに、スロットオプション :allocation の設定は「クラス優先順位リスト」に従って決定されるのです。これは多重継承でも同じです。次のリストを見てください。
リスト 6 : 局所スロットと共有スロットの衝突 (3) (define-class <baz1> () ((a :accessor baz1-a :init-keyword :a))) (define-class <baz2> () ((a :accessor baz2-a :init-keyword :a :allocation :class))) (define-class <baz3> (<baz1> <baz2>) ()) (define-class <baz4> (<baz2> <baz1>) ())
クラス <baz1> のスロット a は局所スロットで、<baz2> のスロット a は共有スロットです。この 2 つのクラスを多重継承して、クラス <baz3> と <baz4> を作成します。この場合、クラス優先順位リスト(この場合は左優先則)に従って、<baz3> のスロット a は局所スロット、<baz4> のスロット a は共有スロットになります。実行例は次のようになります。
gosh> (define x1 (make <baz3> :a 10)) x1 gosh> (define x2 (make <baz3> :a 20)) x2 gosh> (define y1 (make <baz4> :a 30)) y1 gosh> (define y2 (make <baz4>)) y2 gosh> (baz1-a x1) 10 gosh> (baz1-a x2) 20 gosh> (baz2-a y1) 30 gosh> (baz2-a y2) 30 gosh> (baz1-a y2) 30 gosh> (baz1-a y1) 30
<baz3> のインスタンス x1, x2 のスロット a の値は 10, 20 になるので、局所スロットであることがわかります。次に <baz4> のインスタンス y1, y2 を生成します。y1 のスロット a の値は 30 で、y2 のスロット a も 30 なので、共有スロットであることがわかります。このように、スロット名が衝突した場合、:allocation の設定は「クラス優先順位リスト」に従って決定されます。