クラスは (モジュール, 短縮名) の組で同一性が決まる。2 つのモジュールがそれぞれ
Parser を定義してよく、裸の Parser がどちらを指すかは import する側が決める。
すべてコンパイル時に解決される。VM はクラス ID でディスパッチし、モジュールのことを 何も知らない。つまり名前空間の実行時コストはゼロ。
すべてのファイルがモジュール
ファイルは必ずちょうど 1 つのモジュールに属する。どこにも属さないファイルは無い。
| ファイル | モジュール |
|---|---|
Module name: 'X' を宣言 | X |
| 宣言のないスクリプト | ファイル名を冠した専用モジュール |
プリロードされるライブラリ(lib/Core/Core.st、lib/Collections.st) | Core |
Core は組み込みクラスとプリロードライブラリを保持し、すべてのモジュールの
プレリュードになる。ここから来る帰結が 2 つある。
- スクリプトのクラス名はライブラリと衝突しない。
report.stでParserを定義すれば それはreport.Parserであり、他のどのParserとも別物。 Coreがエクスポートする名前(Association、Exception階層)は import 不要。
モジュールファイルは宣言だけ。トップレベル文は実行されないので、import された 副作用として処理が走ることはない。
解決順序
裸の名前は次の順で解決される。
- 現在のモジュールのクラス
- その import 群 — エクスポートされた名前のみ
Core(プレリュード)- 組み込みクラス
5 番目は無い。どれでもない名前はコンパイルエラーになる。原因から遠い場所の
doesNotUnderstand: として後から現れることはない。
これは例外ハンドラが絞り込むクラスにも及ぶ。on: Errs.MyError do: の名前が
解決できなければコンパイルエラーになる。以前はこれが catch-all にコンパイルされ、
綴り違いが黙って全例外を飲み込んでいた。
Module import: 'Errs'.
[ ... ] on: MyError do: [:ex | ... ]. "エクスポート済みなので短縮名で通る"
[ ... ] on: Errs.MyError do: [:ex | ... ]. "修飾名でも通る"
[ ... ] on: Errs.NoSuchError do: [:ex | ... ].
"コンパイルエラー: cannot resolve Errs.NoSuchError - module or class not found"
モジュールを宣言する
"lib/Shapes/Shapes.st"
Module name: 'Shapes'.
Module export: #(Circle Square).
Object subclass: #Circle instanceVariableNames: 'r'.
Object subclass: #Square instanceVariableNames: 'side'.
Object subclass: #Helper instanceVariableNames: ''. "エクスポートしない: 非公開"
別のファイルから使う。
Module import: 'Shapes'.
Circle new. "エクスポート済みなので短縮名が束縛されている"
Shapes.Circle new. "同じクラスを修飾名で参照"
Shapes.Helper new. "コンパイルエラー: cannot resolve Shapes.Helper"
Helper new. "コンパイルエラー: undefined identifier 'Helper'"
export: は強制される。リストに載せなかった名前は自分のモジュール内からしか
到達できない。短縮名でも修飾名でも同じこと。
モジュールの置き場所
モジュールは名前で探され、ドットはディレクトリ区切りとして扱われる。
| import | ファイル |
|---|---|
Module import: 'Shapes' | lib/Shapes/Shapes.st |
Module import: 'Maybe' | lib/Maybe.st |
Module import: 'Http.Router' | lib/Http/Router.st |
Module import: './local/Thing.st' | そのパス |
どのディレクトリをどの順で探すかはライブラリ・プラグイン探索パスにある。
修飾名は最後のセグメントで分割されるので、モジュール名自体がドットを含んでよい。
Http.Router.WebApp はモジュール Http.Router のクラス WebApp。
1 ファイル 1 モジュール
モジュールは import に使った文字列ではなく、解決先のファイル(正規化済み)で
同一性が決まる。同じファイルを 'Shapes' として import しても
'lib/Shapes/Shapes.st' として import しても 1 つのモジュールであり、
コンパイルも 1 回だけ。
名前は自身の Module name: 宣言から来るので、パスで import したモジュールも
Shapes.Circle で到達できる。
再エクスポート(ファサードパターン)
Module export: はモジュールの公開名を並べるが、それは自分が定義した名前である
必要がない。エクスポートされた名前はエクスポート元のスコープで解決されるため、
import したものをそのまま下流へ渡せる。標準ライブラリの HTTP ファサードがまさにこれ。
"lib/Http/Http.st"
Module name: 'Http'.
Module import: 'Http.Server'.
Module import: 'Http.Router'.
Module export: #(Route WebApp HttpRequest HttpResponse HttpServer).
import 1 つで 5 つとも束縛される。
Module import: 'Http'.
WebApp new. "ファサード経由で束縛されている"
Http.WebApp fullName. "=> 'Http.Router.WebApp'"
fullName が何を返すかに注目。再エクスポートしたファサードではなく、そのクラスを
定義しているモジュールを返す。
既存クラスを拡張する
新しいクラスを定義するのではなく、既存のクラス(組み込みクラスを含む)に到達する 書き方が 2 つある。
StringStream >> myMethod "メソッドを足す: 宣言は不要"
Template instanceVariableNames: 'pattern' "状態を持たせる"
メソッドを受け取るのは既存のクラスそのものなので、そのクラスのすべての利用者に
見える。モジュールが組み込みクラスに機能を足す仕組みはこれであり、組み込みクラス名が
モジュールで修飾されないのもこのため。String はどこでも String。
対して、Parser を別のモジュールから取り込んでいるスコープで
Object subclass: #Parser と書くのは正当で、別のクラスが定義される。
シャドーイングは名前空間の仕事。
2 つの import が同じ名前をエクスポートしたとき
Thing をエクスポートする 2 つのモジュールを import すること自体は問題ない。
問題になるのは裸の名前を使ったときで、コンパイルエラーになり、候補が示される。
Module import: 'ModA'. "Thing をエクスポート"
Module import: 'ModB'. "こちらも Thing をエクスポート"
ModA.Thing new. "可: どちらを指すか示している"
ModB.Thing new. "可"
Thing new.
"コンパイルエラー: class name 'Thing' is ambiguous - imported modules export it
as ModA.Thing and ModB.Thing. Use the qualified name."
衝突が報告されるのは import ではなく裸の名前を使った箇所なので、import を 足したことで、参照を修飾しているファイルが壊れることはない。
現在のモジュールが定義している名前は曖昧にならない。解決順序の 1 番が
無条件に勝つので、import した Thing と並べて Object subclass: #Thing と
書けば、意図したシャドーイングになる。
リフレクション
| 式 | 結果 |
|---|---|
Foo name | 'Circle' — 短縮名。一意ではない |
Foo fullName | 'Shapes.Circle'。組み込み・Core のクラスは素の名前 |
System allClasses | 全クラスの修飾名。エクスポートの有無によらない |
System classNamed: | 修飾名を取る。短縮名はそれを持つクラスが 1 つだけのとき機能し、さもなくば nil |
export: が支配するのはコンパイル時の名前解決であって、リフレクションではない。
System allClasses は非公開クラスも列挙する。
診断
メソッド本体のコンパイルに失敗すると、コンパイル自体が失敗し、該当するエラーは
まとめて報告される。コンパイルできなかったメソッドはインストールされないので、
ビルドを成功させてしまえば、プログラムは真の原因から離れた doesNotUnderstand: の
一歩手前の状態になる。あるいは誰も呼んでいなければそのまま通ってしまう。
fileIn: は import ではない
Smalltalk fileIn: 'helpers.st'.
fileIn: はファイルを現在のモジュールにコンパイルする。そこにテキストを貼った
のと同じ扱いだ。これはソース取り込みの仕組みであってモジュール import ではない。
取り込まれたクラスは取り込んだ側のモジュールに入り、export: リストは関与しない。