The scope of a program changes whenever you hit the def, class, and module keywords, the three Ruby ‘scope gates’. The scope determines the value of self and local variables that are accessible at any given time. Read my blog post on the main object and Ruby’s self keyword if you need some review before diving deeper into Ruby scopes.
# filename: ex1.rb # top level scope p self # => main (the top-level scope object) x = 42 # local variable bound to main object def hi # begin method scope x end # end method scope # back in top level scope p hi() # => NameError: undefined local variable or method `x' for main:Object # x is still accessible in the top level scope p x # => 42
When the ex1.rb file is run (example above) a new scope is opened with the keyword self pointing to the main object. This is referred to as the top-level scope and all local variables and methods defined in the top-level scope are bound to the top-level main object. When the hi() method is defined, a new scope is opened that doesn’t have access to any of the local variables defined in the top-level ‘outer’ scope. After the hi() method is defined, the program reverts back to the top-level scope and the local variables bound to the main object are accessible again.
# filename: ex2.rb # top level scope class A # begin class scope y = 99 p y # => 99 def self.hey # begin method scope y # => error because y is no longer in scope end # end method scope # back in class scope p y # => 99 (local variables are accessible again) end # end class scope # back in top level scope
Local variables defined in the class scope are accessible anywhere within the class scope. When the self.hey() method opens a new scope, none of the local variables defined in the class scope are accessible. The class scope can be thought of as an ‘outer scope’ and the method scope is an ‘inner scope’. None of the local variables defined in the outer scope are accessible by the inner scope.
Instance variables are bound to objects and are accessible whenever the scope causes the self keyword to equal the right object.
# filename: ex3.rb # top level scope p self # => main @hero = 'robert shiller' # @hero is bound to main object def hero # begin method scope p self # => main @hero # accessible because implicit self is the main object end # end method scope # back in top level scope
In ex3.rb, the @hero instance variable is set in the top level scope and accessible in the hero() method, even though the scope has changed. Instance variables are bound to objects and are fetched for the implicit self receiver unless an explicit receiver is used. In ex3.rb, the self keyword points to main in both the top level scope and the hero() method, so the @hero instance variable is accessible in the hero() method.
When modules are included in a class, self is assigned to an instance of the class, so the class’s instance methods have access to the methods and instance variables defined in the module.
# filename: ex4.rb module M # begin module scope def ok_setter # begin method scope @ok = 'ok, alright' end # end method scope end # end module scope class C # begin class scope include M def ok_reader # begin method scope # the module's instance methods are accessible ok_setter @ok end # end method scope end # end class scope
Nesting modules and classes encapsulates code and decreases the liklihood of variable collisions (i.e. two classes with the same name in the same scope). The following example illustrates all the scopes that are opened and closed when modules and classes are nested.
# filename: ex5.rb # top level scope module Wrapper # begin Wrapper module Container # begin Container class A # begin A def hi # begin method 'Wrapper::Container::A#hi' end # end method end # end A end # end Container class A # begin A def hi # begin method 'Wrapper::A#hi' end # end method end # end A end # end Wrapper # end top level scope
A new Ruby scope is created whenever the keywords def, module, and class are used. As a result, these keywords are referred to as ‘scope gates’ in Ruby. Since these keywords are used all the time, new scopes are created constantly. In ex5.rb, there are two A classes (Wrapper::Container::A and Wrapper::A), but they’re defined in different scopes, so the constant variable names do not collide.
Navigating a complex jungle of Ruby scopes can be confusing, especially when dealing with deep nesting and multiple files. Master the scope basics and you’ll save yourself from some ‘banging your head against the wall’ situations.