モジュール

モジュール

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

表記スタイル上の注意点は、モジュールの本体をインデントしないということでしょう。インデントするとなると、通常はファイル全体がインデントされることになってしまいます。

このモジュールでは、型 MyType と 2 つの関数を定義されています。関数 foo と型 MyType がエクスポートされ、他のモジュールへのインポートに使用できるようになります。 関数 barMyModule 内でのみ使用するプライベートな関数です。

using Libと宣言することで、必要に応じて Lib というモジュールを使用して名前を解決します。現在のモジュールに定義がないグローバル変数が検出されると、システムはLib によってエクスポートされた変数の中からその変数を検索し、そこに見つかった場合はインポートします。 つまり、現在のモジュール内でグローバル変数を使うと全てLib 内の同名の変数定義によって解決されることを意味します。

using BigLib: thing1, thing2 とう宣言で、thing1thing2 という識別子が BigLib から現在のスコープに持ち込まれる。もしそれらの名前が、関数を参照するものならば、メソッドの追加は許されません(メソッドを使用するだけで、拡張は許されない)。

import キーワードは using と同じ構文をサポートします。usingのようにモジュール全体を検索対象に追加することはできません。import を使用してインポートされた関数は新しいメソッドで拡張できるという点で、using とは異なります。

上記のMyModuleでは、標準のshow関数にメソッドを追加したかったので、import Base.showと書く必要が有りました。名前が using を介してのみ参照できる関数は拡張できません。

using または import を使用して変数が一度参照されると、その変数と同じ名前を持つ変数を、モジュール内に独自に定義することはできなくなります。インポートされた変数は読み取り専用です。グローバル変数に代入は、常に現在のモジュールが所有する変数に対するものです。そうでなければエラーが発生します。

モジュールの使用法のまとめ

モジュールを読み込むには、usingimport の2つの主要キーワードが利用できます。両者の違いを理解するには、次の例を考えてください:

module MyModule

export x, y

x() = "x"
y() = "y"
p() = "p"

end

このモジュールは、関数 x と 関数 y を(キーワード exportを用いて) エクスポートし、それとは別にエクスポートされていない関数 p も定義されています。モジュールとその内部関数を現在のワークスペースにロードするには、いくつかの異なる方法があります:

インポートするコマンドスコープに導入されるものメソッド拡張に利用できるもの
using MyModuleAll exported names (x and y), MyModule.x, MyModule.y and MyModule.pMyModule.x, MyModule.y and MyModule.p
using MyModule: x, px and p
import MyModuleMyModule.x, MyModule.y and MyModule.pMyModule.x, MyModule.y and MyModule.p
import MyModule.x, MyModule.px and px and p
import MyModule: x, px and px and p

モジュールとファイル

ファイルとファイル名は、ほとんどモジュールとは無関係です。モジュールはモジュール式にのみ関連付けられています。1 つのモジュールが複数のファイルをまたぐこともできますし、複数のモジュールを同じ1つのファイルに定義することもできます:

module Foo

include("file1.jl")
include("file2.jl")

end

異なるモジュールに同じコードを含めると、mixin のような動作が提供されます。これを使用して、異なる基本定義で同じコードを実行できます。例えば、それを実行することである処理を「安全バージョン」実行してコードを検査することができます:

module Normal
include("mycode.jl")
end

module Testing
include("safe_operators.jl")
include("mycode.jl")
end

標準モジュール

There are three important standard modules: * Core contains all functionality "built into" the language. * Base contains basic functionality that is useful in almost all cases. * Main is the top-level module and the current module, when Julia is started.

デフォルトの最上位定義とベアモジュール

using Baseに加えて、モジュールには eval関数と include 関数の定義も自動的に含まれ、そのモジュールのグローバルスコープ内の式やファイルを評価します。

これらのデフォルトの定義が不要な場合は、代わりにキーワード baremodule を使用してモジュールを定義できます (注: baremoduleキーワードを使っても Core はインポートされます)。baremodule の使用、という観点から、標準的なモジュールの動作を見てみると:

baremodule Mod

using Base

eval(x) = Core.eval(Mod, x)
include(p) = Base.include(Mod, p)

...

end

相対モジュールパスと絶対モジュールパス

using Foo 文を記述されていると、システムは最上位モジュールの内部テーブルを参照して Foo という名前のモジュールを探します。モジュールが存在しない場合、システムは require(:Foo) を試み、通常はインストールされたパッケージからコードを読み込みます。

しかし、一部のモジュールにはサブモジュールが含むものがあり、最上位以外の(Mainから直接アクセスできない)モジュールにアクセスする必要がある場合があります。これを行うには 2 つの方法があります。1 つ目は、using Base.Sort などのように絶対パスを使用することです。2 つ目は相対パスを使用する方法で、現在のモジュールや、サブモジュールを含むモジュールから、サブモジュールのインポートを簡単におこなえます:

module Parent

module Utils
...
end

using .Utils

...
end

ここではモジュール Parent にはサブモジュール Utils が含まれており、Parent のコードは Utils の内容を参照する必要があります。これは、using 対象のパスをピリオドから開始することによって行われます。先頭にさらにもう一つピリオドを追加すると、モジュール階層のレベルが上がります。たとえば、using ..UtilsParentモジュール自体ではなく、またさらにその上のParentモジュールを含むモジュール階層で、Utils を探します。

相対インポート修飾子はusing ステートメントと import ステートメントでのみ有効です。

名前空間に関する雑記

名前が修飾されている場合 (例: Base.sin) は、エクスポートされていない場合でもアクセスできます。 これは、多くの場合、デバッグ時に便利です。修飾名を関数名として使用してメソッドを追加することもできます。ただし、構文のあいまいさが生じるため、別のモジュールに含まれる関数で、例えば演算子Base.+のように関数名が記号のみで構成されるものにメソッドを追加したいときには、その関数を参照するのに Base.:+ を使用してください。演算子が複数文字の場合は、次のように括弧で囲んでください: Base.:(==).

インポートおよびエクスポート文の中で、マクロ名は@を付けてimport Mod.@macのように書かれます。他のモジュールのマクロはMod.@mac または @Mod.mac のようにして呼び出すことができます。

構文 M.x = y という構文では、別のモジュールのグローバル変数に代入をすることはできません。グローバル変数の代入は常にローカルなモジュールで行われます。

変数名は、global x のように最上位で宣言することで、変数への代入を行うこと無く、名前を予約することができます。これにより、ロード後に初期化されるグローバル変数名の競合を防ぐことができます。

モジュールの初期化とプリコンパイル

大きなモジュールの読み込みには数秒かかることがあります。モジュール内のすべてのステートメントを実行するには、多くの場合、大量のコードをコンパイルする必要があるためです。 Julia は、この時間を短縮するために、モジュールの事前コンパイル済みキャッシュファイルを作成しておいて、この時間を短縮します。

インクリメンタルプリコンパイルされたモジュールファイルは、importusing が使われてモジュールがロードする際に自動的に作成されます。これにより、初めてインポートしたときに自動的にコンパイルされます。または、手動で Base.compilecache(Module Name)としてコンパイルさせることもできます。得られたキャッシュファイルは、DEPOT_PATH[1]/compiled/ に格納されます。その後、依存関係が変更されるたびに、モジュールは自動的に usingまたはimport時に再コンパイルされます。ここでいう依存関係とは、インポートするモジュール、Julia のビルド、include されrファイル、またはモジュール ファイル内の include_dependency(path) によって宣言された明示的な依存関係などのことです。

ファイルの依存関係については、依存関係の変更があったかどうかは、include によって読み込まれたり、include_dependency によって明示的に追加された各ファイルについて、その更新時刻 (mtime) が変わっていないかどうか、あるいは (1秒以下の精度でmtime をコピーできないシステムに対応するために)最も近い秒に切り捨てられた変更時間と等しいかどうかで判断されます。また、require の検索ロジックで選択されたファイルへのパスが、プリコンパイル ファイルを作成したパスと一致するかどうかも考慮されます。また、現在のプロセスが既に読み込んでいる一連の依存関係も考慮されます。実行中のシステムとプリコンパイル キャッシュの間に不整合が生じないように、実行中にファイルが変更されたり削除されても、それらのモジュールを再コンパイルしません。

モジュールを事前コンパイルするのが安全でないことがわかっている場合 (その理由の一例はこの後に述べますが) 、モジュール ファイル(通常はファイルの上部)に __precompile__(false)を記入してください。 これにより、Base.compilecache がエラーをスローして、using / import が現在のプロセスに直接そのモジュールを読み込み、プリコンパイルとキャッシュをスキップします。 これにより、モジュールが他のプリコンパイル済みモジュールによってインポートされるのを防ぐことができます。

モジュールのコードを書いているときには、増分共有ライブラリの作成に固有の特定の動作に注意する必要があります。たとえば、外部状態は保持されません。 これに対応するために、実行時 に 処理される必要のある初期化ステップと、コンパイル時に処理してもよいステップとを明示的に分離してください。Julia では、モジュールに __init__()関数を定義し、ここに、実行時に処理される必要がある初期化ステップを書くことができます。この関数は(--output-*による)コンパイル中には呼び出されず、 コードが実行されている間に 1 回だけ実行されると想定できます。 もちろん、必要に応じて手動で呼び出すこともできますが、デフォルトでは、この関数が、処理を実行中マシンの計算状態を扱っていると想定できます。ここでいう計算状態とは、プリコンパイルイメージに含まれる必要がない

__init__()関数は、モジュールがプロセスにロードされたあとで、呼ばれます。これは、インクリメンタル コンパイル (-output-incremental=yes) でロードされる場合も該当しますが、フルコンパイルプロセスにロードされる場合は該当しません。

特に、モジュール内に__init__()関数を定義した場合、(例えば、importusingrequireなどを用いて)モジュールがロードされた直後、初めての実行時に __init__()関数が呼ばれます。 (つまり、モジュール内のすべての文が実行された後に__init__ は 1 回だけ呼び出されます)。モジュールが完全にインポートされた後に呼び出されるため、サブモジュールやその他のインポートされたモジュールの __init__()関数はそれを囲むモジュールの__init__よりも 前に 呼び出されます。

__init__ の 2 つの典型的な用途は、外部 C ライブラリのランタイム初期化関数を呼び出し、外部ライブラリによって返されるポインターを含むグローバル定数を初期化することです。 たとえば、実行時に foo_init() 初期化関数を呼び出す必要がある C ライブラリ libfoo を呼び出すとします。libfoo で定義された void *foo_data() 関数の戻り値を保持するグローバル定数 foo_data_ptr も定義するとします。この時、ポインタのアドレスは実行するごとに変わるため、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__のような関数内でグローバルにシンボルを定義することは可能であることに注目してください。これは動的言語を使用する利点の 1 つですが、グローバルスコープで定数とすることで、コンパイラが型を認識し、より最適化されたコードを生成できるようになります。明らかに、モジュール内の他のグローバルも foo_data_ptr に依存するものについては、__init__ で初期化する必要があります。

ccall を使わずに生成されるほとんどの Julia オブジェクトを含む定数は__init__ に配置する必要はありません。 そういう定数の定義は、プリコンパイルでき、キャッシュされたモジュールイメージからロードすることができます。これには、配列のようにヒープを割り当てられた複雑なオブジェクトも含まれますが、生のポインター値を返すルーチンは、プリコンパイルがうまく動作するように実行時に呼び出す必要があります(Ptr オブジェクトは、isbits オブジェクト内に隠されていない限り、null ポインターに なります)。これには、Julia 関数 cfunctionpointerの戻り値が含まれます。

辞書型、集合型、または一般にhash(key)メソッドの出力に何らか依存するものは取り扱いが厄介なケースです。key が数値、文字列、シンボル、範囲、Expr、またはこれらの型の複合型 (配列、タプル、集合、Pairなどを介して) である一般的なケースでは、プリコンパイルしても安全です。 ただし、いくつかのキー型、例えば FunctionDataType そして hashメソッドを定義していない汎用ユーザー定義の型などについは、フォールバック hashメソッドは、(objectid を介する) オブジェクトのメモリ アドレスに依存しており、実行するたびに変更されるかもしれません。これらのキータイプのいずれかを持っている場合、または安全であると確信が持てない場合は、__init__ 関数内からこの辞書を初期化できます。あるいは、IdDict辞書型を使うこともできます。これはコンパイル時に安全に初期化できるように、プリコンパイルによって特殊な処理がされます。

プリコンパイルを使用する場合は、コンパイル フェーズと実行フェーズの違いを明確に理解しておく必要があります。このモードでは、Julia がコンパイル済みコードを生成するスタンドアロンインタプリタではなく、任意の Julia コードの実行を可能にするコンパイラであることが明らかになることがよくあります。

その他既知の失敗しがちなシナリオは下記の通りです:

  1. グローバルカウンター(例えば、オブジェクトを一意に識別する)について、次のコードを考えてみましょう:

    mutable struct UniquedById
        myid::Int
        let counter = 0
            UniquedById() = new(counter += 1)
        end
    end

    このコードの目的は、すべてのインスタンスに一意の ID を与えるものでしたが、コンパイルの最後にカウンタ値はが記録されます。このあとで、インクリメンタルコンパイルされたモジュールが使われるときはいつも、同じカウンタ値から起動します。

    objectid は、メモリポインタをハッシュするものですが、似たような問題があることに注意 ( 下記の Dict 用法参照 ) 。

    もう 1 つの方法は、マクロを使用して @__MODULE__ をキャプチャし、現在の 'カウンタ' 値を使用して単独で格納することです。ただし、このグローバル状態に依存しないようにコードを再設計するほうがよいかもしれません。

  2. 連想コレクション (DictSet など) は __init__で再ハッシュする必要があります。(将来的には、初期化関数を登録するメカニズムが提供されるかもしれません)。

  3. コンパイル時の副作用の影響はは実行時にも残ります。例: 他の Julia モジュールの配列またはその他の変数の内容の変更、ファイルまたはデバイスを開くためにハンドルの保持、他のシステム リソース (メモリを含む) へのポインターの格納。

  4. ルックアップパスではなく、直接参照をすることで、別のモジュールからグローバルな状態の「コピー」を意図せず作成してしまうこと。例えば、(グローバルスコープでは):

    #mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =#
    # instead use accessor functions:
    getstdout() = Base.stdout #= best option =#
    # or move the assignment into the runtime:
    __init__() = global mystdout = Base.stdout #= also works =#

コードのプリコンパイル中に可能な処理には様々な制限が課されます。ユーザーが間違った状況を避けるのを支援するためです:

  1. 別のモジュールで副作用を引き起こすevalの呼び出し。また、インクリメンタルなプリコンパイルのフラグが設定されている時には警告が出力されます。
  2. __init__()が処理開始された後の ローカルスコープからの global const 宣言 (これに対してエラーを発生させようという計画については、issue #12010 を参照)
  3. モジュールの置き換えは、増分プリコンパイルの実行中にランタイム エラーを引き起こします。

その他の注意点は次のとおりです:

  1. ソース ファイル自体に変更が加えられた(Pkg.update 含む)後に 、コードの再読み込み/キャッシュの無効化は実行されません 、Pkg.rm の後にクリーンアップは行われません
  2. Reshape された配列のメモリ共有動作は、プリコンパイルによって無視されます (各ビューは独自のコピーを取得します)
  3. コンパイル時と実行時の間にファイルシステムが変更されないことを期待するもの: 例えば、@__FILE__/source_path()で実行時にリソースを探すためとか、BinDeps の @checked_lib マクロなど。こういうことは避けられないことがあります。ただし、可能であれば、コンパイル時にリソースをモジュールにコピーしておいて、実行時にそのリソースを探す必要を無くしておくのはよいプラクティスです。
  4. WeakRef オブジェクトとファイナライザは、現在シリアライザーによって適切に処理されていません (これは今後のリリースで修正されます)。
  5. 通常は、シリアライザーを混乱させ、望まぬ結果を導く可能性があるため、Method, MethodInstance, MethodTable, TypeMapLeve, TypeMapEntry そしてこれらのオブジェクトのフィールドなどの、内部メタデータオブジェクトのインスタンスへの参照を補足しないようにすることをお考めします。参照を補足することは必ずしもエラーではありませんが、システムがこれらの一部をコピーし、単一・唯一のインスタンスを作成するようにすればよい話です。

モジュールの開発中に、インクリメンタルプリコンパイルをオフにすると便利な場合があります。コマンドラインフラグ --compiled-modules={yes|no} を使用すると、モジュールのプリコンパイルのオンとオフを切り替うことができます。Julia が --compiled-modules=no で起動すると、モジュールとモジュールの依存関係を読み込むときに、コンパイル キャッシュ内のシリアル化されたモジュールは無視されます。--compiled-modules=noにしていてもBase.compilecache は手動で呼び出すことができます。このコマンド ライン フラグの状態は Pkg.build に渡され、パッケージのインストール、更新、および明示的なビルド時に自動プリコンパイルトリガを無効にします。