クラスは (モジュール, 短縮名) の組で同一性が決まる。2 つのモジュールがそれぞれ Parser を定義してよく、裸の Parser がどちらを指すかは import する側が決める。

すべてコンパイル時に解決される。VM はクラス ID でディスパッチし、モジュールのことを 何も知らない。つまり名前空間の実行時コストはゼロ。


すべてのファイルがモジュール

ファイルは必ずちょうど 1 つのモジュールに属する。どこにも属さないファイルは無い。

ファイルモジュール
Module name: 'X' を宣言X
宣言のないスクリプトファイル名を冠した専用モジュール
プリロードされるライブラリ(lib/Core/Core.st、lib/Collections.st)Core

Core は組み込みクラスとプリロードライブラリを保持し、すべてのモジュールの プレリュードになる。ここから来る帰結が 2 つある。

モジュールファイルは宣言だけ。トップレベル文は実行されないので、import された 副作用として処理が走ることはない。


解決順序

裸の名前は次の順で解決される。

  1. 現在のモジュールのクラス
  2. その import 群 — エクスポートされた名前のみ
  3. Core(プレリュード)
  4. 組み込みクラス

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: リストは関与しない。