Techioz Blog

カーネルモジュール関数をグローバルに先頭に追加する

概要

次のように Kernel.rand を先頭に追加したいと思います。

# I try something like

mod = Module.new do
  def rand(*args)
    p "do something"

    super(*args)
  end
end

Kernel.prepend(mod)
# And I expect this behaviour

Kernel.rand            #> prints "do something" and returns random number
rand                   #> prints "do something" and returns random number
Object.new.send(:rand) #> prints "do something" and returns random number 

残念ながら、上記のコードは期待どおりに動作しません。 Kernel.singleton_class を先頭に追加しても機能しません

先頭に追加機能を使用する必要はありません。目的の動作を実現するのに役立つ提案は歓迎です。

解決策

rand などのカーネル メソッドや cos などの Math メソッドは、いわゆるモジュール関数 (module_function を参照) として定義されており、両方として使用できます。

… (パブリック) シングルトン メソッド:

Math.cos(0)  # <- `cos' called as singleton method
#=> 1.0

…および (プライベート) インスタンス メソッド:

class Foo
  include Math

  def calc
    cos(0)   # <- `cos' called from included module
  end
end

foo = Foo.new

foo.calc
#=> 1.0

foo.cos(0)   # <- not allowed
# NoMethodError: private method `cos' called for #<Foo:0x000000010e3ab510>

これを実現するために、Math のシングルトン クラスには、単に Math が含まれるわけではありません (これにより、すべてのメソッドがシングルトン メソッドに変換されます)。代わりに、各「モジュール関数」メソッドは、モジュール内とモジュールのシングルトン クラス内で 2 回定義されます。

Math.private_instance_methods(false)
#=> [:ldexp, :hypot, :erf, :erfc, :gamma, :lgamma, :sqrt, :atan2, :cos, ...]
#                                                                  ^^^

Math.singleton_class.public_instance_methods(false)
#=> [:ldexp, :hypot, :erf, :erfc, :gamma, :lgamma, :sqrt, :atan2, :cos, ...]
#                                                                  ^^^

結果として、別のモジュールを Math の前に追加したり、Math にパッチを当てたりすると、一般に (プライベート) インスタンス メソッドにのみ影響するため、Math を含むクラスにのみ影響します。 Math のシングルトン クラスで別途定義された cos メソッドには影響しません。そのメソッドにもパッチを適用するには、モジュールをシングルトン クラスの先頭にも追加する必要があります。

module MathPatch
  def cos(x)
    p 'cos called'
    super
  end
end

Math.prepend(MathPatch)                 # <- patch classes including Math
Math.singleton_class.prepend(MathPatch) # <- patch Math.cos itself

これにより、次のようになります。

Math.cos(0)
# "cos called"
#=> 1.0

同様に:

foo.calc
# "cos called"
#=> 1.0

ただし、副作用として、インスタンス メソッドもパブリックになります。

foo.cos(0)
# "cos called"
#=> 1.0

Math を例として取り上げたのは、Math がカーネルほど統合されていないためですが、カーネルの「グローバル関数」にも同じルールが適用されます。

カーネルの特別な点は、カーネルが Ruby のデフォルトの実行コンテキストである main にも含まれていることです。つまり、明示的なレシーバーなしで rand を呼び出すことができます。