コードの読み込み

Note

このチャプタではパッケージの読み込みの技術的な詳細について説明します.パッケージをインストールするには,Juliaの組み込みパッケージマネージャであるPkgを使って,パッケージをアクティブな環境に追加します.既にアクティブな環境にあるパッケージを使うには,Modulesdocumentationで説明されているように,import Xまたはusing Xを書いてください.

Definitions

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

  1. コードインクルージョン:,例えばinclude("source.jl").インクルードによって,単一のプログラムを複数のソースファイルに分割することが可能になります.include("source.jl")は,include呼び出しが発生したモジュールのグローバルスコープで,ファイルsource.jlの内容を評価します.include("source.jl")が複数回呼び出された場合,source.jlは複数回評価されます.インクルードされたパスであるsource.jlは,include呼び出しが発生したファイルからの相対パスとして解釈されます.これにより,ソースファイルのサブツリーを簡単に再配置することができます.REPLでは,インクルードされたパスは,現在の作業ディレクトリpwd()を基準に相対的に解釈されます.
  2. パッケージ読み込み:例えば,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というアプリケーションを開発しているとし, その際PubPrivという2つのパッケージを使っているとしましょう.Privはあなたが作った プライベートパッケージであり,Pubはあなたが使用しているが管理はしていないパブリック パッケージです.あなたがPrivを作成した時,Privという名前のパブリックパッケージは ありませんでした.しかしその後,Privという名前の無関係なパッケージが公開され,人気が 出てきました.実際,Pubパッケージはそれを使い始めました.そのため,次にPubを アップグレードして最新のバグフィックスや機能を手に入れようとすると,アップグレード以外に 何もしなくても,AppPrivという名前の異なる2つのパッケージに依存してしまうことに なります.AppはあなたのプライベートなPrivパッケージに直接依存しており,Pubを通して 新しいパブリックなPrivパッケージに間接的に依存しています.これら2つのPrivパッケージは 異なるものですが,Appが正しく動作し続けるためには双方が必要なので,import Privが, Appのコードの中にあるのか,Pubのコードにあるのかによって,異なるPrivパッケージを 参照しなければなりません.これを処理するために,Juliaのパッケージ読み込みメカニズムは 2つのPrivパッケージをUUIDで区別し,そのコンテキスト(すなわちimportを呼んだモジュール) に基づいて,正しい方を選択します.以下のセクションで説明するように,これは環境によって 決まります.

環境

環境とは,様々なコードコンテキストにおけるimport Xおよびusing Xの意味と,これらの ステートメントによって読み込まれるファイルが何かを決定するものです.Juliaは2種類の環境を 理解しています:

  1. プロジェクト環境はプロジェクトファイルとオプションのマニフェストファイルを含むディレクトリで,明示的な環境を形成します.プロジェクトファイルはプロジェクトの直接の依存関係の名前と同一性を決定づけます.マニフェストファイルがあるのであれば,全ての直接および間接的な依存関係,各依存関係の正確なバージョン,正しいバージョンを探して読み込むための十分な情報を含む,完全な依存関係グラフを提供します.
  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::Symboluuid::UUID

    環境のrootsマップは,その環境がメインプロジェクトで利用できるようにしているトップレベルの依存関係(すなわち,Mainで読み込めるもの)の全てのUUIDにパッケージ名を割り当てます.Juliaのメインプロジェクト内でimport Xがあると,JuliaはXのIDをroot[:X]として調べます.

  • graph: context::UUIDname::Symboluuid::UUID

    環境のgraphは,各context UUIDに対して,名前からUUIDへのマップを割り当てる多階層マップで,rootsマップに似ていますが,contextに固有のものです.UUIDがcontextであるパッケージのコードの中でimport Xを見ると,Juliaはgraph[context][:X]としてXの同一性を調べます.特に,これはimport Xcontextによって,異なるパッケージを参照できることを意味します.

  • paths: uuid::UUID × name::Symbolpath::String

    pathsマップは各パッケージのUUID-名前のペアに,そのパッケージのエントリポイントソースファイルの場所を割り当てます.import XXがrootsまたはgraph(メインプロジェクトから読み込まれるか依存関係から読み込まれるかによって変化する)経由でUUIDに名前解決されたあと,Juliaは環境内のpaths[uuid,:X]を検索することで,Xを取得するために読み込むファイルを決定します.このファイルをインクルードすると,Xという名前のモジュールが定義されるはずです.このパッケージが読み込まれると,同じuuidに解決する後続のインポートは,すでにロードされているパッケージモジュールへの新しいバインディングを作成します.

環境の種類ごとに,以下のセクションで詳しく説明するように,これらの3つのマップは異なる定義をしています.

Note

理解を容易にするためにこの章の例では,roots, graph, pathsの完全なデータ構造を示していますが,Juliaのパッケージ読み込みコードはこれらを明示的には作成しません.その代わりに,与えられたパッケージを読み込むのに必要な分だけ,各構造の計算だけを行っています.

プロジェクト環境

プロジェクト環境は,Project.tomlと呼ばれるプロジェクトファイルと,必要に応じてManifest.tomlと呼ばれるマニフェストファイルを含むディレクトリによって決定されます.これらのファイルは,JuliaProject.tomlJuliaManifest.tomlと呼ばれることもあり,この場合はProject.tomlManifest.tomlは無視されます.これにより,Project.tomlManifest.tomlと呼ばれるファイルを重視する他のツールとの共存が可能になります.しかし純粋なJuliaプロジェクトでは,Project.tomlManifest.tomlという名前が好まれます.

プロジェクト環境のrootsマップ,graphマップ,pathsマップは以下のように定義されています:

環境のrootsマップは,プロジェクトファイルの内容,特にトップレベルのnameuuidエントリ,[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を持っています:
    • プライベートなPrivPubZebraパッケージに依存しています.
    • パブリックな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のパスは,以下のルールに世って決定されます(順不同):

  1. ディレクトリ内のプロジェクトファイルがuuidと名前Xに一致する場合は,以下のいずれかのように動作します:
  • トップレベルのpathエントリを持っている場合,uuidはそのパスにマップされ,プロジェクトファイルを含むディレクトリからの相対パスとして解釈されます.
  • そうでなければ,uuidはプロジェクトファイルを含むディレクトリから相対的なパスとして,src/X.jlにマップされます.
  1. 上記のような場合ではなく,プロジェクトファイルに対応するマニフェストファイルがあり,マニフェストにuuidにマッチする節が含まれている場合には,以下のようになります:
  • pathエントリがある場合には,そのパスを使用します(マニフェストファイルを含むディレクトリからの相対パス).
  • git-tree-sha1エントリがある場合は,uuidgit-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は以下のパスが存在するかどうかを調べます:

  1. /home/me/.julia/packages/Priv/HDkrT
  2. /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番目はデフォルトの読み込みパスの一部です):

  1. プライベートなPrivパッケージはAppレポジトリの中にある"vendored"です.
  2. 公開のPrivZebraパッケージはシステムデポにあり,システム管理者によってインストールされ管理されているパッケージが置かれています.これらはシステム上の全てのユーザが利用できます.
  3. Pubパッケージはユーザデポにあり,ユーザによってインストールされたパッケージが置かれています.これらは,それらをインストールしたユーザのみが使用できます.

パッケージディレクトリ

パッケージディレクトリは,名前の衝突を扱う機能を持たずに,よりシンプルな環境を提供します.パッケージディレクトリでは,トップレベルのパッケージのセットは,パッケージのように「見える」サブディレクトリのセットになります.パッケージディレクトリに以下の「エントリポイント」ファイルのいずれかが含まれていれば,パッケージXはパッケージディレクトリ内に存在します:

  • X.jl
  • X/src/X.jl
  • X.jl/src/X.jl

パッケージディレクトリ内のパッケージがインポートできる依存関係は,パッケージにプロジェクトファイルが含まれているかどうかに依存します:

  • プロジェクトファイルがある場合,プロジェクトファイルの[deps]セクションで指定されているパッケージのみをインポートできます.
  • プロジェクトファイルがない場合は,トップレベルのパッケージ,つまりMainやREPLで読み込むことができるパッケージをインポートします.

rootsマップは,存在する全てのパッケージのリストを生成するためにパッケージディレクトリの内容を調べることで決定されます.さらに,以下のように各エントリにUUIDが割り当てられます: フォルダXの中にあるパッケージの場合

  1. X/Project.tomlが存在し,uuidエントリがあれば,uuidがその値になります.
  2. X/Project.tomlは存在しているが,トップレベルのUUIDエントリがない場合,uuidX/Project.tomlへのカノニカル(リアル)パスをハッシュして生成された,ダミーのUUIDとなります.
  3. それ以外の場合(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(),
)

注意すべき一般的なルールをいくつか紹介します:

  1. プロジェクトファイルを持たないパッケージは,トップレベルの依存関係に依存することができ,パッケージディレクトリ内の全てのパッケージがトップレベルで利用可能なので,環境内の全てのパッケージをインポートすることができます.
  2. プロジェクトファイルを持つパッケージは,プロジェクトファイルを持たないパッケージに依存することはできません.これは,プロジェクトファイルを持つパッケージはgraphでしかパッケージを読み込むことができず,プロジェクトファイルを持たないパッケージはグラフには表示されないからです.
  3. プロジェクトファイルを持っていても明示的なUUIDを持たないパッケージは,プロジェクトファイルを持たないパッケージにしか依存できません.

私たちの例のなかで,これらのルールの具体的な例を観察してみましょう:

  • AardvarkBobcatCobraDingoのいずれでもインポートできます.またAardvarkBobcatCobraをインポートできます.
  • BobcatCobraDingoのいずれもインポートすることができます.これらはともにUUIDを持つプロジェクトファイルを持ち,Bobcat[deps]セクションで依存関係として宣言されています.
  • BobcatAardvarkには依存できません,これはAardvarkにはプロジェクトファイルがないためです.
  • CobraはプロジェクトファイルとUUIDを持つDingoをインポートすることができ,Cobra[deps]の中で依存関係として宣言されています.
  • CobraAardvarkにも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が存在します.この設計には注目すべき特徴がいくつかあります.

  1. プライマリ環境(スタックの最初の環境)は,スタック環境に忠実に埋め込まれています.スタック内の最初の環境の完全な依存関係グラフは,全ての依存関係の同じバージョンを含めて,スタック環境にそのまま含まれていることが保証されています.
  2. プライマリ環境以外のパッケージは,たとえ自分の環境が完全に互換性があったとしても,互換性のないバージョンの依存関係を使ってしまう可能性があります.これは,その依存関係の一つが,スタック内の以前の環境のバージョンによって陰になっている場合に起こります(グラフやパス,あるいはそれらの両方によって).

プライマリ環境は通常,作業中のプロジェクトの環境であるため,スタックの後の環境には追加のツールが含まれてしまいますが,これは正しいトレードオフです.すなわち,開発ツールを壊してでもプロジェクトを継続した方が良いということです.このような非互換性が発生した場合,通常は開発ツールをメインプロジェクトと互換性のあるバージョンにアップグレードしたいと思うでしょう.

Conclusion

フェデレートされたパッケージ管理と正確なソフトウェアの再現性は,パッケージシステムにおける難しくも価値のある目標です.これらの目標を組み合わせることで,ほとんどの動的言語が持つよりも複雑なパッケージ読み込み機構を実現するだけでなく,静的言語により一般的に関連付けられるスケーラビリティや再現性も得ることもできます.一般的にJuliaのユーザは,これらの相互作用を正確に理解していなくても,ビルトインのパッケージマネージャを使ってプロジェクトを管理することができます.Pkg.add("X")を呼び出すとPkg.activate("Y")で選択された適切なプロジェクトとマニフェストファイルに追加されるため,将来import Xを呼ぶときには追加で何も考えずともXを読み込むことができます.