Search code examples
rubymetaprogrammingmri

Prepend Kernel module function globally


I want to prepend Kernel.rand like this:

# 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 

Unfortunately, the code above does not work as I want to. Prepending Kernel.singleton_class does not work too

It's not required to use prepend feature, any suggestiion that will help to achieve the desired behaviour is welcome


Solution

  • Kernel methods like rand or Math methods like cos are defined as so-called module functions (see module_function) which makes them available as both,

    ... (public) singleton methods:

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

    ... and (private) instance methods:

    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>
    

    To achieve this, Math's singleton class doesn't simply include Math (which would turn all its methods into singleton methods). Instead, each "module function" method gets defined twice, in the module and in the module's singleton class:

    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, ...]
    #                                                                  ^^^
    

    As a result, prepending another module to Math or patching Math in general will only affect the (private) instance method and thus only classes including Math. It won't affect the cos method which was defined separately in Math's singleton class. To also patch that method, you'd have to prepend your module to the singleton class, too:

    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
    

    Which gives:

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

    As well as:

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

    However, as a side effect, it also makes the instance method public:

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

    I've picked Math as an example because it's less integrated than Kernel but the same rules apply to "global functions" from Kernel.

    What's special about Kernel is that it's also included into main which is Ruby's default execution context, i.e. you can call rand without an explicit receiver.