Search code examples
ruby

Ruby, adding methods to a Module, Class or Object


I noticed something odd while I was adding methods to Kernel to make them available globally. It's interesting, and I'm looking for some documentation or good explanation.

Let's look at the code:

file: ./demo.rb

# example 1

module Kernel
  def foo
    puts "I'm defined inside the module!"
  end
end


# example 2

module Bar
  def bar
    puts "I'm included! (bar)"
  end
end

Kernel.send :include, Bar


# example 3

module Baz
  def baz
    puts "I'm included! (baz)"
  end
end

module Kernel
  include Baz
end

Then, in bash and IRB

$ irb -r ./demo.rb
> foo
# I'm defined inside the module!
> bar
# NameError: undefined local variable or method `bar' for main:Object
> baz
# NameError: undefined local variable or method `baz' for main:Object
>
> self.class.ancestors
# => [Object, Kernel, BasicObject] 
>
> include Kernel
>
> self.class.ancestors
# => [Object, Kernel, Baz, Bar, BasicObject]
>
> foo
# I'm defined inside the module!
> bar
# I'm included! (bar)
> baz
# I'm included! (baz)

foo works as expected, and is available for all objects that include Kernel. bar and baz, on the other hand, are not immediately available.
I imagine it's because the evaluation context of IRB (an Object) already includes Kernel, and including a module A inside a module B will not "reload" all previous inclusions of B.
Ok, it makes perfect sense, and in fact re-including Kernel will add the other two methods.

Then, my questions are:

  1. Why does opening Kernel work? (example 1)
  2. if opening the module is treated differently, why isn't the 3rd example working as well?

Solution

  • What happens when you call foo.bar in Ruby? Something like this:

    foo.class.ancestors.each do |klass|
      if klass.public_instance_methods.include? :bar
        return klass.instance_method(:bar).bind(foo).call
      end
    end
    raise NameError
    

    i.e. Ruby searches through the ancestors to find a matching instance method.

    And what happens when you call A.include B in Ruby? Something like this:

    B.ancestors.each do |mod|
      A.ancestors << mod unless A.ancestors.include? mod
    end
    

    B and all of its ancestors become ancestors of A. These two behaviors explain everything:

    1. Opening Kernel works because it's included in Object and is thus an ancestor of every object, meaning its methods (including new ones) can be searched whenever you call a method on any object.
    2. Opening a module isn't treated differently. Your second and third examples are effectively the same. They both don't work because Kernel's ancestors are only searched when it is included, which was before you added new ancestors to it.