Guile基礎/モジュールの利用

変更履歴

概 要

目 次

参考資料

パブリックインターフェース

use-modulesの利用方法

▹ パブリックインターフェースを通してモジュールを利用する方法は単純です. use-modules 構文形式の引数にモジュール名を指定するだけのことです. 例えば,(ice-9 popen) という名前のモジュールをパブリックインターフェースを通して利用したければ,REPLやスクリプトファイルの冒頭などで次を実行します.
(use-modules (ice-9 popen))
これによって,(ice-9 popen) モジュールが公開する束縛をすべて利用することができます.さらに,use-modules には複数のモジュール名を指定できます. 例えば,(ice-9 popen) に加えて,(srfi srfi-11) モジュールを利用したいとき, 次のように指定できます(もちろん,ばらばらに指定してもかまいません).
(use-modules (ice-9 popen)
             (srfi srfi-11))

▹ 一般に, パブリックインターフェースを通してモジュールを利用するときには, 次のような use-modules 構文形式を使用します.
(use-modules module name$_1$ ... module name$_n$)
ここで,module name$_i$ はモジュール名です.

具体例

▹ 簡単な(でも,ちょっと無理筋な)具体例を示します. 下記のプログラムは,次の3つのモジュールを利用しています.
#!/usr/bin/guile \
-e main -s
!#
;; use-module-a.scm

(use-modules (ice-9 textual-ports)    ;; to use get-line
             (srfi srfi-1)            ;; to use fold  
             (srfi srfi-11))          ;; to use let-values 

(define (main args)
  (let* ((filename (cadr args))
         (lines (get-line-all filename))
         (nums (map string->number lines)))
    (let-values (((average variance) (calc-ave-var nums)))
      (format (current-output-port) 
              "nums: ~A \naverage: ~A  variance: ~A\n" 
              nums average variance))
    ))

(define (get-line-all filename)        
  (call-with-input-file filename
    (lambda (port)
      (let loop ((lst '()) (line (get-line port)))
        (if (eof-object? line) 
            lst 
            (loop (cons line lst) (get-line port)))))))

(define (calc-ave-var nums)
  (define len (length nums))
  (define (add x acc) (+ x acc))
  (define average (/ (fold add 0 nums) len))
  (define (addv x acc) (+ (* (- x average) (- x average)) acc))
  (define variance (/ (fold addv 0 nums) len))
  (values average variance))

上記の main は, テキストファイルのファイル名(filename)をコマンドライン引数から取得して, そのテキストファイルの各行からなるリスト(lines)を作り, その各行を数値に変換したリスト(nums)を作って, そのリストの平均値(average)と分散(variance)を求めます. 最後に,数値リスト(nums),平均値(average),分散(variance)を適当な形式(format)で表示しています. ただし,テキストファイルの各行は数値に変換可能なテキストデータ(つまり, 数字列データ)であることを仮定しています. この main の中で,(srfi srfi-11) モジュールが提供する let-values を利用しています.

get-line-all は,テキストファイルのファイル名(filename)を受け取って, そのファイルの各行からなるリスト(lst)を返します. この手続きの中で,(ice-9 textual-ports) モジュールが提供する get-line 手続きを利用しています.

calc-ave-var は,数値からなるリスト(nums)を受け取って, その平均値(average)と分散(variance)を求めて, これら2つの値をまとめて(多値として)返します. この手続きの中で,(srfi srfi-1) モジュールが提供する fold を利用しています.

簡単な実行例を示します.
$ ./use-module-a.scm numbers.txt
nums: (500 400 300 200 100) 
average: 300  variance: 20000
ここで,numbers.txt は次のようなテキストファイルです.
100
200
300
400
500

カスタムインターフェース

概要

▹ 筆者のような,単なる趣味としてプログラミングを楽しんでいるような者にとっては, パブリックインターフェースだけで十分に思えます. しかし,大規模なソフトウェアを開発するときには, それだけではおそらく不便なのでしょう. そのため,カスタムインターフェースを構築する仕組みが用意されています. カスタムインターフェースは, モジュールからロードするものを制限することと, 束縛の名前を変更して,名前の重複を回避したり, 利便性を高めたりすることを目的としています.

▹ カスタムインターフェースは, モジュール名にオプションを付加することによって構築します. モジュール名そのもの,および,オプションが付加されたモジュール名を総称して,インターフェース仕様(interface specification)と呼びます. この言葉はちょっと長いので,以後,iSpec と略すことにします.use-modules の一般的な形式は次のようになります.
(use-modules iSpec ... )
use-modules の引数にiSpecを指定することによってカスタムインターフェースを構成することになり, そのインターフェースを通してモジュールを利用することになります.

インターフェース仕様の一般的な形式

▹ インターフェース仕様の文法を示します. 下記の角括弧 [ ... ] は,括弧内の構文要素が省略可能であることを示しています.
iSepc       ::= module name
               |  '(' module name ['#:select' selection]
                                    ['#:prefix' prefix]
                                    ['#:renamer' renamer] ')'
module name ::= '(' symbol ... ')'                    ;; モジュール名
selection   ::= '(' sel-spec ... ')'                   ;; sel-spec のリスト
sel-spec    ::= orig-name  
               |  '(' orig-name ' . ' new-name ')'    ;; orig-namenew-name のペア
orig-name   ::= symbol
new-name    ::= symbol
prefix      ::= symbol
renamer     ::= シンボルを受け取ってシンボルを返す手続き.
symbol      ::= シンボル.ただし,クォート(')は付けない.

sels-spec選択スペック(selection-spec)と呼びます.prefixrenamer は, そのまま プリフィックス および リネーマー と呼びます.

▹ 各構文要素の意味は後述するとして,(ice-9 textual-ports) モジュールを題材に, イメージを喚起するするための具体例を示します. 以下の紫色の部分が iSpec です.

#:select select

▹ 上記の文法が示すように,select は,
orig-name または (orig-name . new-name)
からなるリストです. ここで,orig-name は, モジュール内の(オリジナルの)束縛名を表し, new-name は,利用する側で使用する新たな名前を表しています. このオプションは,orig-name で指定された束縛を選択することを示しています.ただし,new-name が指定されたときには,その新たな名前で利用することを示しています. なお,選択しなかった束縛は利用できなくなります.

#:prefix prefix

▹ このオプションは, モジュール内の束縛名の前に prefix を付けて利用することを示しています.

#:renamer renamer

renamer はシンボルを受け取ってシンボルを返す手続きです. このオプションは, モジュール内の束縛名(シンボル)に対して renamer を適用し, その結果として得られるシンボル(名前)を元々の束縛名の代わりに利用することを示しています.

▹ 例えば,あるスクリプトの冒頭に次のような断片があったとしましょう.
(define (sym-proc sym)
   (string->symbol (string-append "text:" (symbol->string sym) "_hoge")))
(use-modules ((ice-9 textual-ports) #:renamer sym-proc))
sym-proc は,シンボル(sym)を受け取って, その前後に text: と _hoge を付加したシンボルを返す手続きです. 例えば,get-char というシンボルを受け取ったら text:get-cahr_hoge というシンボルを返します.そのため,上記の
#:renamer sym-proc
というオブションは,(ice-9 textual-pots) モジュールのすべての束縛名に対して,sym-proc を適用したあとの名前を利用すること,つまり,元々の束縛名の前後に text: と _hoge を付加した名前を利用することを示しています.

▹ (補足) #:prefix オプションと #:renamer オプションを同時に指定したときには #:prefix オプションは無視されるようです. でも,両方を同時に指定することはないでしょう.

リネーマー(renamer)に関する問題点

▹ 上で示した具体例は,動きはするのですが, コンパイル時に「sym-procが未定義」といったエラー(warningレベル)が発生して, コンパイルしてくれません(VMコードがキャッシュされません). そのため,Guileはコンパイルをあきらめて, ソースコードを直接,解釈実行します(注:これも初めて知りましたが,WRNINGレベルだと解釈実行するのだろうと思います).

エラーの原因は,use-modules のマクロ展開時に sym-proc の define 形式の評価が行われていないためです.

use-modules をマクロ展開するときに, おそらく,モジュール内のすべての束縛名に対して sym-proc を適用しようとしているのだと思います.しかしながら, マクロ展開時には sym-proc の define 形式が評価(コンパイル&ロード)されていないので,sym-proc を発見できずにエラーが発生しているのだと思います. 本音としては,マクロ展開後のコンパイル時に適用して欲しいところですが, 筆者には想像できない困難があるのかも知れません.

以上は筆者の推測です.でも,大きくはずれてはいないと思います. 例えば,次のような現象を観測することができます. 実のところ,時間をかけて色々と試してみたのです.

Eval-When(いつ評価するか?)

▹ リネーマーの問題についてあれこれと調べているうちに解決策が分かりました. 先に示した断片を次のように変更します. 赤字が変更したところです.
(eval-when 
 (expand load eval)
 (define (sym-proc sym)
   (string->symbol (string-append "text:" (symbol->string sym) "_hoge"))))
(use-modules ((ice-9 textual-ports) #:renamer sym-proc))
eval-when は, 第2引数として指定された式や構文形式をいつ評価するかを指定するための構文形式です. 第1引数で「いつ」を指定します. 上記の (expand load eval) は,マクロ展開時(expand)と, コンパイル後のプログラムをロード(load)するときと, 直接解釈実行(eval)するときに評価することを指定しています. 上記のように変更すると,無事,コンパイルされます. ちなみに,ロード(実行)時に評価するのは当たり前だと思いますが, load を指定しておかないとロード時に評価してくれなくなります. なので,eval-when は厳密にいつ評価するかを指定するものだと思います.

具体例

▹ 具体例を示します. 以下は先に示したスクリプトを, モジュールに関係するところだけを変更したものです. 紫色は変更箇所を示しています. 処理内容は先のものとまったく同じなので,実行結果は省略します.
#!/usr/bin/guile \
-e main -s
!#

;; use-module-b.scm

(eval-when 
 (expand load eval)
 (define (add-infix str)
   (lambda (sym) 
     (let* ((sym-str (symbol->string sym))
            (lst (string-split sym-str #\-)))
       (if (= (length lst) 2)
           (string->symbol (string-append (car lst) "-" str "-" (cadr lst)))
           sym))))
 )

(use-modules ((ice-9 textual-ports) 
              #:select (get-line)
              #:renamer (add-infix "text"))
             ((srfi srfi-1)
              #:select (fold)
              #:prefix srfi-1:)
             ((srfi srfi-11)
              #:select ((let-values . let-multi-values)))
             )

(define (main args)
  (let* ((filename (cadr args))
         (lines (get-line-all filename))
         (nums (map string->number lines)))
    (let-multi-values (((average variance) (calc-ave-var nums)))
      (format (current-output-port) 
              "nums: ~A \naverage: ~A  variance: ~A\n" 
              nums average variance))
    ))

(define (get-line-all filename)
  (call-with-input-file filename
    (lambda (port)
      (let loop ((lst '()) (line (get-text-line port)))
        (if (eof-object? line) 
            lst 
            (loop (cons line lst) (get-text-line port)))))))
     
(define (calc-ave-var nums)
  (define len (length nums))
  (define (add x acc) (+ x acc))
  (define average (/ (srfi-1:fold add 0 nums) len))
  (define (addv x acc) (+ (* (- x average) (- x average)) acc))
  (define variance (/ (srfi-1:fold addv 0 nums) len))
  (values average variance))
add-infix の中のラムダ式が定める手続きは, シンボル(sym)を受け取って, それ(sym)がハイフンで2つの部分に区切られているとき, その2つの部分の間に文字列(str)で指定されたシンボルを(ハイフンで区切って)挿入して返します.ハイフンがなかったり,3つ以上の部分に区切られているとき(ハイフンが2つ以上含まれているとき)にはシンボル(sym)そのものを返します.

上記の (ice-9 textual-ports) モジュールについては,get-line だけを選択し, その名前の真ん中に -text- というシンボルを挿入して使うことを指定しています.

(srfi srfi-1) モジュールについては,fold だけを選択し, その名前に srfi-1 というプリフィックスを付けて使うことを指定しています.

(srfi srfi-11) モジュールについては,let-values だけを選択し, その名前を let-multi-values に変更して使うことを指定しています.

個別に引き抜く

公開されている束縛を引き抜く

▹ 次の形式を使うことによって,use-modules を使うことなく, モジュール内の束縛を個別的に利用することができます.
(@ module name name)
ここで,module name はモジュール名,name はモジュール内で定義された束縛名です. 例えば,
(@ (ice-9 textual-ports) get-line)
によって,use-modules を使うことなく,(ice-9 testual-ports) モジュール内の get-line を利用することができます. 以下は,これを確認するための実行例です. (a)はREPLを起動した時点では get-line が束縛されていないことを確認しています. (b)は上記の形式によって get-line の束縛が取り出せることを確認しています.
$ guile
GNU Guile 3.0.5
      ...... 起動メッセージ ......
guile> get-line    ……(a) 
      ......
Unbound variable: get-line
      ......
guile [1]>

guile> (@ (ice-9 textual-ports) get-line)    ……(b) 
$1 = #<procedure get-line (port)>
guile> 

非公開の束縛を引き抜く

▹ 次の形式を使うことによって, モジュール内の非公開の束縛を個別的に利用することができます.
(@@ module name name)
ここで,module name はモジュール名,name はモジュール内で定義された束縛名です. 例えば,(srfi srfi-1) モジュールの中に非公開の any1 という手続きがあるのですが,
(@@ (srfi srfi-1) any1)
によってその手続きを利用することができます. 以下は,これを確認するための実行例です. (a)はREPLを起動した時点では any1 が束縛されていないことを確認しています. (b)は公開済みの束縛を引き抜く方法では any1 を取り出せないことを確認しています. 最後に,(c)は上記の形式によって any1 の束縛が取り出せることを確認しています.
$ guile
GNU Guile 3.0.5
      ...... 起動メッセージ ......
guile> any1  ……(a) 
      ......
Unbound variable: any1
      ......
guile [1]> 

guile> (@ (srfi srfi-1) any1)  ……(b) 
      ......
Unbound variable: any1
      ......
guile [1]> 

guile> (@@ (srfi srfi-1) any1)  ……(c) 
$1 = #<procedure any1 (pred ls)>
guile> 
ただ, Guileのマニュアルは,この方法は最終手段(last resort)として, もしくはデバッグのためだけに利用すべしと述べています. (でも,最終手段とせざるを得ない状況(必然性)はあるのでしょうか?)

具体例

▹ これまでに示したスクリプトを,use-modules を利用しないものに書き換えてみます. 紫色が変更したところです. 処理内容はこれまでのものとまったく同じなので,実行例は省略します.
#!/usr/bin/guile \
-e main -s
!#
;; use-module-c.scm

(define (main args)
  (let* ((filename (cadr args))
         (lines (get-line-all filename))
         (nums (map string->number lines)))
    ((@ (srfi srfi-11) let-values) 
     (((average variance) (calc-ave-var nums)))
      (format (current-output-port) 
              "nums: ~A \naverage: ~A  variance: ~A\n" 
              nums average variance))
    ))

(define (get-line-all filename)
  (call-with-input-file filename
    (lambda (port)
      (let loop ((lst '()) 
                 (line ((@ (ice-9 textual-ports) get-line) port)))
        (if (eof-object? line) 
            lst 
            (loop (cons line lst) 
                  ((@ (ice-9 textual-ports) get-line) port)))))))

(define (calc-ave-var nums)
  (define len (length nums))
  (define (add x acc) (+ x acc))
  (define average (/ ((@ (srfi srfi-1) fold) add 0 nums) len))
  (define (addv x acc) (+ (* (- x average) (- x average)) acc))
  (define variance (/ ((@ (srfi srfi-1) fold) addv 0 nums) len))
  (values average variance))
(おしまい)