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
を継承します。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。