ruby scope deep dive
A Deep Dive into Ruby Scopes
2016-03-16
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.