Kill the Proxy and Save Toplevel
June 17, 2012
Kill the Proxy and Save Toplevel
One of the more curious aspects of Ruby is the “top level” object, otherwise known as main.
$ pry
[1] pry(main)> self
=> main
This main is rather odd entity in that it is a special object with special functionality that delegates to the Object class. For example, using def at the top level actually defines a new private method on the Object class.
[1] pry(main)> def hello_world
[2] pry(main)> "Hello Wolrd"
[3] pry(main)> end
[4] pry(main)> Object.private_instance_methods(false)
=> [:DelegateClass, :hello_world]
Did you have any idea that the top level was so powerful as to pervade every object in the system? Well, usually it doesn’t present an issue since NO ONE IN THEIR RIGHT MIND EVER DEFINES A METHOD AT THE TOP LEVEL, at least not in production applications.
Unfortunately it has some other negative side effects as well. For instance, one can’t as easily create a DSL-based scripting language out of Ruby because the DSL methods will end up in every object, which can cause unexpected consequences. One can work around this by using top level singleton methods instead, but then you have to make sure your users know this too and always prefix their methods with self.. Another issue is that main isn’t even a full proxy for Object. For instance, try defining a dynamically named method at the top level using define_method.
[1] pry(main)> define_method(:hello_world) do
[2] pry(main)> "Hello World"
[3] pry(main)> end
NoMethodError: undefined method `define_method' for main:Object
For a very long time I have advocated that both of the these issues be banished by changing the top level into a self extended module instead of the current half-baked proxy. In effect:
module MAIN
extend self
end
When a Ruby script is executed it would be executed within this MAIN namespace. With it, all expected functionality is available to us, such as def, as well as the module methods like define_method. Methods defined in MAIN would no longer invade every object, leaving us free to use the top level as serves our application best.
Some neat side effects of this approach include the ability to include MAIN in class definitions if it suits our usecase, as well as calling on methods defined on MAIN.
class Cool
def hello_world
if MAIN.method_defined?(hello_world)
MAIN.hello_world
else
"Not so worldy, hello."
end
end
end
But by far my favourite use of such a design is the ability to use the top level for Ruby-based DSL scripting. For a familiar example consider Rake. Rake has a Rake::DSL module which is included in a special execution context where rakefiles are evaluated via special loading code. But with MAIN, it would be possible to mix Rake::DSL directly into it and then rakefiles could be loaded with a simple local require.