Many languages allow defining methods globally, which makes them available everywhere. Ruby is not different: you can define a method on the “top–level” and it behave like a global one. However, everything in Ruby is class, which means that these methods should belong to something. In this post we will figure out how global methods really work in Ruby!


Run the irb and define the method like usual:

def foo = 42

If you try to call it—you’ll obviously get 42. What does it mean? It goes without saying that the method works and we can call it, but the interesting part is not about it. We know that Ruby is purely object–oriented language: everything is class or instance of the class.

All examples in this post can be run in IRB or copied to file and run using the ruby interpreter, the result would be the same

Let’s create a new class and try to call #foo from it:

class Example
  def run_foo
    foo
  end
end

Example.new.run_foo # => 42

It works! Our method can be called even from the method in a different class. It behaves like a global method.

Wait, doesn’t it sound suspicious? We defined the method globally and we can call it but there is no global scope: it should be defined in some class. So where is it defined?

Ruby object model

This section is a brief recap of the Metaprogramming Ruby 2 by Paolo Perrotta and Crafting Interpreters by Robert Nystrom

Every class in Ruby is the instance of the class called Class. In the Ruby VM it’s represented as a structure called RClass, which contains:

  • a list of methods;
  • a list of instance variable names;
  • a list of class–level instance variables;
  • a pointer to the class this class is instance of (as we know, it’s Class);
  • a pointer to the superclass, which is a parent class for the current one.

RClass

According to the encapsulation principle of Object–Oriented Programming, data should be bound to the methods that operate on that data into a single unit. However, implementing it in the VM would require copying the byte code of methods to the each instance of the class. Since methods are shared, it makes sense to keep them in the single place, and Ruby keeps them in the instance of the Class (e.g., the byte code of #run_foo is hold in the Example).

Because of the metaprogramming features of Ruby we can access and modify contents of the class in the runtime. With this knowledge, let’s try to understand, where the code from the “top–level scope” is executed.

Examining the top–level scope

Let’s try to understand where we are when we write the code on the top–level scope outside of any class:

puts self #=> main

It did not help a lot: we learned only that we are inside the object that overrides the #to_s method. Let’s check our class:

puts self.class # => Object

Good, we’re inside some Object, which is a default base class for all classes. If we are inside the Object instance and there is a #foo method, we are supposed to be able to create a new instance and call the method:

Object.new.foo #=> private method `foo' called for #<Object:0x00000001402cec50>

Exception: we cannot call the method because it’s private! Let’s make sure that it’s correct:

private_methods.include? :foo #=> true

Does Object have a base class?

Object.superclass # => BasicObject

According to the docs, BasicObject is a base class for new object hierarchies, and it’s completely empty. Therefore, “top–level” methods won’t be available in your class if you inherit right from it:

BasicObject.new.foo # => undefined method `foo' for #<BasicObject:0x00000001058d5648> (NoMethodError)

Let’s summarize our explorations:

  • when some method is defined on the top–level scope, it’s defined in the Object class;
  • this method can be called from any other class, except ones inheriting from BasicObject;
  • the defined method is private, so it cannot be called directly on the instance of the Object.

Looks like that’s the answer! There is no global scope, we can access the method on the top level (because it’s defined here) or in other classes (because they inherit from Object), but this method is not visible from the outside because it’s private.

What about variables?

Let’s repeat the experiment, but instead of defining foo as a method we are going to make it variable:

foo = 42

class Example
  def get_foo
    foo
  end
end

Example.new.get_foo # => undefined local variable or method `foo' for #<Example:0x0000000126ac0d38>

Exception again! The problem is that foo was not available inside the method, because it’s not local for the instance of the Example class.

Like before, let’s check how foo was defined.

instance_variables # => []

No instance variables! Maybe it’s local?

local_variables #=> [:foo, :_, :version, :str]

Correct! Our variable foo is defined locally for the top–level scope (or, more preciselu, for the instance of the Object we are executing our code) and that’s why it’s not available inside the instance of Example, which inherits from the Object.


How does it work?

When we run Ruby script using ruby or REPL using irb, Ruby creates a new workspace for us. By default it will use TOPLEVEL_BINDING for everything, but it’s possible to pass a different binding to the workspace constructor. Here is a source code that runs a new IRB session.