RubyのNamespaces入門
この記事はXavier Noria氏が書いた「Namespaces 101」の翻訳記事です。
Noria氏本人から許可を得て、翻訳し、公開しています。
はじめに
Rubyは(現時点では)デフォルトで無効な実験的機能として namespaces を最近マージしました。
これは@matz自身によって進められた非自明な開発で、Rubyコミッターになったばかり(🎉)の@tagomorisによって主に実装されています。
この機能は長い間議論されてきました。最初のチケットは2年前に作られ(#19744)、ちょうど先週に改訂されたチケットが作られました(#21311)。
すでにいくつかのドキュメントが存在し、チケット上では膨大な量の議論がなされています。詳細について、提案や性能上の懸念点そして意義など、それらを追いかけていくのは少し大変です。
この記事では、namespacesについてダイジェストでまとめています。
何が起こるのかがわかるように、それらのメンタルモデルをお見せできればと思います。
メインアイデア
以下のコードを見てみましょう:
require 'nokogiri'
module M
end
class String
def blank? = ...
end
ns = Namespace.new
ns.require('foo')
はい、namespacesにあるメインアイデアは、foo.rb内のコードが親のコードから隔離されて実行されるということです。あたかも
- Nokogiriがまだロードされていないように
- 定数
Mがまだ定義されていないように -
Stringにモンキーパッチが当てられてないかのようにすることです。
それゆえ -
foo.rbは、衝突することなく、異なるバージョンであっても、Nokogiriをロードすることができ -
foo.rbは、衝突することなく、それ自身のトップレベルのMを定義でき -
foo.rbは、衝突することなく、それ自身のコアエクステンションを定義できます。
それは全て同じプロセスで実行しますが、namespaceの内側と外側から参照できるものはある程度守られていて、それが重要なポイントです。
透過性
foo.rbの作成者は、それを現在のgemのように普通に実行されるように書いています。
この機能により、foo.rbの利用者は普通にロード、またはnamespaceを通してロードすることができます。呼び出し側次第です。foo.rb内のコードは、どちらのシナリオにおいてもそのまま正しく動作すべきです。
個人的にいくつかの相違(#21316, #21318, #21320)を見ましたが、これはすべて準備的なものですので修正可能であると見ています。
グローバル変数はnamespace単位
foo.rbはあたかもNokogiriがまだロードされていないかのようにロードされる、と言いました。
いいですね、特にfoo.rbもまたNokogiriを必要とするのであればロードすることができます:
# foo.rb
require 'nokogiri' # => true
しかし、Kernel#requireの冪等といえるでしょうか?
さて、この新しいパラダイムではグローバル変数はnamespace単位になります。なので、$LOAD_FEATURES(Kernel#requireが冪等性の根拠とするrequireされたファイルの配列)はメインファイル(訳注:foo.rbをnamespace.requireしているファイル)が実行されたことによる変更を持ちません。
つまり、Nokogiriはメモリ上に_2度_ロードされることになります。なので、その能力を得られるとと同時にすべてのもの(コストがかかるもの)と同様にコストもあります。
組み込み定数、クラスとモジュール
前章で、foo.rbがロードされたときにMは存在しないというのを見ました。しかしながら、StringやObjectのような他のトップレベルの定数は存在します。それはどのようにして成り立っているのでしょうか?
Namespacesは_組み込み_定数とユーザ定義定数との明らかな境界を設けています。Hashは組み込み定数ですが、Mは違います。組み込み定数は維持されます。
これはメインアイデアと一致しています:プログラムを起動したときに、それら組み込み定数は存在していますよね?つまり、Namespaceの下にも同様のものがあるということです。
しかし、Stringに当てられたモンキーパッチもnamespaceには影響しません。これはどのようにして可能なのでしょうか?
概念的には、インタプリタが起動したときにこれらの定数が保存するオブジェクトは、その機能が元の形式を保持する、いわばそれらが変更される機会を得る前に、_組み込み_クラスとモジュールとしみなされます。
端的に言うと、foo.rbから参照できる対応する組み込み定数は、それらの元の状態を持つ。
(技術的には、参照は同じ、オブジェクトIDは同じだが、組み込みオブジェクトの状態はnamespace単位である。)
ユーザ定義定数
main.rbのMがfoo.rbから参照できないのは、Objectがfoo.rbの実行コンテキスト(namespace ns)の元の状態にリセットされるからで、つまりその状態はMを持たない(トップレベル定数はObject内に保存されることを思い出してください)。
なので、もしfoo.rbが新しいモジュールを定義したら
# foo.rb
module M
end
foo.rbに関しては、Object内に新しい定数が作られ、新しいモジュールをその定数は持ち、main.rb内のモジュールオブジェクトとは関係なく、そこに同じ名前の定数に保存される。
namespace間の通信
ここで面白いことは、main.rbがnamespace内のものを見たりやりとりしたりできること:
# foo.rb
module Foo
def self.m = 1
end
# main.rb
ns = Namespace.new
ns.require('foo')
ns::Foo.m # => 1
え?namespaceをまたぐコードがあるんですか?namespaceのコンテキストにFoo.mを定義しましたが、それは外側で実行される?ここでそのゲームのルールは何?分離はどうなった?
これについての良いメンタルモデルはリモートプロシージャコール(RPC)だと信じています。
Foo.mの呼び出しはmain.rbで起きていますが、実行されるコード(1)はnamespace内でロードされました。そしてそのコンテキストで実行されます。
例として、より興味深いシナリオを想像してみましょう:
# foo.rb
M = 1
module Foo
def self.m = M
end
# main.rb
ns = Namespace.new
ns.require('foo')
M = 2
ns::Foo.m # => 1
問題は、Foo.mメソッドが定数参照を持っているということです。main namespaceからns::Foo.mとして呼び出したとき、そのMはどのように解決されるのでしょうか?mainのコンテキストまたはnsのコンテキスト?
PRCを考えよう。
main namespace_から_Foo.mを呼び出している一方、そのメソッドの本体はfoo.rbにあり、nsのもとでロードされた。それゆえ、mainの中のMはそこに存在せず、そのメソッドのために存在するMはnsのMだけである。
その逆も同じで、mainからnamespaceのメソッドを呼び出して参照を渡すことができる。もしそれらが組み込みでなかったら、通常のメソッドに反応し、もしそれらのメソッドがnamespaceから呼び出されれば、RPCスタイルでmainコンテキストで実行される。
これは、技術的にはあるバージョンのgemからきたオブジェクトをそのgemの異なるバージョンを使用するnamespaceへ渡すことができるという、何人かの人が指摘していた潜在的な罠の可能性がある。
複数のnamespace
namespaceは好きなようにspawnすることができる。namespace内から新たにnamespaceをspawnすることもできる。
Namespaceは階層構造を形成せず、概念的にはフラットである。これはすべてのnamespaceが同じクリーンな状態から始まり、まったく何も引き継がないためである。
ユースケース
ユースケースは、コードを隔離してコントロールされた状態で動かすという考えがベースになる。例えば、このテストスイートではグローバルな状態を汚したくない、とか、このコードとそのコードをあるgemの異なるバージョンで動かす必要がある、といった感じ。
しかし、この機能が手に届くところにあれば、新しい応用が必ず生まれるという予感があります。おそらく、現在はまだ予見できないような、より創造的なものさえも生まれるでしょう。
試してみよう!
namespaceを試すためには、Rubyをダウンロードし、コンパイルする必要があります。
そのあと、RUBY_NAMESPACE=1をあなたの環境に設定します。
終わりに
namespacesにはここで触れた以上の内容もありますが、この記事がその概要を理解する助けになれば幸いです。
Discussion