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.