Ruby の BasicObject + method_missing で遊んでみる
BasicObject を使いたいことがあるんですね 👀
— ima1zumi (@ima1zumi) September 24, 2020
Twitter 上で BasicObject
についてやり取りしている流れで『BasicObject を使うことってあるんですか?』みたいな質問があったので簡単に BasicObject
+ #method_missing
を使ったデバッグ用のクラスを定義してみました。
任意のオブジェクトを以下の DebugCalled
クラスでラップすると『そのオブジェクトから呼び出されたメソッド』をデバッグ情報として出力する事ができます。
BasicObject
と #method_missing
について
BasicObject
: このクラスを継承するとObject
クラスが継承されなくなり最小限のメソッドのみが組み込まれる#method_missing
: 定義されていないメソッドが呼びされるとこのメソッドにフォールバックされる
DebugCalled クラス
class DebugCalled < BasicObject using ::Module.new { refine ::Object do def __apply__(name, *args) __send__(name, *args) end end refine ::DebugCalled do def __apply__(name, *args) @target.__send__(name, *args) end end } def initialize(target) @target = target end def ==(other) @target == other end def equal?(other) @target.equal? other end # いずれかのメソッドが呼び出されたときに処理をフックする # 呼び出されたメソッドは @target へ委譲する def method_missing(name, *args) result = @target.__send__(name, *args) ensure ::Kernel.puts "[debug called]: #{@target.__apply__(:inspect)}.#{name}(#{args.map { _1.__apply__(:inspect) }.join(", ")}) => #{result.inspect}" end module Refine refine Object do def debug DebugCalled.new(self) end end end end
やってることはそこまで難しくなくて単に #method_missing
を利用して DebugCalled
クラスに定義されていないメソッドが呼ばれた時に @target
にメソッド呼び出しを委譲しているだけになります。
obj = DebugCalled.new("homu") # DebugCalled には #length メソッドは存在しない # なので DebugCalled#method_missing が呼ばれっる # #method_missing 内で "hoge".length が呼び出される obj.length # output: called: homu.length() => 4
ここで肝なのは表題の BasicObject
を継承している点で、 BasicObject
を形容することで DebugCalled
クラスは最小限のメソッドのみが定義されます。
例えば #tap
や #then
#class
などと言ったメソッドは DebugCalled
クラスでは使えなくなります。
なので obj.class
などを呼び出した場合は #method_missing
を経由して @target
の #class
が呼びされれることになります。
obj = DebugCalled.new("homu") # "homu".class が呼びされる obj.class # output: called: homu.class() => String
このように汎用的なメソッドも委譲したい場合に BasicObject
を継承することで利用する事ができます。
で、このクラスを利用すると実際にどのメソッドが呼びされるのかを調べることができます。
def plus(a, b) a + b end def twice(a) plus(a, a) end using DebugCalled::Refine # メソッド内でどのメソッドが呼び出されるのかを調べる plus(1.debug, 2) # output: [debug called]: 1.+(2) => 3 plus(1, 2.debug) # output: [debug called]: 2.coerce(1) => [1, 2] puts "---" twice("homu".debug) # output: # [debug called]: "homu".to_str() => "homu" # [debug called]: "homu".+("homu") => "homuhomu" puts "---" # 標準のメソッドでも出力される(こともある %w(homu mami mado).grep "homu".debug # output: # [debug called]: "homu".===("homu") => true # [debug called]: "homu".===("mami") => false # [debug called]: "homu".===("mado") => false
結構雑につくってみたんですが意外と便利そうな気がしています。
gem にしちゃってもいいかもなあ。
と、いう感じで BasicObject
と #method_missing
を使うとこんな面白いことができるので Ruby は楽しい言語です。