読者です 読者をやめる 読者になる 読者になる

Pryではトップレベルで定義したメソッドがObjectクラスのpublicなインスタンスメソッドになる

表題の通り。

今日Pry触ってて気づいたんだけど、Pryのトップレベルで定義したメソッドはObjectクラスのpublicなインスタンスメソッドになるっぽくて、大抵のオブジェクトから呼び出せる様になる。意図した仕様なのかバグなのかは分からないけど、びっくりしたからメモっとく。

ちなみにRubyのversionは2.1.5。Pryは普通にgem installした。irbRubyを普通に起動した場合には挙動が違ってて、privateなインスタンスメソッドになる。

$ ruby -v
ruby 2.1.5p273 (2014-11-13 revision 48405) [x86_64-darwin14.0]
$ pry -v
Pry version 0.10.1 on Ruby 2.1.5

何に驚いたのか?

以下にコードで例を示す。

[1] pry(main)> def my_method
[1] pry(main)*   p 'my_method'
[1] pry(main)* end
=> :my_method
[2] pry(main)> [].my_method // arrayオブジェクトのメソッドとしてmy_methodが呼び出せる!!
"my_method"
=> "my_method"
[3] pry(main)> [].methods.include?(:my_method) // arrayオブジェクトの呼び出し可能なメソッドにmy_methodが含まれてる!!
=> true

トップレベルで定義したmy_methodメソッドが、空のarrayオブジェクトである[]から呼び出せている。割と驚きの挙動。

なぜこうなるのか?

表題にも書いたけど、Objectクラスのインスタンスメソッドとしてmy_methodが定義されてるのが原因。Objectクラスはほとんどのオブジェクトの上位クラスに位置しているので、大抵のオブジェクトからmy_methodが呼び出せるようになる。

[4] pry(main)> Object.instance_methods(false).include?(:my_method) // instance_methods(false)で、そのクラスで定義されたinstance_methodsが取得できる
=> true
[5] pry(main)> [].class
=> Array
[6] pry(main)> [].class.superclass // Arrayの上位クラスにObjectが位置している
=> Object

自分で適当に定義したクラスもObjectクラスを継承した状態になるので、そこから生成したオブジェクトもmy_methodを呼び出せる。

[7] pry(main)> class MyClass; end
=> nil
[8] pry(main)> my_obj = MyClass.new
=> #<MyClass:0x007fbf6972ce20>
[9] pry(main)> my_obj.my_method
"my_method"
=> "my_method"

Objectよりもさらに上位のBaicObjectを継承すれば、こうはならない。

[10] pry(main)> class MyBasicClass < BasicObject; end
=> nil
[11] pry(main)> my_basic_obj = MyBasicClass.new
=> #<MyBasicClass:0x3fdfb62dab38>
[12] pry(main)> my_basic_obj.my_method
NoMethodError: undefined method `my_method' for #<MyBasicClass:0x007fbf6c5b5670>
from (pry):12:in `__pry__'

ここまでがPryでの話。

irbRubyコマンドで起動した場合はどうなるか

Objectのprivateなインスタンスメソッドとして定義されるので、外部から呼び出しは出来ない。

irb(main):001:0> def my_method
irb(main):002:1>   p 'my_method'
irb(main):003:1> end
=> :my_method
irb(main):004:0> [].my_method
NoMethodError: private method `my_method' called for []:Array
  from (irb):4
  from /Users/(ユーザ名)//.rbenv/versions/2.1.5/bin/irb:11:in `<main>'

じゃあ何でこんな挙動(わざわざObjectクラスにprivateなインスタンスメソッドとして定義)になってるかと言うと、オブジェクトのメソッドを定義する時なんかにトップレベルで定義されたメソッドを使えるようにする為。

irb(main):005:0> class MyClass
irb(main):006:1>   def try_2_my_method
irb(main):007:2>     my_method           // my_methodが使える!!
irb(main):008:2>     my_method           // my_methodが使える!!
irb(main):009:2>   end
irb(main):010:1> end
=> :try_2_my_method
irb(main):011:0> my_obj = MyClass.new
=> #<MyClass:0x007fefdb285938>
irb(main):012:0> my_obj.try_2_my_method
"my_method"
"my_method"
=> "my_method"

要は、Rubyではいわゆるグローバルスコープ的な機能を提供する為にObjectクラスを利用してるっぽい。

この事は、BaiscObjectを継承するクラスの中でインスタンスメソッドを定義する時にはトップレベルで定義したメソッドが使えない事からも確認できる。

irb(main):013:0> class MyBasicClass < BasicObject
irb(main):014:1>   def try_3_my_method
irb(main):015:2>     my_method
irb(main):016:2>     my_method
irb(main):017:2>     my_method
irb(main):018:2>   end
irb(main):019:1> end
=> :try_3_my_method
irb(main):020:0> my_basic_obj = MyBasicClass.new
(Object doesn't support #inspect)
=>
irb(main):021:0> my_basic_obj.try_3_my_method
NameError: undefined local variable or method `my_method' for #<MyBasicClass:0x007fefdc86dc50>
    from (irb):15:in `try_3_my_method'
  from (irb):21
  from /Users/(ユーザ名)/.rbenv/versions/2.1.5/bin/irb:11:in `<main>'

try_3_my_methodメソッドの定義の中でmy_methodを呼び出そうとしてエラーが出てる事が分かる。my_methodはグローバルに定義されてる訳では無くて、あくまでObjectクラスのインスタンスメソッドとして定義されてる事が確認出来たと言える。

まとめ

Pryの驚きの挙動から始めて、Rubyメソッドスコープの仕組みについての知見を得た。あとどうでも良いかもだけど、methodsとかinstance_methodsとかprivate_instance_methodsとかsingleton_methodsみたいな定義されたメソッドをarrayで取得する系のメソッドが今回かなり役立ったので、使っていくと良いと思う。特に、falseを引数として渡すと上位クラス由来のものは取り除いてくれるので、どこでメソッドが定義されてるかが特定しやすかった。