2

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?

1 Answer 1

2

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.
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. Clear, and it addresses my doubt exactly. Do you know of any good resource on this kind of behaviour? Or should I use the source, Luke?
Programming Ruby explains some of this stuff, but I did have to dig through the source to figure out how include deals with the module's ancestors.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.