M.Hiroi's Home Page

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

オブジェクト指向編 : モジュール

Copyright (C) 2010 Makoto Hiroi
All rights reserved.

はじめに

前回は双方向リストクラス <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 を使います。

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

初版 2010 年 4 月 4 日