Use “barewords” to embrace the true power of late-binding in Ruby

In this episode, one of the most popular in the history of RubyTapas, you'll learn that there are two very different ways to think about and work with names in Ruby. One of those ways embraces the true power of “late binding”, allowing you to gracefully change the source and scope of information, without altering the code that uses it.

Today we've been tasked with writing the software for a new generation of intelligent personal disorganizers for the elite citizens of the city of Ankh-Morpork. Previous iterations of the device contained a tiny, mathematically-inclined imp. But they proved unreliable, and in this generation the Imp will be simulated in software instead.

Here's a rough draft of some code for greeting the user of the device.

salutation  = "Most agreeable to see you"
title       = "Commander"
full_name   = "Sam Vimes"
progname    = "Dis-organizer"
version     = "Mark 7"
designation = "Seeree"
service_inquiry = "order you a coffee"

puts "#{salutation}, #{title} #{full_name}. ",
     "Welcome to #{progname} version #{version}. ",
     "My name is #{designation}.",
     "May I #{service_inquiry}?"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

The greeting is essentially a template, with various values interpolated in at appropriate points.

We start out by pulling this code into a method in a class. We make the title and full_name values arguments to the method, but the actual code for outputting the greeting remains unchanged.

class ObsequiousImp
  def greet(title, full_name)
    salutation  = "Most agreeable to see you"
    progname    = "Dis-organizer"
    version     = "Mark 7"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

According to the design the Architecture Wizards have handed down to us, there will be multiple kinds of Imp. Different kinds of Imp will greet their masters differently. Since this particular salutation is specific to this class of imp, we make it a class-level constant. This necessitates modifying the greeting code to reference the constant instead of a local variable.

class ObsequiousImp
  SALUTATION = "Most agreeable to see you"

  def greet(title, full_name)
    progname    = "Dis-organizer"
    version     = "Mark 7"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{SALUTATION}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

After presenting our class to the diamond troll responsible for tying our code into the user interface, we've discovered a problem: the name of the user will actually be coming to us as separate first name and last name values. So we change the method's arguments accordingly, and update the greeting code.

class ObsequiousImp
  SALUTATION = "Most agreeable to see you"

  def greet(title, first_name, last_name)
    progname    = "Dis-organizer"
    version     = "Mark 7"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{SALUTATION}, #{title} #{first_name} #{last_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

The software's name should really be a global value. In fact, for some reason the architects have mandated that it be a truly global value, outside of any kind of namespacing. We grumble a bit but do as we're told, and update the greeting code to reference a global instead of a local variable. The program version, on the other hand, doesn't have the same constraints on it, so we make it a constant in a module that represents the program as a whole. Again, we update the greeting code to reference this namespaced constant.

$progname = "Dis-organizer"

module DisOrganizer
  VERSION = "Mark 7"
end

class ObsequiousImp
  SALUTATION = "Most agreeable to see you"

  def greet(title, first_name, last_name)
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{SALUTATION}, #{title} #{first_name} #{last_name}. ",
      "Welcome to #{$progname} version #{DisOrganizer::VERSION}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

Each user can name their own imp. To make this possible, we make the imp's designation an instance variable. And we update the greeting code with an ‘@' in front of the variable name.

$progname = "Dis-organizer"

module DisOrganizer
  VERSION = "Mark 7"
end

class ObsequiousImp
  SALUTATION = "Most agreeable to see you"

  def initialize(designation)
    @designation = designation
  end

  def greet(title, first_name, last_name)
    service_inquiry = "order you a coffee"

    puts "#{SALUTATION}, #{title} #{first_name} #{last_name}. ",
      "Welcome to #{$progname} version #{DisOrganizer::VERSION}. ",
      "My name is #{@designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new("Seeree").greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

Finally, Imps may be configured with different special features. The question at the end of the greeting should depend on the enabled feature. We add a special_feature attribute to the class, and update the greeting code to send a message to the object it points to.

$progname = "Dis-organizer"

module DisOrganizer
  VERSION = "Mark 7"
end

class CoffeeEnabled
  def service_inquiry 
    "order you a coffee"
  end
end

class ObsequiousImp
  SALUTATION = "Most agreeable to see you"

  attr_accessor :special_feature

  def initialize(designation)
    @designation = designation
  end

  def greet(title, first_name, last_name)
    puts "#{SALUTATION}, #{title} #{first_name} #{last_name}. ",
      "Welcome to #{$progname} version #{DisOrganizer::VERSION}. ",
      "My name is #{@designation}.",
      "May I #{special_feature.service_inquiry}?"
  end
end

imp = ObsequiousImp.new("Seeree")
imp.special_feature = CoffeeEnabled.new
imp.greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

This is, obviously, a highly contrived example. But bear with me, because I'm trying to make a point about values at different scopes.

We started with a set of named values which were all local variables. Step by step, we moved them to different scopes.

  • The title remained a method argument.
  • The full name was broken out into two arguments.
  • The imp's designation became a property of a single instance.
  • The special feature became a member of a composited object.
  • The salutation became a property of the class.
  • The program version became a constant in a program-wide module namespace.
  • The program name became a global.

At every step, not only did we change the scope of the named value, we also changed the greeting code. Even though the actual logic of the greeting code never changed. That means that with each refactoring, we had an extra opportunity to introduce a defect by mis-typing the change to the greeting code, referencing the wrong variable, or (most likely) simply forgetting to update the variable reference.

Let's go through this process again. But this time, let's see if we can do it in a way that has less impact on the core greeting code.

class ObsequiousImp
  def greet(title, full_name)
    salutation  = "Most agreeable to see you"
    progname    = "Dis-organizer"
    version     = "Mark 7"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

To make the salutation a class-level property, we make it a method. The greeting code is unchanged.

class ObsequiousImp
  def salutation
    "Most agreeable to see you"
  end

  def greet(title, full_name)
    progname    = "Dis-organizer"
    version     = "Mark 7"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

We update the method signature to accept a first name and last name, and add a local variable which combines the two into a full name. The greeting code remains unchanged.

class ObsequiousImp
  def salutation
    "Most agreeable to see you"
  end

  def greet(title, first_name, last_name)
    full_name   = "#{first_name} #{last_name}"
    progname    = "Dis-organizer"
    version     = "Mark 7"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

We make the program name global by defining it as a method on the top-level “main” object. Because of the semi-magical properties of the main object, this causes the method to become available as a private methods on all objects.

We declare the program's version as an instance method in the DisOrganizer module, and we include that module into our imp class. As a result, we're able to reference the #version method directly. The greeting code is still unchanged.

def progname; "Dis-organizer"; end

module DisOrganizer
  def version; "Mark 7"; end
end

class ObsequiousImp
  include DisOrganizer

  def salutation
    "Most agreeable to see you"
  end

  def greet(title, first_name, last_name)
    full_name   = "#{first_name} #{last_name}"
    designation = "Seeree"
    service_inquiry = "order you a coffee"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new.greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

For the designation, we add an attr_reader along with the initializer parameter. The greeting code which once referenced a local variable, now references an instance method, and remains unchanged. You may sense a trend by now.

def progname; "Dis-organizer"; end

module DisOrganizer
  def version; "Mark 7"; end
end

class ObsequiousImp
  include DisOrganizer

  attr_reader :designation

  def initialize(designation)
    @designation = designation
  end

  def salutation
    "Most agreeable to see you"
  end

  def greet(title, first_name, last_name)
    full_name   = "#{first_name} #{last_name}"
    service_inquiry = "order you a coffee"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

ObsequiousImp.new("Seeree").greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

Finally, we once again add the special_feature collaborator to our class. This time, we also define a #service_inquiry method which delegates the #service_inquiry method to this special feature collaborator. The greeting code, as before, remains unchanged.

def progname; "Dis-organizer"; end

module DisOrganizer
  def version; "Mark 7"; end
end

class CoffeeEnabled
  def service_inquiry
    "order you a coffee"
  end
end

class ObsequiousImp
  include DisOrganizer
  attr_reader :designation
  attr_accessor :special_feature

  def initialize(designation)
    @designation = designation
  end

  def salutation
    "Most agreeable to see you"
  end

  def service_inquiry
    special_feature.service_inquiry
  end

  def greet(title, first_name, last_name)
    full_name   = "#{first_name} #{last_name}"

    puts "#{salutation}, #{title} #{full_name}. ",
      "Welcome to #{progname} version #{version}. ",
      "My name is #{designation}.",
      "May I #{service_inquiry}?"
  end
end

imp = ObsequiousImp.new("Seeree")
imp.special_feature = CoffeeEnabled.new
imp.greet "Commander", "Sam", "Vimes"
# >> Most agreeable to see you, Commander Sam Vimes. 
# >> Welcome to Dis-organizer version Mark 7. 
# >> My name is Seeree.
# >> May I order you a coffee?

In this second version of the code, we once again defined named values in many different scopes:

  • Local variable
  • Method argument
  • Individual instance
  • A composited object
  • Class-level
  • A module shared among many classes
  • The global namespace

But this time around, we never once touched the core greeting code. We changed the sources of the values it used, but we never modified it.

And this is good, because the greeting code shouldn't care about where the values it interpolates in come from. The only thing that code should be concerned with is how it ties those values together into a human-readable string.

In the Perl programming language, a token in the code which has no quotes around it, no special sigil in front of it, no method-calling syntax preceding it, and no parentheses after it is referred to as a “bareword”. I think barewords are a useful concept in the Ruby language as well.

If you have code that references named values, a bareword is the most flexible way to refer to those values. A bareword in Ruby might refer to a local variable, a method parameter, or a parameter-less method, either public or private. The code containing the bareword doesn't need to be concerned with which of those contexts the value is defined in. In fact, over the life of the program a bareword might start out referring to a local variable and wind up referring to a method, and the transition is transparent from the point of view of any code referring to it.

Any time you refer to a named value in a method, and you're tempted to use an instance variable, class variable, constant, or explicit method call, ask yourself if there's any reason not to refer to the value as a bareword instead. Chances are there isn't. In most cases, using barewords to name values will make your code more flexible, enabling you to move your values around from scope to scope without modifying the code that uses them.

This has been a long episode, so that's enough for now. Happy hacking!