vetements
vetements

Reputation: 45

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

Upvotes: 1

Views: 106

Answers (1)

Stefan
Stefan

Reputation: 114237

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.

Upvotes: 3

Related Questions