Ruby Objects and Dot Syntax

Coming from JavaScript I'm very accustomed to doing something like this:

var person = { name: 'Rob', city: 'San Francisco' }

console.log( person.city );   // 'San Francisco'

Using dot syntax to access a Hash is second nature to me. That's why I was surprised when I ran into the following error yesterday while writing some Ruby.

person = {name: 'Rob', city: 'San Francisco'}  
 => {:name=>"Rob", :city=>"San Francisco"} 

puts person.city

NoMethodError: undefined method `city' for {:name=>"Rob", :city=>"San Francisco"}:Hash  

"Hmm, weird," I thought. I know I've seen dot syntax used in Ruby before..what gives?

Dot Syntax and the Ruby Hash Object

As it turns out Ruby does not support dot syntax for the Hash object. If I had wanted to access the city property from my previous example I should have done it using the symbol key like so:

person[:city]  
 => "San Francisco"

There are a few data structures that are very similar to Hashes and seeing those used in the wild perhaps threw me off. So I figured I'd write a post about the do's and dont's of dot syntax and how different object types react to it.

Class

The first and most obvious one is the Class object. Really I'm talking about instances of a Class here, for example an instance of class Person might have a city attribute. Here's what that would look like.

class Person  
  def initialize(name, city)
    @name = name
    @city = city
  end

  def name
    @name
  end

  def city
    @city
  end
end

person = Person.new('Rob', 'San Francisco')  
 => #<Person:0x007ff15412a8c0 @name="Rob", @city="San Francisco">

person.city  
 => "San Francisco" 

Since I've defined methods for both name and city, using dot syntax to access those properties basically means we're calling those methods. The methods just return the instance variables, acting as getters. You can shorten this by using attr_reader or attr_accessor.

class Person  
  attr_accessor :name, :city
  def initialize(name, city)
    @name = name
    @city = city
  end
end

person = Person.new('Rob', 'San Francisco')  
 => #<Person:0x007ff15412a8c0 @name="Rob", @city="San Francisco">

person.city  
 => "San Francisco" 

Struct

The Struct object is another commonly used element which allows dot access to its attributes. Quoting from the documentation:

A Struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class.

Examples speak louder than words so here's our Person again.

Person = Struct.new(:name, :city)  
 => Person 

person = Person.new('Rob', 'San Francisco')  
 => #<struct Person name="Rob", city="San Francisco">

person.city  
 => "San Francisco"

As I understand it a Struct is basically sealed after you've given it an initial definition. This means that you can't keep tacking on properties like you can with a Hash

# Continuing from above...

person.age = 28  
NoMethodError: undefined method `age=' for #<struct Person name="Rob", city="San Francisco">

person[:age] = 28  
NameError: no member 'age' in struct  

OpenStruct

Finally we come to the OpenStruct which has both dynamic attributes and dot syntax. The documentation describes it like so:

An OpenStruct is a data structure, similar to a Hash, that allows the definition of arbitrary attributes with their accompanying values.

And again here is our Person from before. Note that OpenStruct needs you to require it.

require 'ostruct'

person = OpenStruct.new  
 => #<OpenStruct> 

person.name = 'Rob'  
 => "Rob" 

person.city = 'San Francisco'  
 => "San Francisco" 

person.city  
 => "San Francisco" 

If you noticed, we didn't need to define the attributes of our Person before creating an instance of it. This means we could keep adding attributes indefinitely. Want your person to respond to age? Just tack it on.

person.age = 28  
 => 28

person.age  
 => 28

For the sake of brevity you can pass in a Hash and OpenStruct will covert it for you.

require 'ostruct'

person = OpenStruct.new(name: 'Rob', city: 'San Francisco')  
 => #<OpenStruct name="Rob", city="San Francisco"> 

person.city  
 => "San Francisco"

This all seems wonderful but there's one huge caveat which comes from the way OpenStruct finds all of these dynamic attributes. As the documentation describes it:

An OpenStruct utilizes Ruby’s method lookup structure to and find and define the necessary methods for properties. This is accomplished through the method methodmissing and definemethod.

This should be a consideration if there is a concern about the performance of the objects that are created, as there is much more overhead in the setting of these properties compared to using a Hash or a Struct.

Definitely keep that in mind if you're writing time sensitive code. In those situations you'll want to use a Hash or a Struct instead.

Source:

http://www.ruby-doc.org/core-1.9.3/Class.html
http://www.ruby-doc.org/core-1.9.3/Struct.html
http://ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html
http://stackoverflow.com/questions/9356704/unable-to-use-dot-syntax-for-ruby-hash
http://www.rubyist.net/~slagell/ruby/accessors.html

You should follow me on Twitter here.