Archive for November, 2010

November 23, 2010

What is the Ruby Top-Level?

The Ruby top-level is an interesting place, behaving at times like a class (the Object class) and other times like an object (a plain instance of Object). It is where we define most of our classes and where we define so-called global functions.

Top-level as an Object

First, let’s verify that top-level is an object:

puts self #=> main
puts self.class #=> Object
puts self.kind_of?(Module) #=> false

So, top-level is a direct instance of the Object class (it is not a Module or a Class) and invoking inspect on it returns main. It also has its own instance variables:

@x = :hello
instance_variables #=> [:@x]
Object.instance_variables #=> []

And, because it is just a plain object and not a Module, it cannot access certain methods:

puts self.ancestors #=> NoMethodError

In summary, the top-level appears at first blush to be a run-of-the-mill Object instance created by a simple Object.new. It is not a class nor a module and so doesn’t inherit methods such as ancestors. It also has its own instance variables that are separate and distinct from the instance variables on the Object class.

Top-level as a Class

Sometimes though, top-level behaves like the Object class itself: Methods defined at top-level become private instance methods on the Object class:

def hello
  puts "hello"
end

method(:hello).owner #=> Object
Object.private_method_defined?(:hello) #=> true

This is how Ruby implements ‘global functions’: since top-level methods are defined on Object they are accessible everywhere, and as they’re private they must be invoked in a functional style (with no explicit receiver).

Similarly, constants defined at top-level become constants defined under Object:

Const = :hello

Object.const_defined?(:Const, false) #=> true

So it seems that when it comes to defining methods and constants, top-level no longer behaves like a simple object and instead acts like the Object class itself.

Top-level as a strange class/object hybrid

The above class-like behaviour of top-level can be explained by the deep-magic of altering the default definee and cref contexts. But top-level also supports some method calls that have no place in the method table of a lowly object:

module M
  def hello
    :hello
  end
end

# wtf?
include M

# wtf?
public
def goodbye
  :goodbye
end

These methods behave as if the receiver is the Object class (include brings the module into Object) but are nonetheless invoked on main. Where do these methods come from?

method(:include).owner #=> #<Class:#<Object:0xb0f0b8>>
method(:public).owner #=> #<Class:#<Object:0xb0f0b8>>

They’re defined on the singleton class of main. Let’s see what other methods are defined there:

puts singleton_class.instance_methods(false).inspect
#=> [:to_s, :public, :private, :include]

Note that these methods (with the exception of to_s) are highly unusual as they act on an object other than their receiver.

This behaviour is a combination of class-like and object-like behaviour. The method itself is defined on the singleton class of the main object yet it is acting on the Object class. Nonetheless these are useful methods to have at top-level and their behaviour more or less corresponds to what you’d expect.

Pulling back the veil

Let’s remove the to_s method from the singleton class of main:

self.to_s #=> "main"

class << self
  remove_method :to_s
end

self #=> #<Object:0xb0f0b8>

As expected, the enigmatic main is just the return value of its singleton’s to_s method.

As main is just an object (albeit an unusual one) we can pass it around to methods, and make use of some of its curious properties (its singleton methods):

def weird(obj)
  obj.include Module.new { def hello; :hello; end }
end

weird(self)

hello #=> :hello

From above, we can invoke the include method on main even when it’s passed as a parameter to a method — but this should not be surprising. The module is then mixed into the `Object` class as per its behaviour with main as implied receiver.

Conclusion

Top-level has a dual-nature. In some circumstances (i.e the getting and setting of ivars and method lookup) it behaves as an ordinary Object instance. In other circumstances (i.e the defining of methods and constants) it acts as if it was the Object class itself. And in still other situations it behaves like a kind of combination of the two (i.e include, public, and private).

Eerie.

Tags: , ,