コードの読み込み
定義
Julia にはコードを読み込むメカニズムが二種類あります:
-
コードのインクルード (
include("source.jl")
): 単一のプログラムを複数のソースファイルに分けるためのメカニズムです。include("source.jl")
という式を評価すると、include
の呼び出しが起こったモジュールのグローバルスコープでファイルsource.jl
の内容が評価されます。include("source.jl")
を複数回呼び出すと、source.jl
の内容は複数回評価されます。インクルードパスsource.jl
はinclude
の呼び出しが起こったファイルからの相対パスとして解釈されます。このためインクルードを使うとソースツリーの一部分の移動が簡単に行えます。REPL では、インクルードパスは現在の作業ディレクトリpwd()
からの相対パスとして解釈されます。 - パッケージの読み込み (
import X
とusing 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) をサポートします。これは独立した団体がパブリックおよびプライベートなパッケージとそのレジストリを独自に管理でき、一つのプロジェクトは異なる複数のレジストリに属するパブリックおよびプライベートなパッケージに依存できることを意味します。様々なレジストリが持つパッケージのインストールと管理は共通のツールとワークフローを使って行われます。Julia プロジェクトが依存パッケージのインストールと管理に使う共通のツールは Julia に付属する Pkg
パッケージマネージャであり、このツールがプロジェクトファイル (あるプロジェクトが依存するパッケージを記述する) とマニフェストファイル (完全な依存グラフを含む特定のバージョンにおけるプロジェクトのスナップショットを記述する) の作成と操作を支援します。
連邦制パッケージ管理を採用する帰結の一つが、パッケージの名前を管理する中央権威の不存在です。異なるグループが関連の無いパッケージに対して同じ名前を用いる可能性がありますが、グループ同士は協調せず、そもそもお互いを知らない可能性もあるので、この衝突を避ける方法はありません。また名前を管理する中央権威が存在しないので、単一のプロジェクトが同じ名前の異なるパッケージに依存することもあります。Julia のパッケージ読み込みメカニズムはパッケージの名前がグローバルに一意であることを要求せず、単一のプロジェクトの依存グラフに含まれるパッケージであっても名前は一意でなくて構いません。
パッケージの識別に使われるのは名前ではなく、パッケージが作成されるときに割り当てられる汎用一意識別子 (universally unique identifier, UUID) です。通常は Pkg
が UUID の収集や追跡を行うので、この長たらしい 128 ビットの識別子を直接扱う必要はありません。ただ、この UUID は「X
が指すパッケージは何?」という質問に対する決定的な答えを提供します。
この分散型の名前付けはいくらか抽象的なので、この問題を理解するために具体例を一つ説明しましょう。App
というアプリケーションを開発していて、App
が Pub
と Priv
のという名前の二つのパッケージを使っているとします。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 X
や using X
の意味、そしてこれらの式によって読み込まれるファイルを定めます。Julia は二種類の環境を認識します:
- プロジェクト環境 (project environment) はプロジェクトファイルと省略可能なマニフェストファイルを持つディレクトリが形成する明示的な環境です。プロジェクトファイルは依存パッケージの名前と識別子を与えます。マニフェストファイルは、もし存在すれば、直接的および間接的な依存パッケージを含む完全な依存グラフおよび正しいバージョンを探索して読み込むのに十分な情報を与えます。
- パッケージディレクトリ環境 (package directory environment) はパッケージのソースツリーをサブディレクトリに持つディレクトリが形成する暗黙な環境です。
X
がパッケージディレクトリのサブディレクトリで、X/src/X.jl
が存在するなら、パッケージディレクトリ環境でパッケージX
が利用可能になり、X/src/X.jl
が読み込まれるソースファイルとなります。
二つの環境を混ぜることもでき、その場合はスタック環境 (stacked environment) が形成されます。これはプロジェクト環境とパッケージディレクトリ環境を順序付けて積み重ね、一つに合成した環境です。スタック環境から利用可能なパッケージや読み込むパッケージのディレクトリを決定するときは、そのスタック環境に含まれる環境が持つパッケージの優先順位や可視性の規則が組み合わさります。例えば Julia の LOAD_PATH
はスタック環境を形成します。
これらの環境はそれぞれ異なる用途を持ちます:
- プロジェクト環境は再現性を提供します。メインプロジェクトのソースコードに加えてプロジェクト環境をバージョン管理システム (例えば git レポジトリ) にチェックインしておけば、依存パッケージを含むプロジェクト全体の正確な状態を再現できるようになります。特にマニフェストファイルは全ての依存パッケージの正確なバージョンをキャプチャし、そのときはソースツリーの暗号学的ハッシュが識別子として使われるので、
Pkg
は全ての依存パッケージの正しいバージョンを取得して正確に同じコードを実行していることを確認できます。 - パッケージディレクトリ環境は利便性を提供します。注意深く追跡されたプロジェクト環境が必要でないならこちらを使ってください。パッケージを適当な場所に配置してプロジェクト環境を作成することなく直接利用するときに有用です。
- スタック環境はプライマリ環境に対するツールの追加を可能にします。開発ツールを含む環境をスタックにプッシュすれば、それを REPL やスクリプトから利用可能にできます (こうしても各環境に含まれるパッケージからは使えません)。
環境が定める三つの写像
高いレベルで言えば、各環境は三つの写像を定義します: ルート写像、グラフ写像、そしてパス写像です。import X
の意味を解決するときは最初にルート写像またはグラフ写像を使って X
が識別され、パス写像を使って X
のソースコードが検索されます。三つの写像の意味は次の通りです:
-
ルート写像:
name::Symbol
⟶uuid::UUID
環境のルート写像はパッケージの名前を受け取って UUID を返します。ルート写像が定義されるのは環境のメインプロジェクトが利用可能なトップレベルの依存パッケージ (
Main
で読み込めるプロジェクト) に対してだけです。Julia はメインプロジェクトでimport X
に遭遇すると、roots[:X]
でX
の識別子を取得します。 -
グラフ写像:
context::UUID
⟶name::Symbol
⟶uuid::UUID
環境のグラフ写像は多段階であり、
context
という文脈を表す UUID を受け取って「名前から UUID への写像」を返します。返り値の写像はルート写像と同じ型を持ちますが、context
が表す文脈でのみ有効な写像となっています。Julia は UUID がcontext
のパッケージのコードでimport X
に遭遇すると、graph[context][:X]
でX
の識別子を取得します。 -
パス写像:
uuid::UUID
×name::Symbol
⟶path::String
環境のパス写像はパッケージの UUID と名前の組を受け取り、パッケージのエントリーポイントを含むソースファイルのパスを返します。
import X
中のX
の UUID がルート写像あるいはグラフ写像 (メインプロジェクトではルート写像、依存パッケージではグラフ写像) で識別されると、Julia はパッケージX
を読み込むためにインクルードすべきファイルのパスをpaths[uuid,:X]
で取得します。このファイルのインクルードによってX
という名前のモジュールが定義されるべきです。一度パッケージが読み込まれると、以降それと同じuuid
に解決されるインポートは既に読み込まれたパッケージモジュールへの新しい参照を作成するだけになります。
三種類の環境はそれぞれ異なる方法でこの三つの写像を定義します。この方法について以降の節で見ていきます。
簡単のため、この章の例ではルート写像・グラフ写像・パス写像の完全なデータ構造を示しています。しかし Julia のパッケージ読み込みコードはこういったデータ構造を明示的には計算せず、パッケージの読み込みで実際に必要になったときに必要な分だけを怠惰に計算します。
プロジェクト環境
プロジェクトファイル Project.toml
を含むディレクトリはプロジェクト環境を定めます。同じディレクトリにマニフェストファイル Manifest.toml
があればこれも環境設定の一部となりますが、このファイルは省略可能です。この二つのファイルの名前はそれぞれ JuliaProject.toml
と JuliaManifest.toml
でも構いません。Julia
の付いた名前を使うと Project.toml
と Manifest.toml
は無視されます。こうなっているのは、Project.toml
や Manifest.toml
を特殊なファイルとみなす他のツールと Julia を共存できるようにするためです。ただし Julia だけを使うプロジェクトでは Project.toml
と Manifest.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
プロジェクトの完全な依存グラフ (の可能性の一つ) が記述されています:
-
App
はPriv
という名前の二つの異なるパッケージを使っています。プライベートなPriv
はルートからの直接的な依存パッケージであり、パブリックなPriv
はPub
を通した間接的な依存パッケージです。この二つは UUID によって区別され、deps
も異なります:- プライベートな
Priv
はPub
とZebra
に依存します。 - パブリックな
Priv
に依存パッケージはありません。
- プライベートな
App
はPub
パッケージにも依存します。Pub
の依存パッケージはパブリックなPriv
と、Priv
が依存するのと同じZebra
パッケージです。
この依存グラフを辞書で表すと、次のようになります:
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
のとき、まず次の手続きが実行されます:
-
uuid
とX
がプロジェクトファイルにある値と一致するなら:- もしプロジェクトファイルにトップレベルの
path
エントリーが存在するなら、それをプロジェクトファイルがあるディレクトリからの相対パスとして解釈したものを返す。 - そうでなければ、プロジェクトファイルがあるディレクトリからの相対パス
src/X.jl
を返す。
- もしプロジェクトファイルにトップレベルの
- 一つ目の規則が当てはまらなかった場合、もしプロジェクトファイルに対応するマニフェストファイルが存在して
uuid
の値が一致する項目があるなら:- その項目に
path
エントリーがあるなら、そのパス (をマニフェストファイルがあるディレクトリからの相対パスと解釈したパス) を返す。 - その項目に
git-tree-sha1
エントリーがあるなら、uuid
とgit-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 は次のパスが存在するかを順に確認します:
/home/me/.julia/packages/Priv/HDkrT
/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
の一部です):
- プライベートな
Priv
パッケージはApp
レポジトリにベンダー化されています。 - パブリックな
Priv
パッケージとZebra
パッケージはシステムのパッケージ格納場所に存在します。これらのパッケージはシステム管理者によってインストール・管理され、システムの全ユーザーが利用できます。 Pub
パッケージはユーザーのパッケージ格納場所に存在します。このパッケージはユーザーによってインストールされており、インストールを行ったユーザーからのみ利用できます。
パッケージディレクトリ環境
パッケージディレクトリ環境は名前の衝突を解決する機能を持たない簡単な環境を提供します。この環境では、パッケージのように見えるサブディレクトリがトップレベルのパッケージとなります。具体的に言うと、次の "エントリーポイント" ファイルのいずれかが存在するとき、そのパッケージディレクトリ環境はパッケージ X
を持つとみなされます:
X.jl
X/src/X.jl
X.jl/src/X.jl
パッケージディレクトリ環境に含まれるパッケージがインポートできるパッケージは、パッケージがプロジェクトファイルを持つかどうかで変わります:
- パッケージがプロジェクトファイルを持つなら、その
[deps]
セクションで宣言されたパッケージだけをインポートできます。 - パッケージがプロジェクトファイルを持たないなら、トップレベルパッケージ ──REPL あるいは
Main
が読み込めるパッケージ── をインポートできます。
ルート写像
ルート写像はパッケージディレクトリ環境を探索して存在するパッケージのリストを生成することで定まります。そのとき UUID は次のように割り当てられます: フォルダ X
に含まれるパッケージについて...
X/Project.toml
が存在してトップレベルuuid
エントリーを持つなら、UUID はその値となる。X/Project.toml
が存在してトップレベルのuuid
エントリーを持たないなら、X/Project.toml
の正準パス (実際のパス) をハッシュして得られるダミー UUID が使われる。- それ以外の場合 (
Project.toml
が存在しないとき) は、UUID は全てゼロの nil UUID となる。
グラフ写像
プロジェクトディレクトリ環境のグラフ写像 (依存グラフ) はパッケージを表すサブディレクトリのプロジェクトファイルの内容 (および有無) で決まります。規則は次の通りです:
- パッケージを表すサブディレクトリがプロジェクトファイルを持たないなら、そのパッケージはグラフから取り除かれ、インポート文はトップレベル (メインプロジェクトや REPL) と同じように扱われる。
- パッケージを表すサブディレクトリがプロジェクトファイルを持つなら、そのパッケージの UUID に対するグラフエントリーはプロジェクトファイルの
[deps]
セクションにあるものだけ (このセクションが存在しないなら空) となる。
例として、次の構造と内容を持つパッケージディレクトリを考えます:
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(),
)
一般的な注意事項を示します:
- プロジェクトファイルを持たないパッケージは、トップレベルにある任意の依存パッケージに依存できます。パッケージディレクトリ環境に含まれる全てのパッケージはトップレベルにあるので、これは環境内の全てのパッケージをインポートできることを意味します。
- プロジェクトファイルを持つパッケージはプロジェクトファイルを持たないパッケージに依存できません。なぜなら、プロジェクトファイルを持つパッケージが依存できるのは
graph
に含まれるパッケージだけであり、プロジェクトファイルを持たないパッケージはgarph
に加われないからです。 - プロジェクトファイルを持ち明示的な UUID を持たないパッケージに依存できるのは、プロジェクトファイルを持たないパッケージだけです。なぜなら、こういったパッケージに割り当てられるダミーの UUID は厳密に内部でのみ利用されるからです。
今の例では、この注意事項は次のように適用されます:
Bobcat
はCobra
とDingo
をインポートでき、両方ともインポートしています。この二つのパッケージは両方ともuuid
エントリーを持ったプロジェクトファイルを持っており、Bobcat
のプロジェクトファイルの[deps]
セクションで依存先として宣言されています。Aardvark
はプロジェクトファイルを持たないので、Bobcat
はAardvark
に依存できません。Cobra
はDingo
をインポートでき、インポートしています。Dingo
はプロジェクトファイルと UUID と持ち、Cobra
のプロジェクトファイルの[deps]
セクションで依存先として宣言されています。Aardvark
とBobcat
は UUID を持たないので、Cobra
はこの二つのパッケージに依存できません。Dingo
のプロジェクトファイルは[deps]
セクションを持たないので、何もインポートできません。
パス写像
パッケージディレクトリ環境のパス写像は簡単です: 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
関数がキーの衝突で最後の引数を優先するためです。
この設計の特筆すべき特徴を示します:
- スタックの一番上にあるプライマリ環境はスタック環境に確実に埋め込まれます。プライマリ環境の完全な依存グラフはスタック環境全体の依存グラフにバージョンや依存パッケージを含めて無傷で存在することが保証されます。
- スタックの一番上にない (プライマリでない) 環境に含まれるパッケージは、最終的なスタック環境で互換性の無いバージョンが読み込まれる可能性があります。それぞれの環境内でパッケージが互換性を持っていたとしても、そのバージョンが読み込まれることは保証されません。互換性が失われるのは、スタックに含まれる自身より若い (上にある) 環境が自身の依存パッケージの異なるバージョンを使うことで、自身の依存パッケージ (のグラフとパスのいずれか、もしくは両方) が隠されたときです。
通常プライマリ環境はあなたが取り組むプロジェクトの環境であり、追加ツールはスタックでそれよりも下に位置するので、これは正しいトレードオフと言えます: ユーザーのプロジェクトと開発ツールのどちらかが壊れるなら、開発ツールを壊してユーザーのプロジェクトの動作を優先させるべきです。こういった非互換性が起きた場合には、開発ツールのバージョンをメインプロジェクトと互換なバージョンにアップグレードすることになるでしょう。
最後に
パッケージシステムにおける連邦制パッケージ管理と正確なソフトウェア再現性は困難であるもののそれだけの価値がある目標です。この二つの目標を達成するには多くの動的言語が持つものより複雑なパッケージ読み込みメカニズムが必要になりますが、達成できればスケーラビリティと再現性という静的言語に関連付けられることの多い特徴が手に入ります。典型的な Julia ユーザーは組み込みのパッケージマネージャを内部で起きていることを理解することなく使うことができるはずです。Pkg.add("X")
とすれば (Pkg.activate("Y")
で選択される) 適切なプロジェクトのマニフェストファイルにパッケージが追加され、それだけで import X
が X
を読み込むようになります。