[Ruby] sendとmethod_missingと分かりづらい仕様

つい最近、腰痛で一時動けなかくつらい思いを久々にしました。
昔痛めて生涯の友となった腰痛ですが、数年に一回来るので辛いです。


さて、最近はRubyでとあるライブラリを作成しているのですが、黒魔術を使わざるを得ない状況はどうしても出てくるものです。
そこで不可解なエラーに遭遇しました。

コードを単純化するとこうなります。

class Test
  def method_missing(method, *args)
    Test1.new.send(method, *args)
  end
end

class Test1
  def method_missing(method, *args)
  end
end

Test.new.select(1)


実行するとなぜかエラー。

$ ruby hoge.rb 
hoge.rb:3:in `select': wrong argument type Integer (expected Array) (TypeError)
        from hoge.rb:3:in `method_missing'
        from hoge.rb:12:in `<main>'

試しに他の存在するメソッド(例えば__id__とか)を呼ぶと問題は無い。
Test、Test1クラスはObjectクラスを継承しただけなので、ほぼ同じ。
https://docs.ruby-lang.org/ja/latest/class/Object.html を見るとselectは存在しないはず...
仕方なく同僚に相談して、二人で悩んで解決しました

ちなみにTest1を使用しなくてもこれで反応します。

class Test
  def method_missing(method, *args)
    send(method, *args)
  end
end

Test.new.select(1)


まず、見るべきはsendの仕様。
https://docs.ruby-lang.org/ja/latest/class/Object.html#I___SEND__

send, __send__ は、メソッドの呼び出し制限 にかかわらず任意のメソッドを呼び出せます。


メソッドの呼び出し制限 にかかわらず??


そして、private methodを見るとselectが存在する!

> Object.new.private_methods.sort
=> [:Array, :Complex, :DelegateClass, :Float, :Hash, :Integer, :Rational, :String, :__callee__, :__dir__, :__method__, :`, :abort, :at_exit, :autoload, :autoload?, :binding, :block_given?, :caller, :caller_locations, :catch, :default_src_encoding, :eval, :exec, :exit, :exit!, :fail, :fork, :format, :gem, :gem_original_require, :gets, :global_variables, :initialize, :initialize_clone, :initialize_copy, :initialize_dup, :irb_binding, :iterator?, :lambda, :load, :local_variables, :loop, :method_missing, :open, :p, :print, :printf, :proc, :putc, :puts, :raise, :rand, :readline, :readlines, :require, :require_relative, :respond_to_missing?, :select, :set_trace_func, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined, :sleep, :spawn, :sprintf, :srand, :syscall, :system, :test, :throw, :trace_var, :trap, :untrace_var, :warn]

つまり、この流れでエラーとなっているようです

  • sendはprotect、privateであっても呼び出しが可能
  • Object#selectがprivateメソッドとして用意されている
  • よって、Test1#method_missingを呼ぶ前にselectメソッドを見つけてエラーとなる

さて、問題はどうするか?ということですが、メソッドのリストをよく見るとpublic_sendというのがあるようです
sendをpublic_sendにすると問題なく呼ばれたようで解決しました。

class Test
  def method_missing(method, *args)
    Test1.new.public_send(method, *args)
  end
end

初めはRubyのバグを踏んだ?と思いましたが、よくよく調べると仕様通りの動作ですがなかなかややこしい問題でした。
黒魔術はほどほどに...