Ruby

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.

Daniel P. Clark

Daniel P. Clark is a freelance developer, as well as a Ruby and Rust enthusiast. He writes about Ruby on his personal site.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button