概要
Ruby では、モジュールに定義されている特異メソッドは モジュール名.メソッド名
で実行出来るがインスタンスメソッドではこれは出来ない。しかしモジュールを include するとインスタンスメソッドも同じやり方で実行出来るようになる。もし method_missing を期待している場合は注意が必要。
環境
- Ruby 3.2.2
- irb 1.7.4
背景
モジュールで、method_missing 内で反応する特異メソッドを定義して、その特異メソッドを呼び出す同名のインスタンスメソッドを定義した。だけど、そのモジュールを include したら モジュール名.メソッド名
の形でも同名のインスタンスメソッドが優先して実行されるようになって困った。1
実例
それっぽいモジュールを定義する。
module Human
def self.method_missing(name, *args)
if name.to_s == "hello"
"hello! (singleton)"
else
super
end
end
# 無くても動くけど作法に従った感じで
def self.respond_to_missing?(symbol, include_private)
return true if symbol.to_s == "hello"
super
end
def hello
"hi! (instance)"
end
end
例えばこれを読み込んで irb で実行してみると次のようになる。
irb> Human.respond_to?(:hello)
=> true
irb> Human.hello
=> "hello! (singleton)"
しかしモジュール Human
を include したあとに hello
を呼ぶとインスタンスメソッドの方に取られる。
irb> include Human
=> Object
irb> hello
=> "hi! (instance)"
irb> Human.hello
=> "hi! (instance)"
なのでモジュールを include したあとでも同じ動作を期待しているのなら注意する必要がある。
ちなみに後からでもきちんと特異メソッドを定義すれば同名でもそちらの実行が優先されるようだ。irb で上に続いて以下を実行すると特異メソッドが呼ばれる。
irb* module Human
irb* def self.hello
irb* "hey! (new)"
irb* end
irb> end
=> :hello
irb> Human.hello
=> "hey! (new)"
irb> hello
=> "hi! (instance)"
include 事情に詳しく無いせいだと思うけど何だか不思議な挙動。
-
もう少し細かく言うと Ruby 版の PyCall を使ったモジュールを作っているとき遭遇した。PyCall では Python 側のメソッドを最初に動かすときは、まず method_missing を呼んで、Python 側で実行可能なメソッドであればそれを呼び出す特異メソッドを Ruby 側のモジュールに定義する仕組みになっているらしい。いま作っているモジュールでは、そのモジュールを include したらモジュール名を省略してメソッドを実行したいのでインスタンスメソッドでラップしようと思ったら無限ループになってコケた。 ↩