モジュール
Julia のモジュールは他の部分と分離された自由に使えるワークスペースであり、新しいグローバルスコープを作成します。モジュールは module Name ... end
を使って構文的に区切られます。モジュールでは他の人が書いたコードとの名前の衝突を気にすることなくトップレベルの定義 (グローバル変数) を利用できます。モジュールでは他のモジュールに含まれる名前のどれを見えるようにする (インポートする) か、自身に含まれる名前のどれをパブリックにする (エクスポートする) かを指定できます。
モジュールの主な機能を次の例に示します。機能を示すためのコードであり、実行はできません:
module MyModule
using Lib
using BigLib: thing1, thing2
import Base.show
export MyType, foo
struct MyType
x
end
bar(x) = 2x
foo(a::MyType) = bar(a.x) + 1
show(io::IO, a::MyType) = print(io, "MyType $(a.x)")
end
モジュールはファイル全体に渡ることが多いので、モジュールの本体はインデントしないのが標準のスタイルとなっています。
この MyModule
というモジュールは型 MyType
と二つの関数を定義します。関数 foo
と型 MyType
はエクスポートされるので、他のモジュールからインポートが可能です。一方で関数 bar
は MyModule
にプライベートとなります。
二行目の文 using Lib
により、Lib
という名前のモジュールが必要に応じて名前解決で利用可能になります。グローバル変数が現在のモジュールで定義を持たないと、システムは Lib
がエクスポートする変数を探索し、見つかればそれをインポートします。これは現在のモジュールにおけるそのグローバル変数の利用が全て Lib
で定義された同じ名前の変数として解決されることを意味します。
四行目の文 using BigLib: thing1, thing2
はモジュール BigLib
から識別子 thing1
と thing2
だけを現在のモジュールに取り入れます。この名前が関数を指す場合には、その関数にメソッドを追加することはできません (つまり "使う" ことはできますが、拡張は許されません)。
import
キーワードは using
と同じ構文をサポートします。ただし using
と異なり、import
を使っても探索を行うモジュールは増えません。またインポートした関数にメソッドを追加できるのは import
を使ったときだけという違いもあります。
上の MyModule
では標準の show
関数にメソッドを追加しているので、import Base.show
を使う必要があります。using
で見えるようになった関数は拡張できません。
using
または import
によって変数がモジュールから見えるようになると、そのモジュールでは同じ名前の変数を作成できなくなります。またインポートした変数は読み込み専用です: グローバル変数への代入は現在のモジュールが持つ変数に対してだけ実行でき、代入先の変数を持つのが現在のモジュールでなければエラーが発生します。
モジュールの使い方のまとめ
モジュールを読み込むためのキーワードは using
と import
の二つです。この違いを理解するために、次の例を考えます:
module MyModule
export x, y
x() = "x"
y() = "y"
p() = "p"
end
このモジュールは関数 x
と y
を (export
キーワードで) エクスポートし、関数 p
をエクスポートしません。このモジュールとそこに含まれる関数を他のワークスペースから読み込むための方法を次の表にまとめます:
インポートコマンド | スコープに入る名前 | メソッドを拡張できる 関数 |
---|---|---|
using MyModule |
export された全ての名前 (x と y ) MyModule.x , MyModule.y , MyModule.p |
MyModule.x , MyModule.y , MyModule.p |
using MyModule: x, p |
x , p |
(存在しない) |
import MyModule |
MyModule.x , MyModule.y , MyModule.p |
MyModule.x , MyModule.y , MyModule.p |
import MyModule.x, MyModule.p |
x , p |
x , p |
import MyModule: x, p |
x , p |
x , p |
モジュールとファイル
基本的にファイルとファイル名はモジュールと関係がありません。モジュールはモジュール式にだけ関連付きます。一つのモジュールを複数のファイルに書くことも、一つのファイルに複数のモジュールを書くこともできます。複数のファイルに書かれたモジュールを一つのモジュール式に読み込む例を示します:
module Foo
include("file1.jl")
include("file2.jl")
end
同じコードを異なるモジュールにインクルードすれば、ミックスインのような効果を得られます。この手法を使うと同じコードを異なる定義で実行でき、例えば特定の演算子を安全なバージョンに切り替えてコードをテストするといった使い方が可能です:
module Normal
include("mycode.jl")
end
module Testing
include("safe_operators.jl")
include("mycode.jl")
end
標準モジュール
重要な標準モジュールが三つあります:
-
Core
: 言語に組み込みの機能を全て含みます。 -
Base
: ほぼ全ての状況で役に立つ基本的な機能を含みます。 -
Main
: Julia を起動したときのトップレベルモジュール (カレントモジュール) です。
デフォルトのトップレベル定義と baremodule
全てのモジュールには using Base
という文に加えて eval
と include
という関数の定義が自動的に含まれます。この二つの関数はそれぞれ式とファイルをモジュールのグローバルスコープで評価します。
これらのデフォルトの定義が必要でないなら、モジュールを baremodule
キーワードで定義してください (注意: こうしたとしても Core
はインポートされます)。baremodule
を使って通常の module
を使ったときと同じコードを書くと次のようになります:
baremodule Mod
using Base
eval(x) = Core.eval(Mod, x)
include(p) = Base.include(Mod, p)
...
end
モジュールの相対パスと絶対パス
using Foo
という文が与えられると、システムはトップレベルモジュールを収めた内部テーブルから Foo
という名前のモジュールを検索します。もしそこに Foo
が存在しなければ、システムは require(:Foo)
を呼び出します。これにより多くの場合インストールされたパッケージからのコードの読み込みが起こります。
しかし中には自身の内部にモジュールを持つモジュールもあり、そのような場合にはトップレベルでないモジュールへのアクセスも必要になります。非トップレベルのモジュールへのアクセス方法は二つあります。一つは using Base.Sort
のような絶対パスを使うもので、二つ目は次の例のように相対パスを使うものです。相対パスを使うと現在のモジュール (またはその親) が持つサブモジュールへのアクセスが簡単に行えます:
module Parent
module Utils
...
end
using .Utils
...
end
ここでは Parent
モジュールがサブモジュール Utils
を持ち、Paren
のコードから Utils
をインポートしようとしています。using
の後に続くパスをドットで始めれば相対パスでモジュールをインポートできます。ドットを続けて書くとモジュール階層を上に進み、例えば using ..Utils
と書けば Parent
を含むモジュールから Utils
が検索されます。
相対インポートは using
文と import
文でのみ正当なことに注意してください。
名前空間について
Base.sin
のようにモジュール名で限定した名前を使うと、エクスポートされていない名前にもアクセスできます。これはデバッグで役立ちます。また他のモジュールの関数にメソッドを追加するときは関数名を限定する必要があります。ただし構文的な曖昧性を避けるために、名前が記号だけからなる関数 (例えば演算子) にメソッドを追加するときは、Base.+
を Base.:+
のようにする必要があります。演算子が二文字以上のときはさらに括弧が必要で、例えば Base.:(==)
とします。
マクロのインポートまたはエクスポートでは名前の前に @
を付けます。例えば import Mod.@mac
です。他のモジュールのマクロを呼び出すには Mod.@mac
または @Mod.mac
のようにします。
M.x = y
という構文を使っても他のモジュールに含まれるグローバル変数への代入は行えません。グローバルな代入は必ず現在のモジュールにだけ影響を及ぼします。
global x
としてグローバル変数を宣言すると、値を代入することなく変数の名前を "予約" できます。これを使うとモジュールの読み込みの後に初期化されるグローバル変数に対する名前の衝突を回避できます。
モジュールの初期化と事前コンパイル
大きなモジュールは読み込みに数秒単位の時間がかかります。これはモジュールに含まれる全ての文を実行するために大量のコードのコンパイルが必要となるためです。Julia はモジュールを事前コンパイルした結果のキャッシュを作成することでこの時間を削減します。
import
または using
でモジュールを読み込むと、漸進的に事前コンパイルされたモジュールファイルが自動で作成されます。そのためモジュールのコンパイルは初めてインポートしたときに自動で始まります。また Base.compilecache(modulename)
を使えば手動で自動コンパイルを起動できます。作成されるキャッシュファイルは DEPOT_PATH[1]/compiled/
に保存されます。モジュールの依存先が変更されると、次に using
あるいは import
したときに再コンパイルが自動で行われます。ここでモジュールの依存先とは、Julia のビルド・インポートするモジュール・インクルードするファイル・モジュールのファイルで include_dependency(path)
を使って明示的に宣言された依存ファイルといった情報のことです。
依存ファイルに対する変更の有無は include
で読み込まれたファイルまたは include_dependency
を使って明示的に追加されたファイルの変更時刻 (mtime) で判断されます。正確に言うと更新時刻を秒で切り捨てた時刻が使われます (更新時刻が秒未満の精度を持たないシステムをあるためです)。require
の探索処理が見つけたファイルのパスが事前コンパイルされたファイルのパスと一致するかどうかも確認されます。また現在のプロセスに読み込まれた依存先も考慮し、実行中のシステムと事前コンパイルされたキャッシュが非互換性にならないように、既に読み込まれた依存ファイルが変更あるいは削除されたときはモジュールの再コンパイルを行いません。
あるモジュールを事前コンパイルしてはいけないと分かっている場合には、そのモジュール (の通常は先頭部分) に __precompile__(false)
を追加してください。こうすると Base.compilecache
でエラーが発生するようになります。そのため using
と import
はこのモジュールに対する事前コンパイルとキャッシュ作成をスキップし、現在のプロセスにモジュールを直接読み込みます。さらにそのモジュールは他の事前コンパイルされたモジュールからインポートできなくなります。
モジュールを書くときは、漸進的にリンクされる共有ライブラリに特有の挙動に注意が必要です。例えば外部の状態はキャッシュに保存されないので、実行時の初期化とコンパイル時の初期化の区別を意識する必要があります。この区別を分かりやすくするために、Julia では実行時に実行する初期化処理を __init__()
関数に書くことになっています。__init__
関数はコンパイル時 (--output-*
) には実行されません。この関数は事実上コードが読み込まれるときに一度だけ実行されるものと仮定して構いません。もちろん必要なら事前コンパイル中にこの関数を呼び出しても構いませんが、この関数はコンパイルイメージに格納する必要のない ──あるいは格納するべきではない── ローカルマシンの状態を使った処理を行うものとされます。__init__
関数はモジュールがプロセスに読み込まれた後に呼び出されます。漸進的コンパイル (--output-incremental=yes
) で読み込まれるときは実行されますが、フルコンパイルで読み込まれるときは実行されません。
具体的に言うと、あるモジュールで function __init__()
を定義したとき、Julia はそのモジュールを (import
, using
, require
などで) 初めて読み込んだ直後に __init__()
を実行します。言い換えると、__init__
はモジュールに含まれる全ての文を実行した後に一度だけ実行されます。モジュールのインポートが完了した後に呼ばれるので、サブモジュールやインポートしたモジュールの __init__
は親のモジュールの __init__
の前に呼ばれます。
よくある __init__
の利用例としては、外部 C ライブラリの関数の初期化や、外部ライブラリが返すポインタが関係するグローバル変数の初期化があります。例として C ライブラリ libfoo
を使おうとしている状況を考えます。実行時にこのライブラリの初期化関数 foo_init()
を呼び出す必要があり、さらに void *foo_data()
という関数が返すポインタをグローバル変数 foo_data_ptr
に格納したいとします ──ポインタアドレスは実行ごとに変わるので、この変数は (コンパイル時ではなく) 実行時に初期化が必要です。この初期化処理は、次の __init__
を定義することで実現できます:
const foo_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
ccall((:foo_init, :libfoo), Cvoid, ())
foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Cvoid}, ())
nothing
end
本筋とは関係ありませんが、__init__
のような関数の中でグローバル変数を定義しても問題はありません: これは動的言語を使う利点の一つです。ただグローバルスコープで定数を作っておけば、コンパイラに型を伝えることでより最適化されたコードの生成が可能になります。それから当然、foo_data_ptr
を利用する他のグローバル変数も __init__
の中で初期化される必要があります。
ccall
を使わずに計算できる Julia オブジェクトを指す定数の計算は基本的に __init__
で行わなくて構いません。定数の定義を事前コンパイルして、キャッシュされたモジュールイメージからロードできます。事前コンパイルできるオブジェクトには配列のようなヒープにアロケートされる複雑なオブジェクトも含まれます。ただし事前コンパイルした結果を正しく動作させるには、生のポインタ値を返すルーチンだけは実行時に呼び出す必要があります (キャッシュされるとき Ptr
オブジェクトは isbits
オブジェクトとして隠されていない限りヌルポインタに変換されます)。Julia 関数 cfunction
と pointer
も同様です。
辞書型や集合型、そして一般には hash(key)
の出力を利用する任意の型の値では、事前コンパイルで厄介な問題が発生します。多くの場合キーは数値・文字列・シンボル・区間・Expr
もしくはこれらの複合型 (配列・タプル・集合・ペアなど) であり、この場合には事前コンパイルに問題はありません。問題が起こるのはキーが Function
や DataType
といった特定の型のとき、およびキーが hash
メソッドを定義していないユーザー定義型で、フォールバックの hash
メソッドが (objectid
を通じて) オブジェクトのメモリアドレスを参照しているためにハッシュが実行ごとに変わるときです。キーがこういった型を含む場合、あるいは含まないと確信できない場合には、念のため辞書の初期化を __init__
関数で行うようにしてください。あるいはコンパイル時に安全に初期化できるよう事前コンパイル処理で特別に扱われる辞書型 IdDict
を使うこともできます。
事前コンパイルを使うときは、コンパイルフェーズと実行フェーズの区別をしっかり意識することが重要です。このモードでは 「julia
は任意の Julia コードを実行できるコンパイラであって、コンパイルされたコードを生成できるスタンドアローンのインタープリタではない」という事実が明確になるでしょう。
事前コンパイルでありがちなミスをさらにいくつか示します:
-
グローバルなカウンター (例えばオブジェクトをユニークに特定するための識別子) を使いたいとします。次のスニペットを考えてください:
mutable struct UniquedById myid::Int let counter = 0 UniquedById() = new(counter += 1) end end
このコードは全てのインスタンスにユニークな ID を与えることを意図していますが、カウンターの値はコンパイルの最後に記録されます。そのため漸進的にコンパイルされたこのモジュールを使う全てのモジュールはカウンターを同じ値から使い始めることになります。
objectid
(メモリポインタのハッシュとして計算される値) にも同様の問題があることに注意してください (下記のDict
に関する注意事項を参照)。対処法の一つとして、
@__MODULE__
をキャプチャするマクロを使って現在のcounter
の値だけでなくモジュールの名前も保存するという方法があります。ただ、グローバルな状態に依存しないようコードを再設計した方がよいかもしれません。 -
Dict
やSet
といった連想コレクションは__init__
で再ハッシュが必要です (もしかすると将来、初期化関数を登録する仕組みが追加されるかもしれません)。 -
読み込みまで影響が残る副作用のある処理をコンパイル時に行わないでください。例えば、他の Julia モジュールに含まれる配列や変数を改変したり、ファイルやデバイスのハンドルを管理したり、メモリなどのシステムリソースへのポインタを保存したりしないでください。
-
他のモジュールのグローバルな状態のコピーを作らないでください。コピーは探索パスを使わずに直接変数を参照すると発生します。例えば、グローバルスコープにおける次のコードを考えます:
# これは正しくない。Base.stdout をこのモジュールにコピーしてしまう。 # mystdout = Base.stdout # アクセッサ関数を使うのが正しい: getstdout() = Base.stdout # 最良の方法 # 代入を実行時に行ってもよい: __init__() = global mystdout = Base.stdout # これでもよい
間違った処理を行わないように、事前コンパイル中に行える演算には追加の制限があります:
- 他のモジュールの
eval
を呼び出して副作用を起こすことはできません。漸進的事前コンパイルフラグが設定されているときは警告が発生します。 __init__()
が開始された後にローカルスコープでglobal const
文を使うことはできません。- 漸進的事前コンパイルを行っているときは、モジュールの置き換えが実行時エラーとなります。
さらに注意すべき点を示します:
- ソースファイルを変更しただけではコードの再読み込みやキャッシュの無効化は行われません (
Pkg.update
でも同様です)。またPkg.rm
の後にクリーンアップは行われません。 - 形を変更した配列がメモリを共有する機能は事前コンパイル中は無効化されます (それぞれのビューが別々のコピーを受け取ります)。
- コンパイル時と実行時でファイルシステムが変更されないと仮定するのは間違っています。つまり
@__FILE__
やsource_path()
あるいは BinDeps.jl の@checked_lib
を使って実行時にリソースを探索するのは避けるべきです。これが避けられない場合もありますが、可能な限り、コンパイル時にリソースをモジュールへコピーして実行時にはコピーが必要ないようにしてください。 - シリアライザは
WeakRef
オブジェクトとファイナライザを正しく処理できません (将来のリリースで改善される予定です)。 - 内部メタデータオブジェクト (
Method
,MethodInstance
,MethodTable
,TypeMapLevel
,TypeMapEntry
やこれらのフィールド) のインスタンスに対する参照をキャプチャするのは基本的に避けるべきです。シリアライザがこういったオブジェクトを上手く処理できず、予期しない結果が得られる可能性があります。これを行ってもエラーにはなりませんが、システムがオブジェクトをコピーしたり、オブジェクトのユニークなインスタンスを作成したりする可能性があることを認識しておいてください。
モジュールを開発するときは漸進的事前コンパイルをオフにすると便利な場合もあります。コマンドライン引数 --compiled-modules={yes|no}
を使えばモジュールの事前コンパイルを制御できます。--compiled-modules=no
として Julia を起動すると、モジュール (およびその依存先) の読み込みにおいてコンパイルキャッシュにあるシリアライズされたモジュールが無視されるようになります (Base.compilecache
の呼び出しは可能です)。このコマンドライン引数は Pkg.build
に渡され、パッケージのインストール・更新・明示的ビルドにおける自動的な事前コンパイルが有効化/無効化されます。