コードの読み込み

情報

この章はパッケージ読み込みの技術的詳細を説明します。パッケージをインストールするには、Julia 組み込みのパッケージマネージャ Pkg を使ってアクティブな環境にパッケージを追加してください。アクティブな環境が持つパッケージを利用するには import X または using X を実行します。こういったコマンドの意味はモジュールの章で説明されています。

定義

Julia にはコードを読み込むメカニズムが二種類あります:

  1. コードのインクルード (include("source.jl")): 単一のプログラムを複数のソースファイルに分けるためのメカニズムです。include("source.jl") という式を評価すると、include の呼び出しが起こったモジュールのグローバルスコープでファイル source.jl の内容が評価されます。include("source.jl") を複数回呼び出すと、source.jl の内容は複数回評価されます。インクルードパス source.jlinclude の呼び出しが起こったファイルからの相対パスとして解釈されます。このためインクルードを使うとソースツリーの一部分の移動が簡単に行えます。REPL では、インクルードパスは現在の作業ディレクトリ pwd() からの相対パスとして解釈されます。
  2. パッケージの読み込み (import Xusing X): このメカニズムはパッケージを読み込みます。パッケージとは再利用可能な独立した Julia コードの集合をモジュールとしてまとめたものであり、そのモジュールはインポートしたモジュールから X という名前で参照できるようになります。同じ Julia セッションで同じパッケージを複数回インポートしても、読み込みは最初のインポートでだけ起こります ──二回目以降のインポートでは初回にインポートされたモジュールに対する参照が返ります。ただし import X は文脈によって異なるパッケージを読み込む可能性があることに注意してください: X はメインプロジェクトに含まれる X という名前のパッケージを表せますが、依存パッケージに含まれる X という名前のパッケージも表せます。詳細は後述します。

コードのインクルードは非常に明快かつ単純です: ソースファイルを呼び出し側の文脈で評価するだけです。パッケージの読み込みはコードのインクルードの上に構築されており、異なる用途で使われます。この章の残りの部分ではパッケージの読み込みの振る舞いとメカニズムに焦点を当てます。

パッケージ (package) は標準的なレイアウトを持ったソースツリーであり、他の Julia プロジェクトから再利用できる機能を提供します。パッケージは import X または using X という文で読み込みます。この二つの文は X という名前のモジュール ──パッケージコードの読み込み結果── をインポートを行ったモジュールで利用可能にします。import X における X の意味は文脈依存です: 読み込まれるパッケージ X がどれかはインポート文が起こったコードの場所よって変わります。具体的に言うと、import X は二つのステージで処理されます: 最初に現在の文脈において Xどのパッケージとして定義されるかを計算し、それから Xどこにあるかを計算します。

この二つの要素の計算では、LOAD_PATH の各要素が表すプロジェクト環境からプロジェクトファイル (Project.toml または JuliaProject.toml) またはマニフェストファイル (Manifest.toml または JuliaManifest.toml) もしくはソースファイルを含むフォルダが探索されます。

連邦制パッケージ管理

ほとんどの場合、パッケージはその名前で一意に識別できます。しかしときには、プロジェクト内の二つの異なるパッケージに同じ名前を使わなければならないこともあります。どちらかのパッケージの名前を変更すれば問題を解決できるかもしれませんが、大規模な共有コードベースでそれを強制すると大きな混乱が起きます。Julia のコード読み込みメカニズムでは改名が強制されることはなく、同じパッケージ名でアプリケーションの異なる部分に属する異なるパッケージを参照できるようになっています。

Julia は連邦制パッケージ管理 (federated package management) をサポートします。これは独立した団体partyがパブリックおよびプライベートなパッケージとそのレジストリを独自に管理でき、一つのプロジェクトは異なる複数のレジストリに属するパブリックおよびプライベートなパッケージに依存できることを意味します。様々なレジストリが持つパッケージのインストールと管理は共通のツールとワークフローを使って行われます。Julia プロジェクトが依存パッケージのインストールと管理に使う共通のツールは Julia に付属する Pkg パッケージマネージャであり、このツールがプロジェクトファイル (あるプロジェクトが依存するパッケージを記述する) とマニフェストファイル (完全な依存グラフを含む特定のバージョンにおけるプロジェクトのスナップショットを記述する) の作成と操作を支援します。

連邦制パッケージ管理を採用する帰結の一つが、パッケージの名前を管理する中央権威の不存在です。異なるグループが関連の無いパッケージに対して同じ名前を用いる可能性がありますが、グループ同士は協調せず、そもそもお互いを知らない可能性もあるので、この衝突を避ける方法はありません。また名前を管理する中央権威が存在しないので、単一のプロジェクトが同じ名前の異なるパッケージに依存することもあります。Julia のパッケージ読み込みメカニズムはパッケージの名前がグローバルに一意であることを要求せず、単一のプロジェクトの依存グラフに含まれるパッケージであっても名前は一意でなくて構いません。

パッケージの識別に使われるのは名前ではなく、パッケージが作成されるときに割り当てられる汎用一意識別子 (universally unique identifier, UUID) です。通常は Pkg が UUID の収集や追跡を行うので、この長たらしい 128 ビットの識別子を直接扱う必要はありません。ただ、この UUID は「X が指すパッケージは何?」という質問に対する決定的な答えを提供します。

この分散型の名前付けはいくらか抽象的なので、この問題を理解するために具体例を一つ説明しましょう。App というアプリケーションを開発していて、AppPubPriv のという名前の二つのパッケージを使っているとします。Priv はあなたが作ったプライベートなパッケージで、Pub はあなたが管理していないパブリックなパッケージだとします。

あなたが Priv を作ったとき同じ名前のパブリックなパッケージはありませんでしたが、後に Priv という同じ名前の関係ないパッケージが公開され有名になりました。そして Pub パッケージがパブリックな Priv を使い始めました。そのため次に Pub を更新してバグ修正や新機能を手に入れようとすると、App は二つの Priv パッケージに依存することになります。App はプライベートな Priv に直接依存しており、Pub を通して新しいパブリックな Priv パッケージにも間接的に依存しています。二つの Priv パッケージは異なるパッケージなので、App が正しい動作を続けるには import Priv という式が App のコードと Pub で異なる Priv パッケージを参照しなければなりません。

この問題に対処するために、Julia のパッケージ読み込みメカニズムは二つの Priv パッケージを UUID で区別し、文脈 (import を呼び出したモジュール) に応じて正しいパッケージを選択します。パッケージの区別は環境によって行われます。以降の節では環境を使ったパッケージの選択について説明します。

環境

環境 (environment) は様々なコードの文脈における import Xusing X の意味、そしてこれらの式によって読み込まれるファイルを定めます。Julia は二種類の環境を認識します:

  1. プロジェクト環境 (project environment) はプロジェクトファイルと省略可能なマニフェストファイルを持つディレクトリが形成する明示的な環境です。プロジェクトファイルは依存パッケージの名前と識別子を与えます。マニフェストファイルは、もし存在すれば、直接的および間接的な依存パッケージを含む完全な依存グラフおよび正しいバージョンを探索して読み込むのに十分な情報を与えます。
  2. パッケージディレクトリ環境 (package directory environment) はパッケージのソースツリーをサブディレクトリに持つディレクトリが形成する暗黙な環境です。X がパッケージディレクトリのサブディレクトリで、X/src/X.jl が存在するなら、パッケージディレクトリ環境でパッケージ X が利用可能になり、X/src/X.jl が読み込まれるソースファイルとなります。

二つの環境を混ぜることもでき、その場合はスタック環境 (stacked environment) が形成されます。これはプロジェクト環境とパッケージディレクトリ環境を順序付けて積み重ね、一つに合成した環境です。スタック環境から利用可能なパッケージや読み込むパッケージのディレクトリを決定するときは、そのスタック環境に含まれる環境が持つパッケージの優先順位や可視性の規則が組み合わさります。例えば Julia の LOAD_PATH はスタック環境を形成します。

これらの環境はそれぞれ異なる用途を持ちます:

環境が定める三つの写像

高いレベルで言えば、各環境は三つの写像を定義します: ルート写像、グラフ写像、そしてパス写像です。import X の意味を解決するときは最初にルート写像またはグラフ写像を使って X が識別され、パス写像を使って X のソースコードが検索されます。三つの写像の意味は次の通りです:

三種類の環境はそれぞれ異なる方法でこの三つの写像を定義します。この方法について以降の節で見ていきます。

情報

簡単のため、この章の例ではルート写像・グラフ写像・パス写像の完全なデータ構造を示しています。しかし Julia のパッケージ読み込みコードはこういったデータ構造を明示的には計算せず、パッケージの読み込みで実際に必要になったときに必要な分だけを怠惰に計算します。

プロジェクト環境

プロジェクトファイル Project.toml を含むディレクトリはプロジェクト環境を定めます。同じディレクトリにマニフェストファイル Manifest.toml があればこれも環境設定の一部となりますが、このファイルは省略可能です。この二つのファイルの名前はそれぞれ JuliaProject.tomlJuliaManifest.toml でも構いません。Julia の付いた名前を使うと Project.tomlManifest.toml は無視されます。こうなっているのは、Project.tomlManifest.toml を特殊なファイルとみなす他のツールと Julia を共存できるようにするためです。ただし Julia だけを使うプロジェクトでは Project.tomlManifest.toml の名前が推奨されます。

プロジェクト環境が持つルート写像・グラフ写像・パス写像は次のように定義されます:

ルート写像

ルート写像はプロジェクトファイルが指定します。具体的には、トップレベルの name エントリと uuid エントリおよび [deps] セクションで決まります (全て省略可能)。上で考えた想像上のアプリケーション App に対するプロジェクトファイルの一例は次の通りです:

name = "App"
uuid = "8f986787-14fe-4607-ba5d-fbff2944afa9"

[deps]
Priv = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
Pub  = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"

このプロジェクトファイルが意味するルート写像を Julia の辞書として表現すると次のようになります:

roots = Dict(
    :App  => UUID("8f986787-14fe-4607-ba5d-fbff2944afa9"),
    :Priv => UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"),
    :Pub  => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
)

Julia は App のコードで import Priv という文を見つけると、ルート写像に Priv を適用した roots[:Priv] を探索し、この文脈において読み込むべき Priv パッケージの UUID ba13f791-ae1d-465a-978b-69c3ad90f72b を取得します。この UUID はメインアプリケーションが import Priv または using Priv を評価したときに読み込むべき Priv パッケージを表します。

グラフ写像

グラフ写像はマニフェストファイルが (存在すれば) 指定します。マニフェストファイルが存在しなければグラフ写像は空です。マニフェストファイルは直接的および間接的な依存パッケージのそれぞれに対して、パッケージの UUID・ソースツリーのハッシュ・ソースコードへの明示的なパスを記述します。App のマニフェストファイルの一例を示します:

[[Priv]] # プライベートな Priv
deps = ["Pub", "Zebra"]
uuid = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
path = "deps/Priv"

[[Priv]] # パブリックな Priv
uuid = "2d15fe94-a1f7-436c-a4d8-07a9a496e01c"
git-tree-sha1 = "1bf63d3be994fe83456a03b874b409cfd59a6373"
version = "0.1.5"

[[Pub]]
uuid = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"
git-tree-sha1 = "9ebd50e2b0dd1e110e842df3b433cb5869b0dd38"
version = "2.1.4"

  [Pub.deps]
  Priv = "2d15fe94-a1f7-436c-a4d8-07a9a496e01c"
  Zebra = "f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"

[[Zebra]]
uuid = "f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"
git-tree-sha1 = "e808e36a5d7173974b90a15a353b564f3494092f"
version = "3.4.2"

このマニフェストファイルには App プロジェクトの完全な依存グラフ (の可能性の一つ) が記述されています:

この依存グラフを辞書で表すと、次のようになります:

graph = Dict(
    # プライベートな Priv:
    UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b") => Dict(
        :Pub   => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
        :Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
    ),
    # パブリックな Priv:
    UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c") => Dict(),
    # Pub:
    UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1") => Dict(
        :Priv  => UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"),
        :Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
    ),
    # Zebra:
    UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62") => Dict(),
)

Julia は Pub パッケージ (UUID: c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1) で import Priv を見つけると、この依存グラフから次の値を検索します:

graph[UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1")][:Priv]

この値は 2d15fe94-a1f7-436c-a4d8-07a9a496e01c であり、これは Pub パッケージの文脈では import Priv がパブリックな Priv パッケージを参照することを示します。App が直接依存するプライベートな Priv パッケージではありません。この仕組みがあるおかげで Priv という名前はメインプロジェクトとその依存パッケージ Pub で異なるパッケージを参照でき、パッケージエコシステムにおける名前の重複が許されます。

メインの App 内のコードで import Zebra を評価すると何が起こるでしょうか? Zebra はプロジェクトファイルに存在しないので、Zebra はマニフェストファイルに含まれるにもかかわらず、インポートは失敗します。また import Zebra がパブリックな Priv パッケージ (UUID: 2d15fe94-a1f7-436c-a4d8-07a9a496e01c) で起きたとしても、インポートは失敗します。パブリックな Priv パッケージはマニフェストファイルで依存パッケージを宣言していないので、どんなパッケージも読み込むことはできません。Zebra パッケージを読み込めるのは、それに依存するとマニフェストファイルで明示的に宣言したパッケージ、今の例では Pub とプライベートな Priv だけです。

パス写像

プロジェクト環境のパス写像はマニフェストファイルから抽出されます。パッケージの名前が X で UUID が uuid のとき、まず次の手続きが実行されます:

  1. uuidX がプロジェクトファイルにある値と一致するなら:
    • もしプロジェクトファイルにトップレベルの path エントリが存在するなら、それをプロジェクトファイルがあるディレクトリからの相対パスとして解釈したものを返す。
    • そうでなければ、プロジェクトファイルがあるディレクトリからの相対パス src/X.jl を返す。
  2. 一つ目の規則が当てはまらなかった場合、もしプロジェクトファイルに対応するマニフェストファイルが存在して uuid の値が一致する項目があるなら:
    • その項目に path エントリがあるなら、そのパス (をマニフェストファイルがあるディレクトリからの相対パスと解釈したパス) を返す。
    • その項目に git-tree-sha1 エントリがあるなら、uuidgit-tree-sha1 から決定的なハッシュ slug を計算し、Julia のグローバル変数 DEPOT_PATH に含まれるディレクトリのそれぞれを起点として相対パス packages/X/$slug を検索する。最初に見つかったディレクトリを返す。

この手続きがディレクトリパスを返した場合はそれに src/X.jl を付けたパスが、ファイルパスを返した場合はそれがそのまま、パッケージのエントリーポイントのファイルパスとなります。この手続きが返らなければ uuid に対応するパスは存在しないということで検索は失敗し、適切なパッケージのインストールか修正処理 (依存パッケージの宣言を見直すなど) を行うよう促すメッセージがユーザーに表示されます。

上記のマニフェストファイルの例で一つ目の Priv パッケージ (UUID: ba13f791-ae1d-465a-978b-69c3ad90f72b) のパスを検索するとき、Julia はこの UUID を持つマニフェストファイルの項目を検索し、path エントリを読み、App を起点とした相対パス deps/Priv を見に行きます。App/home/me/projects/App にあるとすれば Julia は /home/me/projects/App/deps/Priv の存在を確認し、Priv をそこから読み込みます。

これに対して、もう一つの Priv パッケージ (UUID: 2d15fe94-a1f7-436c-a4d8-07a9a496e01c) を読み込むときは、Julia はこの UUID を持つマニフェストファイルの項目を検索し、path エントリが存在せず代わりに git-tree-sha1 エントリが存在することを確認します。その後 Julia はこの UUID と SHA-1 の組に対する slug を計算し、HDkrT を得ます (計算の詳細は重要ではありませんが、この計算は一貫していて決定的です)。これは読み込もうとしている Priv パッケージが DEPOT_PATH のいずれかから packages/Priv/HDkrT/ と進んだディレクトリに存在することを意味します。DEPOT_PATH["/home/me/.julia", "/usr/local/julia"] だとすれば、Julia は次のパスが存在するかを順に確認します:

  1. /home/me/.julia/packages/Priv/HDkrT
  2. /usr/local/julia/packages/Priv/HDkrT

Julia は最初に見つけたディレクトリをパブリックな Priv パッケージが存在するディレクトリとみなし、ファイル packages/Priv/HDKrT/src/Priv.jl を読み込みます。

例として使ってきた App プロジェクト環境におけるパス写像の一例を示します。この写像は上記のマニフェストファイルが定める依存グラフを使ったローカルのファイルシステムを検索によって定まります:

paths = Dict(
    # プライベートな Priv:
    (UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"), :Priv) =>
        # App レポジトリ内の相対エントリーポイント:
        "/home/me/projects/App/deps/Priv/src/Priv.jl",
    # パブリックな Priv:
    (UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"), :Priv) =>
        # システムのパッケージ格納場所に保存されたパッケージ
        "/usr/local/julia/packages/Priv/HDkr/src/Priv.jl",
    # Pub:
    (UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"), :Pub) =>
        # ユーザーのパッケージ格納場所に保存されたパッケージ
        "/home/me/.julia/packages/Pub/oKpw/src/Pub.jl",
    # Zebra:
    (UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"), :Zebra) =>
        # package installed in the system depot:
        "/usr/local/julia/packages/Zebra/me9k/src/Zebra.jl",
)

この例には三つの異なるパッケージの格納場所が使われています (一つ目と三つ目はデフォルトの LOAD_PATH の一部です):

  1. プライベートな Priv パッケージは App レポジトリにベンダー化されています。
  2. パブリックな Priv パッケージと Zebra パッケージはシステムのパッケージ格納場所に存在します。これらのパッケージはシステム管理者によってインストール・管理され、システムの全ユーザーが利用できます。
  3. Pub パッケージはユーザーのパッケージ格納場所に存在します。このパッケージはユーザーによってインストールされており、インストールを行ったユーザーからのみ利用できます。

パッケージディレクトリ環境

パッケージディレクトリ環境は名前の衝突を解決する機能を持たない簡単な環境を提供します。この環境では、パッケージのように見えるサブディレクトリがトップレベルのパッケージとなります。具体的に言うと、次の “エントリーポイント” ファイルのいずれかが存在するとき、そのパッケージディレクトリ環境はパッケージ X を持つとみなされます:

パッケージディレクトリ環境に含まれるパッケージがインポートできるパッケージは、パッケージがプロジェクトファイルを持つかどうかで変わります:

ルート写像

ルート写像はパッケージディレクトリ環境を探索して存在するパッケージのリストを生成することで定まります。そのとき UUID は次のように割り当てられます: フォルダ X に含まれるパッケージについて...

  1. X/Project.toml が存在してトップレベル uuid エントリを持つなら、UUID はその値となる。
  2. X/Project.toml が存在してトップレベルの uuid エントリを持たないなら、X/Project.toml の正準パス (実際のパス) をハッシュして得られるダミー UUID が使われる。
  3. それ以外の場合 (Project.toml が存在しないとき) は、UUID は全てゼロの nil UUID となる。

グラフ写像

プロジェクトディレクトリ環境のグラフ写像 (依存グラフ) はパッケージを表すサブディレクトリのプロジェクトファイルの内容 (および有無) で決まります。規則は次の通りです:

例として、次の構造と内容を持つパッケージディレクトリを考えます:

Aardvark/
    src/Aardvark.jl:
        import Bobcat
        import Cobra

Bobcat/
    Project.toml:
        [deps]
        Cobra = "4725e24d-f727-424b-bca0-c4307a3456fa"
        Dingo = "7a7925be-828c-4418-bbeb-bac8dfc843bc"

    src/Bobcat.jl:
        import Cobra
        import Dingo

Cobra/
    Project.toml:
        uuid = "4725e24d-f727-424b-bca0-c4307a3456fa"
        [deps]
        Dingo = "7a7925be-828c-4418-bbeb-bac8dfc843bc"

    src/Cobra.jl:
        import Dingo

Dingo/
    Project.toml:
        uuid = "7a7925be-828c-4418-bbeb-bac8dfc843bc"

    src/Dingo.jl:
        # 何もインポートしない

対応するルート写像を辞書として示します:

roots = Dict(
    # nil UUID (プロジェクトファイルが無い)
    :Aardvark => UUID("00000000-0000-0000-0000-000000000000"),
    # パスから生成されたダミーの UUID
    :Bobcat   => UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"),
    # プロジェクトファイルにある UUID
    :Cobra    => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"),
    # プロジェクトファイルにある UUID
    :Dingo    => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
)

対応するグラフ写像を辞書として示します:

graph = Dict(
    # Bobcat:
    UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf") => Dict(
        :Cobra => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"),
        :Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
    ),
    # Cobra:
    UUID("4725e24d-f727-424b-bca0-c4307a3456fa") => Dict(
        :Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
    ),
    # Dingo:
    UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc") => Dict(),
)

一般的な注意事項を示します:

  1. プロジェクトファイルを持たないパッケージは、トップレベルにある任意の依存パッケージに依存できます。パッケージディレクトリ環境に含まれる全てのパッケージはトップレベルにあるので、これは環境内の全てのパッケージをインポートできることを意味します。
  2. プロジェクトファイルを持つパッケージはプロジェクトファイルを持たないパッケージに依存できません。なぜなら、プロジェクトファイルを持つパッケージが依存できるのは graph に含まれるパッケージだけであり、プロジェクトファイルを持たないパッケージは garph に加われないからです。
  3. プロジェクトファイルを持ち明示的な UUID を持たないパッケージに依存できるのは、プロジェクトファイルを持たないパッケージだけです。なぜなら、こういったパッケージに割り当てられるダミーの UUID は厳密に内部でのみ利用されるからです。

今の例では、この注意事項は次のように適用されます:

パス写像

パッケージディレクトリ環境のパス写像は簡単です: UUID とサブディレクトリの名前を受け取り、エントリーポイントのパスを返します。例として使っているプロジェクトのディレクトリが /home/me/animals だとすれば、パス写像は次の辞書で表されます:

paths = Dict(
    (UUID("00000000-0000-0000-0000-000000000000"), :Aardvark) =>
        "/home/me/AnimalPackages/Aardvark/src/Aardvark.jl",
    (UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), :Bobcat) =>
        "/home/me/AnimalPackages/Bobcat/src/Bobcat.jl",
    (UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), :Cobra) =>
        "/home/me/AnimalPackages/Cobra/src/Cobra.jl",
    (UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), :Dingo) =>
        "/home/me/AnimalPackages/Dingo/src/Dingo.jl",
)

パッケージディレクトリ環境では、定義により、全てのパッケージが決まった名前のエントリーポイントファイルを持つサブディレクトリなので、paths のエントリは必ずこの形になります。

スタック環境

最後に説明する三つ目の環境は、複数の環境を積み重ねて組み合わせることで、各環境で利用可能なパッケージを単一の環境で利用可能にしたものです。この合成環境はスタック環境 (stacked environment) と呼ばれます。

例えば Julia のグローバル変数 LOAD_PATH はスタック環境を定義します ──これは Julia プロセスが動作する環境です。もし Julia プロセスのアクセスを一つのプロジェクトやパッケージのディレクトリだけに制限したいなら、それを LOAD_PATH の唯一のエントリとしてください。ただ、プロジェクトが依存していなくてもアクセスを与えておくと非常に役立つパッケージやツール ──標準ライブラリ・プロファイラ・デバッガ・個人的なユーティリティ関数など── もあります。こういったパッケージやツールを含む環境を LOAD_PATH に加えれば、プロジェクトにそれを加えなくてもトップレベルのコードからアクセスできるようになります。

スタック環境においてルート写像・グラフ写像・パス写像を表すデータ構造を組み合わせる方法は単純です: 写像は辞書として統合され、キーが衝突した場合にはスタックの上にある若いエントリが古いエントリより優先されます。言い換えると、もし stack = [env₁, env₂, …] というスタック環境はあるなら、次のようになります:

roots = reduce(merge, reverse([roots₁, roots₂, ]))
graph = reduce(merge, reverse([graph₁, graph₂, ]))
paths = reduce(merge, reverse([paths₁, paths₂, ]))

添え字付きの変数 rootsᵢ, graphᵢ, pathsᵢ はスタック内の同じ添え字の環境 envᵢ に対応します。reverse があるのは、merge 関数がキーの衝突で最後の引数を優先するためです。

この設計の特筆すべき特徴を示します:

  1. スタックの一番上にあるプライマリ環境はスタック環境に確実に埋め込まれます。プライマリ環境の完全な依存グラフはスタック環境全体の依存グラフにバージョンや依存パッケージを含めて無傷で存在することが保証されます。
  2. スタックの一番上にない (プライマリでない) 環境に含まれるパッケージは、最終的なスタック環境で互換性の無いバージョンが読み込まれる可能性があります。それぞれの環境内でパッケージが互換性を持っていたとしても、そのバージョンが読み込まれることは保証されません。互換性が失われるのは、スタックに含まれる自身より若い (上にある) 環境が自身の依存パッケージの異なるバージョンを使うことで、自身の依存パッケージ (のグラフとパスのいずれか、もしくは両方) が隠されたときです。

通常プライマリ環境はあなたが取り組むプロジェクトの環境であり、追加ツールはスタックでそれよりも下に位置するので、これは正しいトレードオフと言えます: ユーザーのプロジェクトと開発ツールのどちらかが壊れるなら、開発ツールを壊してユーザーのプロジェクトの動作を優先させるべきです。こういった非互換性が起きた場合には、開発ツールのバージョンをメインプロジェクトと互換なバージョンにアップグレードすることになるでしょう。

最後に

パッケージシステムにおける連邦制パッケージ管理と正確なソフトウェア再現性は困難であるもののそれだけの価値がある目標です。この二つの目標を達成するには多くの動的言語が持つものより複雑なパッケージ読み込みメカニズムが必要になりますが、達成できればスケーラビリティと再現性という静的言語に関連付けられることの多い特徴が手に入ります。典型的な Julia ユーザーは組み込みのパッケージマネージャを内部で起きていることを理解することなく使うことができるはずです。Pkg.add("X") とすれば (Pkg.activate("Y") で選択される) 適切なプロジェクトのマニフェストファイルにパッケージが追加され、それだけで import XX を読み込むようになります。


日本語 Julia 書籍 (Amazon アソシエイト)
1 から始める Julia プログラミング
Julia プログラミングクックブック―言語仕様からデータ分析、機械学習、数値計算まで
スタンフォード ベクトル・行列からはじめる最適化数学