コードの読み込み
このチャプタではパッケージの読み込みの技術的な詳細について説明します.パッケージをインストールするには,Juliaの組み込みパッケージマネージャであるPkg
を使って,パッケージをアクティブな環境に追加します.既にアクティブな環境にあるパッケージを使うには,Modulesdocumentationで説明されているように,import X
またはusing X
を書いてください.
Definitions
Juliaには2種類のコード読み込みのメカニズムがあります:
- コードインクルージョン:,例えば
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
という名前で利用できるようにします.同じX
パッケージが同じJuliaセッションで複数回インポートされた場合,読み込まれるのは最初のものだけで,その後のインポートではインポートモジュールは同じモジュールへの参照を取得します.ただし,import X
は異なるコンテキストでは異なるパッケージを読み込むことができることに注意してください:x
はメインプロジェクトではX
という名前のパッケージを参照しますが,依存関係ごとにX
という名前の異なるパッケージを参照する可能性があります.これについては後述します.
コードインクルージョンは,非常に簡単でシンプルです: 呼び出し元のコンテキストで与えられたソースファイルを評価します.パッケージの読み込みはコードインクルージョンの上に構築され,different purposeを果たします.このチャプタの残りの部分はパッケージの読み込みの動作と仕組みに焦点を当てていきます.
パッケージとは,他の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はフェデレートされたパッケージ管理をサポートしており,これは,複数の独立したパーティが パブリックおよびプライベートなパッケージとパッケージのレジストリの両方を管理し,プロジェクト は異なるレジストリからパブリックおよびプライベートパッケージを混在させながら依存できることを 意味します.様々なレジストリのパッケージは,共通のツールとワークフローのセットを使って インストールされ,管理されます.JuliaにビルトインなPkg
というパッケージマネージャを使用 すると,プロジェクトに依存関係のあるものをインストールし,管理することができます. プロジェクトファイル(プロジェクトが依存している他のプロジェクトを記述したもの)や マニフェストファイル(プロジェクトの完全な依存関係グラフの正確なバージョンをスナップショット したもの)の作成と操作を支援します.
フェデレーションの一つの結果として,パッケージの命名のための中央管理者は存在しません. 異なるエンティティが無関係なパッケージを参照するために同じ名前を使うことがあります. これらのエンティティは連携しておらず,お互いのことを知らない場合もあるので,この可能性は 不可避です.中央の命名権限がないため,単一のプロジェクトが同じ名前の異なるパッケージに 依存してしまう可能性があります.Juliaのパッケージ読み込みメカニズムでは,単一のプロジェクト の依存関係グラフ内であっても,パッケージ名がグローバルに一意である必要はありません. その代わりに,パッケージはuniversally unique identifiers (UUIDs)で識別され, このUUIDは各パッケージが生成された時に割り当てられます.通常,Pkg
が生成や追跡を担って くれるので,この128ビットの識別子を直接扱う必要はありません.しかし,これらのUUIDは X
はどのパッケージを参照しているか?という質問に対する確実な答えを提供します.
中央管理されていない名前の問題は抽象的なので,問題を理解するためには具体的なシナリオを 見ていくことが役立つかもしれません.今,App
というアプリケーションを開発しているとし, その際Pub
とPriv
という2つのパッケージを使っているとしましょう.Priv
はあなたが作った プライベートパッケージであり,Pub
はあなたが使用しているが管理はしていないパブリック パッケージです.あなたがPriv
を作成した時,Priv
という名前のパブリックパッケージは ありませんでした.しかしその後,Priv
という名前の無関係なパッケージが公開され,人気が 出てきました.実際,Pub
パッケージはそれを使い始めました.そのため,次にPub
を アップグレードして最新のバグフィックスや機能を手に入れようとすると,アップグレード以外に 何もしなくても,App
はPriv
という名前の異なる2つのパッケージに依存してしまうことに なります.App
はあなたのプライベートなPriv
パッケージに直接依存しており,Pub
を通して 新しいパブリックなPriv
パッケージに間接的に依存しています.これら2つのPriv
パッケージは 異なるものですが,App
が正しく動作し続けるためには双方が必要なので,import Priv
が, App
のコードの中にあるのか,Pub
のコードにあるのかによって,異なるPriv
パッケージを 参照しなければなりません.これを処理するために,Juliaのパッケージ読み込みメカニズムは 2つのPriv
パッケージをUUIDで区別し,そのコンテキスト(すなわちimport
を呼んだモジュール) に基づいて,正しい方を選択します.以下のセクションで説明するように,これは環境によって 決まります.
環境
環境とは,様々なコードコンテキストにおけるimport X
およびusing X
の意味と,これらの ステートメントによって読み込まれるファイルが何かを決定するものです.Juliaは2種類の環境を 理解しています:
- プロジェクト環境はプロジェクトファイルとオプションのマニフェストファイルを含むディレクトリで,明示的な環境を形成します.プロジェクトファイルはプロジェクトの直接の依存関係の名前と同一性を決定づけます.マニフェストファイルがあるのであれば,全ての直接および間接的な依存関係,各依存関係の正確なバージョン,正しいバージョンを探して読み込むための十分な情報を含む,完全な依存関係グラフを提供します.
- パッケージディレクトリはパッケージの集合のソースツリーをサブディレクトリとして含むディレクトリであり,暗黙の環境を形成します.
X
がパッケージディレクトリのサブディレクトリであり,X/src/X.jl
が存在する場合,パッケージX
はパッケージディレクトリ環境で利用可能であり,X/src/X.jl
はそれがロードされるソースファイルです.
これらを混ぜ合わせてスタック環境を作成することができます,すなわち,プロジェクト環境と パッケージディレクトリを順番に重ね合わせて一つの複合環境を作るということです.優先度と 可視性のルールを組み合わせて,どのパッケージが利用可能で,どこからロードされるのかを決定 します.例えばJuliaの読み込みパスはスタック環境を形成します.
これらの環境はそれぞれ異なる目的を持っています:
- プロジェクト環境は再現性を提供します.プロジェクト環境をバージョンコントロール(例えばgitリポジトリなど)でプロジェクトのの残りのソースコードと一緒にチェックすることで,プロジェクトとその依存関係の全てを正確に再現することができます.特にマニフェストファイルは,ソースツリーの暗号化ハッシュによって識別される全ての依存関係の正確なバージョンをキャプチャします.これにより,
Pkg
は正しいバージョンを取得し,全ての依存関係について記録された正確なコードを実行していることを確認することが可能になります. - パッケージディレクトリは,完全に注意深く追跡されたプロジェクト環境は必要ないような場合に便利です.パッケージディレクトリはパッケージのセットをどこかに置いておいて,そのパッケージ向けのプロジェクト環境を作らなくても直接使えるようにしたいときに便利です.
- スタック環境では,プライマリ環境にツールを追加することができます.開発ツールの環境をスタックの端にプッシュしてREPLやスクリプトから利用できるようにすることはできますが,パッケージの内部からは利用できません.
高いレベルでは,各環境は概念的に,roots, graph, pathsの3つのマップを定義しています.import X
の意味を解決するとき,rootsマップとgraphマップはX
の同一性を決定するために使用され,pathsマップはX
のソースコードを見つけるために使用されます.3つのマップの具体的な役割は以下の通りです:
roots:
name::Symbol
⟶uuid::UUID
環境のrootsマップは,その環境がメインプロジェクトで利用できるようにしているトップレベルの依存関係(すなわち,
Main
で読み込めるもの)の全てのUUIDにパッケージ名を割り当てます.Juliaのメインプロジェクト内でimport X
があると,JuliaはX
のIDをroot[:X]
として調べます.graph:
context::UUID
⟶name::Symbol
⟶uuid::UUID
環境のgraphは,各
context
UUIDに対して,名前からUUIDへのマップを割り当てる多階層マップで,rootsマップに似ていますが,context
に固有のものです.UUIDがcontext
であるパッケージのコードの中でimport X
を見ると,Juliaはgraph[context][:X]
としてX
の同一性を調べます.特に,これはimport X
がcontext
によって,異なるパッケージを参照できることを意味します.paths:
uuid::UUID
×name::Symbol
⟶path::String
pathsマップは各パッケージのUUID-名前のペアに,そのパッケージのエントリポイントソースファイルの場所を割り当てます.
import X
のX
がrootsまたはgraph(メインプロジェクトから読み込まれるか依存関係から読み込まれるかによって変化する)経由でUUIDに名前解決されたあと,Juliaは環境内のpaths[uuid,:X]
を検索することで,X
を取得するために読み込むファイルを決定します.このファイルをインクルードすると,X
という名前のモジュールが定義されるはずです.このパッケージが読み込まれると,同じuuid
に解決する後続のインポートは,すでにロードされているパッケージモジュールへの新しいバインディングを作成します.
環境の種類ごとに,以下のセクションで詳しく説明するように,これらの3つのマップは異なる定義をしています.
理解を容易にするためにこの章の例では,roots, graph, pathsの完全なデータ構造を示していますが,Juliaのパッケージ読み込みコードはこれらを明示的には作成しません.その代わりに,与えられたパッケージを読み込むのに必要な分だけ,各構造の計算だけを行っています.
プロジェクト環境
プロジェクト環境は,Project.toml
と呼ばれるプロジェクトファイルと,必要に応じてManifest.toml
と呼ばれるマニフェストファイルを含むディレクトリによって決定されます.これらのファイルは,JuliaProject.toml
やJuliaManifest.toml
と呼ばれることもあり,この場合はProject.toml
やManifest.toml
は無視されます.これにより,Project.toml
やManifest.toml
と呼ばれるファイルを重視する他のツールとの共存が可能になります.しかし純粋なJuliaプロジェクトでは,Project.toml
やManifest.toml
という名前が好まれます.
プロジェクト環境のrootsマップ,graphマップ,pathsマップは以下のように定義されています:
環境のrootsマップは,プロジェクトファイルの内容,特にトップレベルの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マップを意味しています:
roots = Dict(
:App => UUID("8f986787-14fe-4607-ba5d-fbff2944afa9"),
:Priv => UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"),
:Pub => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
)
このようなrootsマップが与えられると,App
のコードでは,import Priv
というステートメントがJuliaにroots[:Priv]
を検索させ,そのコンテキストで読み込まれるPriv
パッケージのUUIDであるba13f791-ae1d-465a-978b-69c3ad90f72b
が生成されます.このUUIDは,メインアプリケーションがimport Priv
を評価する際に,どのPriv
パッケージを読み込んで使用するかを識別します.
プロジェクト環境の依存関係graphは,マニフェストファイルがあれば,その内容によって決定されます.マニフェストファイルがない場合は,graphは空です.マニフェストファイルには,直接または間接的な依存関係のそれぞれについての節が含まれています.各依存関係について,ファイルにはパッケージのUUIDとソースツリーハッシュ,またはソースコードへの明示的なパスがリストされています.次の例で,例としてApp
のマニフェストファイルを見てみましょう:
[[Priv]] # the private one
deps = ["Pub", "Zebra"]
uuid = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
path = "deps/Priv"
[[Priv]] # the public one
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
プロジェクトの完全な依存関係グラフが記述されています:
- アプリケーションが使用する
Priv
という名前の,異なる2つのパッケージがあります.ルート依存関係にあるプライベートパッケージと,Pub
を通じて間接的に依存関係にあるパブリックパッケージを使用しています.これらは異なるUUIDによって区別されており,異なるdepsを持っています:- プライベートな
Priv
はPub
とZebra
パッケージに依存しています. - パブリックな
Priv
には依存関係はありません.
- プライベートな
- アプリケーションは
Pub
パッケージにも依存しており,Pub
パッケージはパブリックなPriv
と,プライベートなPriv
が依存しているのと同じZebra
パッケージに依存しています.
この依存関係graphを辞書で書くと,以下のようになります:
graph = Dict(
# Priv – the private one:
UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b") => Dict(
:Pub => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
:Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
),
# Priv – the public one:
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(),
)
この依存関係graph
が与えられ,Pub
パッケージ,すなわちUUIDとしてc07ecb7d-0dc9-4db7-8803-fadaaeaf08e1
を持つものの中で,import Priv
がある際には,Juliaは以下のようにして調べ,2d15fe94-a1f7-436c-a4d8-07a9a496e01c
を得ます:
graph[UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1")][:Priv]
これは,Pub
パッケージのコンテキストでは,アプリが直接依存しているプライベートなものではなく,import Priv
はパブリックなPriv
パッケージを参照していることを示しています.このようにして,Priv
という名前はそのパッケージの依存関係の一つではなく,メインプロジェクト内の異なるパッケージを参照することができ,パッケージエコシステム内で名前を重複させることを可能にします.
import Zebra
がメインのApp
コードベースで評価されるとどうなるのでしょうか?Zebra
はプロジェクトファイルには現れないため,マニフェストファイルではZebra
が記載されていたとしても,インポートは失敗します.さらに,パブリックなPriv
パッケージ(UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01c
)でimport Zebra
が行われた場合,そのPriv
パッケージはマニフェストファイルで宣言された依存関係を持たず,ゆえにどのパッケージを読み込むこともできないため,これもまた失敗します.Zebra
パッケージはマニフェストファイルで明示的に依存関係として記載されているパッケージ(Pub
パッケージとPriv
パッケージのうちの一つ)によってのみ読み込むことができます.
プロジェクト環境のpathsマップは,マニフェストファイルから抽出されます.X
という名前のパッケージuuid
のパスは,以下のルールに世って決定されます(順不同):
- ディレクトリ内のプロジェクトファイルが
uuid
と名前X
に一致する場合は,以下のいずれかのように動作します:
- トップレベルの
path
エントリを持っている場合,uuid
はそのパスにマップされ,プロジェクトファイルを含むディレクトリからの相対パスとして解釈されます. - そうでなければ,
uuid
はプロジェクトファイルを含むディレクトリから相対的なパスとして,src/X.jl
にマップされます.
- 上記のような場合ではなく,プロジェクトファイルに対応するマニフェストファイルがあり,マニフェストに
uuid
にマッチする節が含まれている場合には,以下のようになります:
path
エントリがある場合には,そのパスを使用します(マニフェストファイルを含むディレクトリからの相対パス).git-tree-sha1
エントリがある場合は,uuid
とgit-tree-sha1
の決定論的ハッシュ関数(slug
と呼びます)を計算し,JuliaのDEPOT_PATH
グローバル配列の各ディレクトリの中からpackages/X/$slug
という名前のディレクトリを探します.存在する中で最初に見つかったディレクトリを使用します.
これらいずれかが成功した場合,ソースコードのエントリポイントへのパスは,その結果か,その結果からの相対パスにsrc/X.jl
を加えたものになります.それ以外の場合はuuid
のパスマッピングはありません.X
を読み込む際,ソースコードのパスが見つからない場合には,検索に失敗し,適切なパッケージのバージョンをインストールするか,その他の修正をするように促されることがあります(例: X
を依存関係として宣言する).
上の例のマニフェストファイルでは,最初のPriv
パッケージ(UUID ba13f791-ae1d-465a-978b-69c3ad90f72b
)のパスを見つけるために,Juliaはマニフェストファイルでその節を探し,path
エントリがあることを確認し,App
プロジェクトディレクトリからの相対パスでdeps/Priv
を見て(例えば,App
が/home/me/projects/App
にあるとすれば,/home/me/projects/App/deps/Priv
を見て),それが存在することを確認し,そこからPriv
を読み込みます.
一方で,Juliaが もう一つの Priv
パッケージ(UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01c
)を読み込むと,マニフェストの中でその節を見つけ,path
エントリはないが,git-tree-sha1
エントリはあることを確認します.そしてこのUUID/SHA-1ペアのslug
を計算し,これがHDkrT
になります(この計算の正確な詳細は重要ではありませんが,一貫性があり決定論的なものです).これは,このPriv
パッケージへのパスがパッケージデポの一つにあるpackages/Priv/HDkrT/src/Priv.jl
になることを意味します.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
ファイルから,Priv
パッケージを読み込むために,これらのうちの最初のものを使用します.
ここでは,例のApp
プロジェクト環境のために,ローカルファイルシステムを検索した後,依存関係グラフのために上で与えられたマニフェストで提供される可能性のあるパスマップを表現しています:
paths = Dict(
# Priv – the private one:
(UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"), :Priv) =>
# relative entry-point inside `App` repo:
"/home/me/projects/App/deps/Priv/src/Priv.jl",
# Priv – the public one:
(UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"), :Priv) =>
# package installed in the system depot:
"/usr/local/julia/packages/Priv/HDkr/src/Priv.jl",
# Pub:
(UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"), :Pub) =>
# package installed in the user depot:
"/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",
)
この例のマップには,3種類のパッケージの場所が含まれています(1番目と3番目はデフォルトの読み込みパスの一部です):
- プライベートな
Priv
パッケージはApp
レポジトリの中にある"vendored"です. - 公開の
Priv
とZebra
パッケージはシステムデポにあり,システム管理者によってインストールされ管理されているパッケージが置かれています.これらはシステム上の全てのユーザが利用できます. Pub
パッケージはユーザデポにあり,ユーザによってインストールされたパッケージが置かれています.これらは,それらをインストールしたユーザのみが使用できます.
パッケージディレクトリ
パッケージディレクトリは,名前の衝突を扱う機能を持たずに,よりシンプルな環境を提供します.パッケージディレクトリでは,トップレベルのパッケージのセットは,パッケージのように「見える」サブディレクトリのセットになります.パッケージディレクトリに以下の「エントリポイント」ファイルのいずれかが含まれていれば,パッケージX
はパッケージディレクトリ内に存在します:
X.jl
X/src/X.jl
X.jl/src/X.jl
パッケージディレクトリ内のパッケージがインポートできる依存関係は,パッケージにプロジェクトファイルが含まれているかどうかに依存します:
- プロジェクトファイルがある場合,プロジェクトファイルの
[deps]
セクションで指定されているパッケージのみをインポートできます. - プロジェクトファイルがない場合は,トップレベルのパッケージ,つまり
Main
やREPLで読み込むことができるパッケージをインポートします.
rootsマップは,存在する全てのパッケージのリストを生成するためにパッケージディレクトリの内容を調べることで決定されます.さらに,以下のように各エントリにUUIDが割り当てられます: フォルダX
の中にあるパッケージの場合
X/Project.toml
が存在し,uuid
エントリがあれば,uuid
がその値になります.X/Project.toml
は存在しているが,トップレベルのUUIDエントリがない場合,uuid
はX/Project.toml
へのカノニカル(リアル)パスをハッシュして生成された,ダミーのUUIDとなります.- それ以外の場合(
Project.toml
が存在しない場合)は,uuid
は全てゼロのnil UUIDとなります.
プロジェクトディレクトリの依存関係graphは各パッケージのサブディレクトリにあるプロジェクトファイルの存在と内容によって決定されます.ルールは以下の通りです:
- パッケージのサブディレクトリにプロジェクトファイルがない場合,そのパッケージはグラフから省略され,そのコードのインポート文はメインプロジェクトや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:
# no imports
ここで,対応する根の構造を辞書で表すと,以下のようになります:
roots = Dict(
:Aardvark => UUID("00000000-0000-0000-0000-000000000000"), # no project file, nil UUID
:Bobcat => UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), # dummy UUID based on path
:Cobra => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), # UUID from project file
:Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), # UUID from project file
)
ここで対応するグラフ構造を辞書で表すと,以下のようになります:
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
でしかパッケージを読み込むことができず,プロジェクトファイルを持たないパッケージはグラフには表示されないからです. - プロジェクトファイルを持っていても明示的なUUIDを持たないパッケージは,プロジェクトファイルを持たないパッケージにしか依存できません.
私たちの例のなかで,これらのルールの具体的な例を観察してみましょう:
Aardvark
はBobcat
,Cobra
,Dingo
のいずれでもインポートできます.またAardvark
はBobcat
とCobra
をインポートできます.Bobcat
はCobra
とDingo
のいずれもインポートすることができます.これらはともにUUIDを持つプロジェクトファイルを持ち,Bobcat
の[deps]
セクションで依存関係として宣言されています.Bobcat
はAardvark
には依存できません,これはAardvark
にはプロジェクトファイルがないためです.Cobra
はプロジェクトファイルとUUIDを持つDingo
をインポートすることができ,Cobra
の[deps]
の中で依存関係として宣言されています.Cobra
はAardvark
にもBobcat
にも依存できません.それはこれらのいずれも実際のUUIDを持っていないからです.Dingo
はプロジェクトファイルに[deps]
セクションがないので,何もインポートできません.
パッケージディレクトリのpathsマップは簡単で,サブディレクトリ名とそれに対応するエントリポイントのパスをマッピングしています.言い換えれば,例の中のプロジェクトディレクトリへのパスが/home/me/animals
であれば,paths
マップはこの辞書で表されます:
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
マップエントリは常にこの形式を持っています.
環境スタック
3番目の最後の環境は,いくつかの環境を重ね合わせて他の環境を結合し,それぞれの環境のパッケージを一つの複合環境で利用可能にしたものです.これらの複合環境は環境スタックと呼ばれています.JuliaのLOAD_PATH
グローバルは,環境スタック,つまりJuliaプロセスが動作する環境を定義します.もし,Juliaプロセスが,あるプロジェクトやパッケージディレクトリにあるパッケージだけにアクセスできるようにしたい場合は,LOAD_PATH
の唯一のエントリにしてください.しかし,作業中のプロジェクトに依存していなくても,標準ライブラリ,プロファイラ,デバッガ,パーソナルユーティリティなど,お気に入りのツールにアクセスできるようにしておくと,非常に便利な場合があります.これらのツールを含む環境を読み込みパスに追加することで,プロジェクトに追加しなくても,トップレベルのコードですぐにアクセスできるようになります.
環境スタックのコンポーネントの,roots,graph,pathsのデータ構造を結合する仕組みは単純です.辞書としてマージされ,キーが衝突した場合には,後のエントリよりも前のエントリが優先されます.言い換えると,stack = [env₁ , env₂ , …]
とすると,次のようになります.
roots = reduce(merge, reverse([roots₁, roots₂, …]))
graph = reduce(merge, reverse([graph₁, graph₂, …]))
paths = reduce(merge, reverse([paths₁, paths₂, …]))
添え字付きのrootsᵢ
,graphᵢ
,pathsᵢ
は,stack
に含まれる添え字付きの環境envᵢ
に対応しています.引数辞書のキー間で衝突があった場合,merge
は最初の引数ではなく最後の引数を優先するため,reverse
が存在します.この設計には注目すべき特徴がいくつかあります.
- プライマリ環境(スタックの最初の環境)は,スタック環境に忠実に埋め込まれています.スタック内の最初の環境の完全な依存関係グラフは,全ての依存関係の同じバージョンを含めて,スタック環境にそのまま含まれていることが保証されています.
- プライマリ環境以外のパッケージは,たとえ自分の環境が完全に互換性があったとしても,互換性のないバージョンの依存関係を使ってしまう可能性があります.これは,その依存関係の一つが,スタック内の以前の環境のバージョンによって陰になっている場合に起こります(グラフやパス,あるいはそれらの両方によって).
プライマリ環境は通常,作業中のプロジェクトの環境であるため,スタックの後の環境には追加のツールが含まれてしまいますが,これは正しいトレードオフです.すなわち,開発ツールを壊してでもプロジェクトを継続した方が良いということです.このような非互換性が発生した場合,通常は開発ツールをメインプロジェクトと互換性のあるバージョンにアップグレードしたいと思うでしょう.
Conclusion
フェデレートされたパッケージ管理と正確なソフトウェアの再現性は,パッケージシステムにおける難しくも価値のある目標です.これらの目標を組み合わせることで,ほとんどの動的言語が持つよりも複雑なパッケージ読み込み機構を実現するだけでなく,静的言語により一般的に関連付けられるスケーラビリティや再現性も得ることもできます.一般的にJuliaのユーザは,これらの相互作用を正確に理解していなくても,ビルトインのパッケージマネージャを使ってプロジェクトを管理することができます.Pkg.add("X")
を呼び出すとPkg.activate("Y")
で選択された適切なプロジェクトとマニフェストファイルに追加されるため,将来import X
を呼ぶときには追加で何も考えずともX
を読み込むことができます.