A Deep Dive into Ruby Scopes
The Ruby language was designed with a pure object-oriented approach. In Ruby, everything is an object.
Object-oriented design provides encapsulation for properties and actions. Encapsulation’s purpose is to protect methods and data from outside interference and misuse. With encapsulation, everything has certain scopes from which they may be utilized. Several categories of scope in Ruby are global, instance, and local scopes. These are the primary scopes within Ruby, but there are some outliers to the rules, such as class variables and the use of lexical scope with refinements.
Understanding Ruby scopes will go a long way in helping you fully leverage the language. I’ve compiled an in-depth overview to demonstrate how they can assist you with having a more beautiful code base.
Encapsulation
Let’s start out with an example of encapsulation with local variables. (Local variables can be created when you use the equals sign for assignment, such as a = 1
.) First, I’ll show some code written within a begin-end block which has no encapsulation and then a simple method definition which does.
begin a = 4 end puts a if defined?(a) # 4 def local_var_example b = 4 end local_var_example puts b if defined?(b) # => nil
Here we can clearly see that when we assigned the value 4 to the local variable b
that the variable did not exist beyond the scope of the function. This way, you can write as much code as you want within the method and not worry about variables leaking out. Local variables are very scoped; you cannot write a local variable before a standard method definition and retrieve it.
a = "example" def a? puts a end a? #NameError: undefined local variable or method `a' for main:Object
If you want to draw in the environment and access local scope from outside a method definition, you may use closure definitions such as define_method
, proc
, or lambda
.
word = "moo" define_method :x do puts word end x # moo y = proc {puts word} y.call # moo z = lambda {puts word} z.call # moo
Caveat
Local variables take precedence over methods of the same name. To explicitly ask for the method result when there’s a local variable of the same name, you can use the send
method.
a = 4 def a 5 end puts a # 4 send :a # => 5
Instances
With Ruby being an object-oriented language, we get to create multiple object instances of their class definitions. Every Ruby object is its own singleton instance. And I mean that purely by the definition of the word singleton: “a single person or thing of the kind under consideration.” If you had two identical human clones, they would still each be their own individual existence. It’s the same way in Object-Oriented Programming.
When you want to define a kind of object that you plan on having more than one instance of, you write a classification of the object with class.
class Pet def mood "hungry" end end cat = Pet.new dog = Pet.new mouse = Pet.new cat.mood # => "hungry" dog.mood # => "hungry"
The method mood
is an instance method. It’s defined only on all instances created from the class Pet. The one place it is not defined, however, is on the classification of Pet itself.
Pet.mood #NoMethodError: undefined method `mood' for Pet:Class
The Pet class is not an instance of itself; it is a classification of the kind of objects you can propagate from it. This is the intent of Ruby’s class design, where it provides the new
method for you to instantiate individual instances of this kind of class. So the scope of methods defined in this way are all for the objects that will be created from it.
Now it is possible to write methods for the class Pet itself and not for the instances created from it. To do this, we define a method on self
. Here are two ways you may do this:
class Pet def self.definition "Living thing belongs to an owner and is cared for. Can be a plant, animal, or amoeba." end class << self def free? "Not likely. How much money do you have?" end end end Pet.definition # => "Living thing belongs to an owner and is cared for. Can be a plant, animal, or amoeba." Pet.free? # => "Not likely. How much money do you have?" dog = Pet.new dog.free? #NoMethodError: undefined method `free?' for #<Pet:0x00000002845330>
As you can see, the scope of methods defined on self
within a class is only available on that singleton instance of the class. The created objects from this classification will not have those methods defined, as they were written specifically for the class Pet.
The same thing is true with modules. When you define a method with def mood
in a module, it will only be available within the scope of classes that have it “included” (much like what the class method new
does). And if you use the self
identifier for defining a method on a module, it will only be available on that singleton instance of the module and not any class it is inherited in.
module Car def self.description "A vehicle of transportation" end def engine "vroom" end end Car.description # => "A vehicle of transportation" Car.engine #NoMethodError: undefined method `engine' for Car:Module class Boxcar include Car end betsy = Boxcar.new betsy.engine # => "vroom" betsy.description #NoMethodError: undefined method `description' for #<Boxcar:0x000000025e61c0>
The scope of the methods defined are dependent on whether you assign it to the singleton instance of that object or let it be defined on instances from that object.
Singleton Instance
Saying “singleton instance” feels a bit repetitive to me, but it is important to specify so as not to confuse it with the Singleton Design Pattern or the singleton_class
object which exists on most Ruby objects (which is not the singleton instance of the object it is on but is an extra singleton instance of its own).
Ruby is designed where everything is an instance of the Object class and therefore is a singleton instance, meaning that it exists as its own individual self. Yes, this may seem confusing at first, but once you see every module, class, and object as their own singleton instance which may or may not create more singleton instances from their definitions, then things become clearer.
Global Scope
When you write code at the top level, you are writing in global scope. Local variables will not cross over any scope, instance variables will become available to local methods, and methods and classes are available everywhere.
local_variable = 1 # not available in any other scope @instance_variable = 2 # available within methods in the same scope $global_variable = 3 # available everywhere CONSTANT = 4 # available everywhere def a_method # available everywhere end class Klass # available everywhere end module Mod # available everywhere end
All of these, except for global variables, can be encapsulated within singleton instances and maintain the same behavior as described above.
Now the way method definitions are managed in the global namespace is quite interesting. As you may recall that everything in Ruby is an object, they are all also instances of the Object class itself. So the way that global methods are handled is that they are defined as private instance methods on the Object class.
def hey_you "it's me" end Object.private_method_defined? :hey_you # => true Array.send :hey_you # => "it's me" 12345.send :hey_you # => "it's me" nil.send :hey_you # => "it's me"
This is also how classes and modules are made available at lower levels of scope.
Namespacing
Namespacing is the practice of placing code within the scope of another class or module. This is a good practice for both clarifying purpose, usage, and to protect code from potential clashes with other people’s code. You may reuse class or module names within a namespace without overwriting the outer definitions.
module Help def self.me "this is a general help" end end module Dog module Help def self.me "woof woof woof woof" end end end Help.me # => "this is a general help" Dog::Help.me # => "woof woof woof woof"
You’ve protected your code from overwriting the other Help object by namespacing one specifically for Dog. If you want access to constants, classes, or modules at the top level of scope, you may use a double-colon ::
before the object to access it.
CONSTANT = "world" module Greeter CONSTANT = "hello" def self.greet puts CONSTANT + " " + ::CONSTANT end end Greeter.greet # hello world
Refinements
In Ruby, you can reopen every object to add or make changes to it. Making changes outside of the scope of the original definition is known as monkey patching.
class Warn def warn "original behavior" end end class Warn alias_method :_warn, :warn def warn "not " + _warn end end Warn.new.warn # => "not original behavior"
The problem with monkey patching is that the changes happen globally. Any other place in your code base where others have used this object and method has now been changed. Very often this is how things will break; when depended-upon code is modified globally, everyone experiences the change.
Ruby’s solution for this is to use refinements. Refinements allow you to do the same thing as monkey patching but restrict the changes only to the very specific places you specify to use it. This way you won’t break anyone else’s code because your changes are lexically scoped.
module FixForMe refine String do alias_method :_to_s, :to_s def to_s "not " + _to_s end end end class A using FixForMe def a "to be".to_s end end class B def b "to be".to_s end end A.new.a # => "not to be" B.new.b # => "to be"
Here we have changed the behavior for String#to_s
only where we’ve written using FixForMe
, and the to_s
behavior did not change in class B
. This is how lexical scope works.
Lexical scope only goes as far as the visual block of code before you. If you reopen a class and don’t write the using
syntax in it, the refinements behavior will not be there even if you’ve previously used it in the same class. Refinements are well worth using to avoid the pains that monkey patching may bring.
Binding: The Exception to Scope
The Binding object is the only object that lets you pass and modify local variables out of scope. To create a binding, you simply type the method binding
, and it creates a binding of the local environment. You may pass this binding into other scopes and access the local variables from where the binding was instantiated.
module A def self.a(bnd) printf "%s\n", bnd.local_variables x = bnd.local_variable_get :x y = bnd.local_variable_get :y z = bnd.local_variable_get :z printf "%s\n", [x, y, z] bnd.local_variable_set(:z, x + y) end end module B def self.b x = 1 y = 7 z = 0 A.a(binding) puts "x + y = #{z}" end end B.b # [:x, :y, :z] # [1, 7, 0] # x + y = 8
The local variable z
has been changed from a different scope in A
, and the result was within B
.
Class Variables
Class variables are rarely used as the scope is broadened to all instances of the same class. If you were to modify the value of a class variable in one instance, it will be changed for all other instances.
class Building def initialize @@state ||= :built end def state(value = nil) @@state = value if value @@state end end library = Building.new office = Building.new library.state # => :built office.state # => :built office.state :demolished # => :demolished library.state # => :demolished
As you can see, using class variables may cause surprise values in your other classes if they aren’t managed properly. It would probably be wise to think of using these variables for either read-only values or by having a thread-safe system in place.
Conclusion
Ruby is a language that has been designed to make programmers happy, and understanding its scope gives you full leverage in using the language. With it, you may employ many strategies in design that help you toward having a more beautiful code base.
I recommend studying good object-oriented design. Each language/feature is a tool, and tools are most effective when understood and mastered. Encapsulation as a core design in Ruby will serve you well if you use it in the way it was designed to be used. Thankfully Ruby is a very flexible language, so we do have a lot of free reign in how we use it.
Reference: | A Deep Dive into Ruby Scopes from our WCG partner Florian Motlik at the Codeship Blog blog. |