chicken schemeのモジュールの話

グローバルな名前が多くなってくると、
名前の衝突や汚染を防ぐためにモジュール分けをするようになっていきます。

Chicken Schemeではmodule機能があるため、
Nobukoでも利用していくことにしました。

module

http://wiki.call-cc.org/man/4/Modules

(module myutil ()
  (import chicken scheme)
  (include "util.scm"))

モジュールの定義はこんな感じです。
モジュール内では、主にimportとexportを利用します。

importは他モジュールのexportされたシンボルを
モジュールに対して束縛するための機能です。

モジュールの定義は完全に空の状態からスタートするので、
はじめにchicken schemeの定義済みモジュールをimportすることになります。

includeは外部のソースファイルへと展開される機能です。
モジュールのbodyに直接定義を書くこともできますが、
ネストが深くなることと、実装とモジュール定義を分離しにくくなるので、
実装本体はincludeするようにすることをおすすめします。

(export single?)
(define (single? lst)
  (and (pair? lst) (null? (cdr lst))))

exportはモジュールの外部に公開するシンボルを設定する機能です。
exportは引数に複数のシンボルを取れるため、
モジュールを定義しているファイルでまとめて記述してもよし、
各手続き定義の先頭につけるなどすることもできます。

どちらも一長一短があるため、
好きな記法を利用すると良いと思われます。

インタプリタでの使用

直接インタプリタで読み込んで使うことができます。

> (load "myutil.scm")
> (import myutil)
> (single? '(1))

コンパイル

共有ライブラリにして読み込んで利用することもできます。
ただし、Chicken Schemeはユニット単位でコンパイルするため、
ユニットをまたいだモジュールの名前解決のために .import.scm の拡張子がつく
import-libraryファイルを生成する必要があります。

生成は csc にオプションで -J をつけることで自動生成されます。

> csc -s -J myutil.scm
> ls 
myutil.so myutil.import.scm

コンパイルするアプリケーション側で利用する場合、
この .import.scm が生成されていることが必要になるため、
生成し忘れに注意しましょう。

(load "myutil")
(import (prefix myutil my:))

(define (myapp)
  (display "> ")
  (display (my:single? (read))))

(myapp)

モジュールを環境にimportする際にprefixをつけることで、
シンボルに接頭語をつけられるので、名前が重複する場合などに利用できます。

また、renameなどの改名機能などモジュールの名前解決のための機能は
いくつかあるため、柔軟に利用することができます。

モジュールを使う上で注意すべきこと

名前解決

モジュールは空の環境からimportを行って、
モジュールのボディの評価に必要な名前解決が全てできる必要があります。
必要なimportは全て行う必要があるため、
複雑なモジュールを実装する際にはimportを書くこと自体が大変になることもあります。

import-library

Chicken Scheme
名前解決の都合ですが、
単純な共有ライブラリを作っていると必要ないものであるため、
moduleを使い始めた際に、意識し始めなければなりません。

中身は単純なSchemeのコードなので、
挙動は納得できるかと思いますが、moduleを使うために一手間いる印象があります。

NEXT

モジュールを使うようにしたので、
各手続きで内部定義をしすぎるマンモス手続きを書かないようにできそうです。
後々、いままで書いていた手続きも綺麗にばらしていこうかと思います。

また、今後はmoduleのprefixが使えるので、
モジュール内の定義名自体を複雑化させたりすることは抑えようと思います。
単一のソース内ではprefixがいらなくなるので、
コードの可読性が上がっていくことを期待しています。