Ruby: グローバルメソッドというものは(Rubyには)ない(翻訳)
Rubyにおけるトップレベルメソッドは、実際には何であるか、どこに属しているか、どのように名前空間化されているか。
数日前、Redditの/r/rubyで興味深い質問を見かけました。手短に言うと、「Kernelモジュールのメソッドは、どのようにしてトップレベルのスコープで利用可能になるのか?」というものです。
この質問はrandメソッドのみを対象としていましたが、(著者も適切に指摘しているように)Kernelモジュールに属しているとドキュメントに記載しているその他多くの「トップレベル」メソッド(文字列を出力するputsや、別のファイルからコードを読み込むrequire、例外を発するraiseなども含む)にも当てはまりそうです。
ご存知の通り、Rubyのあらゆるメソッドは何らかのオブジェクトに属しており、そのオブジェクトのクラスやモジュールで定義されます。ドキュメントには、「グローバル」メソッドはKernelモジュールが由来であると書かれており、他のモジュールやオブジェクトや何らかのおまじない(名前空間の読み込みや、読み込んだ名前空間の現在のスコープへの追加など)をまったく参照せずに呼び出せるのが普通です。では、以下のコードが動く仕組みをどう理解すればよいでしょうか?
puts "Hello World"
🔗 実は常にselfのメソッド
Rubyは(他のナントカ言語と異なり)、fooのような小文字のみの識別子は常に「ローカル変数」か「現在のオブジェクトにあるメソッド(現在のスコープにその名前の変数が見つからない場合)」のどちらかを参照します。そして後者は、現在のスコープ内でselfが指すものが該当します。
すなわち、以下は
puts "Hello world"
常に1以下と同じなのです。
self.puts "Hello world"
さて、トップレベルのスコープでselfを書いたとき、このselfは一体何なのでしょうか?
このselfは、mainという特殊なオプジェクトです(ただしmainという識別子ではアクセスできないので、この"main"は単なる表示上の名前です)。
self #=> main
self.class #=> Object
self.class.ancestors #=> [Object, Kernel, BasicObject]
つまり、selfは、トップレベルのスコープでアクセス可能な汎用のObjectのインスタンスにすぎないということがわかります。そしてKernelモジュールに含まれるすべてのメソッドがそこにincludeされ、こうしてputsメソッドが呼び出し可能になるという流れです。
m = method(:puts) #=> #<Method: Object(Kernel)#puts(*)>
m.owner #=> Kernel -- メソッドが定義されている場所を返す
m.receiver #=> main -- その呼び出しを受信するオブジェクトを返す
しかし、そもそもKernelモジュールとは何なのでしょうか?
🔗 KernelとObjectの昔から紛らわしい点
Kernelモジュールは、Rubyの理念上は、あらゆる場所で利用できる「グローバル」メソッドの置き場所とすることが意図されていました。これらはすべてprivateメソッド2なのですが、現在のオブジェクトに対してそのオブジェクトの「内部で」呼び出される場合にのみ呼び出し可能であり、それによって(privateであるにもかかわらず)グローバルであるかのように見えます。
self.private_methods.include?(:puts) #=> true
ref = self
ref.puts "Something" # refオブジェクトの「外からは」putsを呼び出せない
# NoMethodError: private method `puts' called for main:Object
それと同時に、ベースクラス(他のすべてのクラスの共通祖先となるクラス3)であるObjectクラスに定義されているメソッドは、publicメソッドであり、どのオブジェクトの中でも、他のオブジェクトの外部から呼び出せます( #inspect、#to_s、#respond_to?(メソッド)、#is_a?(クラス)など多数)。
しかし、この振る舞いは、かつては「意図した通り」だったのです。実際、上述のpublicメソッドのほとんどは、Kernelモジュールにも同じものが定義されていて、そのことは以下のようにすぐ確認できます。
Object.instance_method(:is_a?) #=> #<UnboundMethod: Kernel#is_a?(_)>
Object.instance_method(:is_a?).owner #=> Kernel
# ObjectにあってKernelにないメソッドを表示する
Object.instance_methods - Kernel.instance_methods
#=> [:!, :equal?, :__id__, :__send__, :==, :!=, :instance_eval, :instance_exec]
見ての通り、Rubyの「理念上は」あらゆるオブジェクトのpublicメソッドであるはずのもののうち、ごく一握りのメソッドは、実はObjectクラスの方に定義されています。
しかし、Objectクラスのドキュメントを読んでみると、さらに多くのことがわかります。このドキュメントでは、RDoc(Rubyのドキュメント生成システム)を文字通りハックする形で、Objectクラスが本来あるべき姿であるかのように見せかけています。
しかし、このドキュメントハックはもはや現状に適していません。(Cではなく)Rubyで定義されたコアメソッドを認識しないうえに、(Rubyの理念上は本来Objectに置かれるべき)publicメソッドの一部が、Kernelモジュールの方で表示されています(例: #thenや#class:(obj.class)など)。これをもっと健全な方法で処理することについて(実際に何が起きているのかをある程度説明することも含めて)#19304で長年議論されていますが、進捗ははかばかしくありません。
🔗 putsをオブジェクトの内部に書くとどうなるか
となると、putsが実は「グローバル」メソッドではなく、あらゆるオブジェクト内でKernelモジュールからincludeされるprivateメソッドであるならば、「他のクラスのメソッド内でputsを呼んだとき、そのputsは一体誰のものなのか?」という疑問が生じます。
その答えは、「内部でputsを呼び出しているオブジェクトのもの」であり、つまり「putsの呼び出し元オブジェクトがオーナー」なのです!
class A
def test
p method(:puts) #=> #<Method: A(Kernel)#puts(*)>
# ↑メソッドの本当のオーナーはこのAというクラス
p method(:puts).receiver #=> #<A:0x00...>
end
end
A.new.test
このようなプログラミング言語はほとんどありません(おそらく、多くの開発者がデフォルトで感じる直感とも異なります)。ほとんどの言語では、「グローバル」メソッドは何らかの「グローバル」スコープに実際に属しているものであり、グローバルなメソッドが現在のオブジェクトに属するようなことはありません。
これはRuby内部における謎の癖とみなすことも可能かもしれませんが、この癖を理解しておくと役に立つこともあります。
たとえば、あるText UIクラスをテストするときを考えてみましょう。あるメソッドを呼び出すとUIの要素を出力するようなクラスをテストするコードを書きたいとしましょう(それ用のRSpecマッチャーもありますが、ここではシンプルなコード例を使うことにしましょう。テストでstubやexpectを書きたくなりそうなKernelメソッドは他にもいろいろあります)。
class MyUI
def header
puts "-----"
end
end
RSpec.describe MyUI do
let(:instance) { described_class.new }
describe '#header' do
it "outputs header (失敗する)" do
# こうはならない: クラス内部の`puts`はKernel.putsを呼び出さない
expect(Kernel).to receive(:puts).with('-----')
instance.header
end
it "outputs header (正しい方法)" do
# `puts`を所有しているのはこのインスタンスなので成功する
expect(instance).to receive(:puts).with('-----')
instance.header
end
end
end
🔗 ウクライナ通信🇺🇦
ほんの少しお時間をください。私が生活しているウクライナが現在も侵略を受けていることを思い出していただくため、記事の途中にはさむことにしています。どうかお読みください。
とあるニューストピック: 信頼できる情報筋によると、北朝鮮の兵士(12,000人ほど)がウクライナ戦争に参戦するための訓練をロシアで受けているとのことです。
とある背景情報: 一年前の2023年10月21日、軍の訓練キャンプ時代の親友が前線で命を落としました。彼については過去記事で少し取り上げたことがあります。Twitterにもささやかな追悼スレッドを書きました。
とある募金活動: ヘルソン地域の高齢者や障害者などの恵まれない住民を支援しているボランティアであるOlena Samoilenkoさんの募金活動にご協力ください。
引き続き記事をどうぞ。
🔗 独自のトップレベルメソッドはどう振る舞うか
一見「標準のグローバルメソッド」に見えたものが、実は現在のオブジェクトのメソッド(Kernelからincludeしたもの)だったとしたら、以下のようにグローバルメソッドを独自に定義するとどうなるでしょうか?
def my_method
puts "who am i? #{self}"
end
このシチュエーションも、ほぼ同様の結果になります。このようなカスタムグローバルメソッドはObjectクラスのprivateインスタンスメソッドになり、あらゆるオブジェクトから利用可能になります。
method(:my_method) #=> #<Method: Object#my_method() test.rb:1>
my_method # mainオブジェクトのコンテキストで呼び出される
# prints: "who am i? main"
class A
def test
my_method
end
end
a = A.new
a.method(:my_method) #=> #<Method: A(Object)#my_method() test.rb:1>
a.test # Aに属するmy_methodを呼び出す
# prints: who am i? #<A:0x0...>
大事なことなのでもう一度繰り返します。あらゆるトップレベルのメソッドは、実際にはあらゆるオブジェクトに存在します。
これはシステムとしては明確かつ一貫しています。ただし、メタプログラミングのコード(あるメソッドが存在するかどうかをメソッド名で判断し、結果に応じて振る舞いを変更する)ではものすごく奇妙な振る舞いをすることがあります。
実は、先週そういう問題がひとつ見つかりました。Railsの奥深くにあるシリアライズのコードは、現在のオブジェクトがrespond_to?(:avatar_url)となるかどうかに依存していました。そして、それとまったく無関係に、とあるヘルパーモジュールがグローバルスコープにincludeされ、それによってあらゆるオブジェクトでavatar_urlメソッドにアクセス可能になっていたのです。しかし、これはシリアライズのコードで期待される振る舞いではありませんでした。デバッグは楽しいですね!
結論としては、「トップレベルのスコープは、雑多なメソッド(特に一般的な名前のメソッド)で汚されないようクリーンに保つべき」「includeしたモジュールからやってくるメソッドについても同様」ということになります。
他の言語ではどうやっているか
私の知る限り、「グローバル」メソッドについてこのような手法を使っている言語は他にありません(少なくともそれなりに主流の言語では)。オブジェクト指向言語のほとんどは、以下のいずれかに該当しますが、他にも何か見落としている言語があるかもしれません。
- これらを完全に禁止している(JavaやC#では
SomeClass.static_methodのようなことしかできません) - オブジェクトがそもそも
thisやselfのようなコンテキストを持たない(Kotlin、Python、PHP) - そういうメソッドは真の意味でグローバルであり、かつ
thisは常に何らかのグローバルオブジェクトを参照する(JavaScriptのglobalThis、Scala)
🔗 mainというスコープは特殊なのか?
「"main"というスコープは特殊であり、そこにあるものはすべてObjectクラスに直接入る」というヒューリスティックは覚えておく価値があるでしょう。
しかし、トップレベルのコードが「任意のメソッド本体」であるかのように振る舞う、つまり、あたかもObjectクラスのインスタンスメソッドであるかのように実行されることに興味を惹かれる人もいるでしょう。
Rubyではメソッド定義をネストすることは可能ですが、このネストしたメソッド定義は、ローカルに存在するのではなく、親クラスに移動します。
class A
def outer
# ヘルパーに内部メソッドを定義している!
def inner(i) = print("iteration #{i}")
5.times { inner(_1) }
end
end
a = A.new
a.outer # "iteration 0", "iteration 1"などが出力される
a.inner(1000) # "iteration 1000"が出力される。つまりこのメソッドはもうa内に定義されている!
# ...(省略)
A.new.inner(2000) # "iteration 2000"が出力される。つまりこのメソッドはAというクラスに属している
Rubyの他の特徴と同様に、「他のほとんどの言語と違うが、Rubyとしては一貫している」のです。
この知識を活用すれば、mainメソッドとその定義の振る舞いを以下のようにモデリングできます。
my_main = Object.new
def my_main.implicit_top_level
# トップレベルのコードはすべてここに置かれる
def other_method
puts "OK!"
end
end
my_main.implicit_top_level
# `my_main`はObjectのインスタンスなので、
# `other_method`はObject内に定義された
# 確認してみよう:
Object.new.other_method
# prints "OK!"
# また、トップレベルのスコープも含め、すべてがObjectから継承されるので
# 以下の結果を得られる:
other_method
# "OK!"が出力される
以上、Rubyの「少々風変わりだが一貫している」という側面がほとんどの場合維持されていることをお見せいたしました。
この同等性は、定数については崩れます。
mainスコープ内のすべての定数(クラス名やモジュール名も含む)もObject内にネストされますが、main_scope_methodでは使えません。要するに、これはちょっとばかり特殊なのです!言い方を変えれば、Rubyは「定数以外のあらゆるものについては、メソッド本体が同じように振る舞う」「定数名については、クラスやモジュールの本体が同じように振る舞う」ということになります。
🔗 まとめ
繰り返しになりますが、
- Rubyには「トップレベルのメソッド」や「グローバルメソッド」というものは存在しません。レシーバー(コアの場合もユーザー定義の場合も)を明示的に指定しないメソッドは、常に現在のオブジェクトのメソッドです。
- トップレベルに定義したメソッドは、あらゆるオブジェクトのインスタンスメソッドになります。
トップレベルのスコープにincludeしたモジュールは、Objectクラスにincludeされます。 - メソッドが決して本物の「グローバル」になりえないという違いは、ほとんどの場合無視しても構いません。
ただし、メタプログラミングや大規模コードベースのデバッグでは、精密なメンタルモデルを持っておくと有用です。 - すべてのものは
inspect可能であり、かつinspectすべきです。 - Rubyには奇妙でありながら一貫しているものがいろいろあります。
本記事が皆さんのお役に立ちますように :)
お読みいただきありがとうございます。ウクライナへの軍事および人道支援のための寄付およびロビー活動による支援をお願いいたします。このリンクから、総合的な情報源および寄付を受け付けている国や民間基金への多数のリンクを参照いただけます。
すべてに参加するお時間が取れない場合は、Come Back Aliveへの寄付が常に良い選択となります。
関連記事
-
後述するように、
putsはprivateメソッドであり、昔のRubyではself.private_methodを呼び出せなかったため、selfのない裸の語として呼び出すことしかできませんでした(「privateメソッドのレシーバは明示しない」という一般則の一部)。Ruby 2.7以降はこの要件が緩和されて、self.private_methodと明示的に呼び出せるようになりました(ただしselfについてのみ可能)(Rubyの変更点)。 ↩ - Rubyのprivateメソッドは、他の言語と異なり、それを定義したクラスの子クラスでもアクセス可能です。Rubyのprotectedメソッドは、他の言語で言う「フレンド」と呼ばれるメソッド、つまり現在のオブジェクトとクラスが同じである他のオブジェクトでアクセスなメソッドであることを示すのに使われます。 ↩
-
状況によっては、余計な便利メソッドが定義されていない非常にシンプルなクラスが必要になることもあります。特殊な
BasicObjectを明示的に継承することでそのようなクラスを得られます。ただし、この特殊な方法を使わないクラスは、すべてObjectを継承します。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。