Exploring Ruby Structs

Ruby structs are a simple but often overlooked data structure.

Structs basically provide a mechanism for creating a class with a fixed set of data attributes. They can be a great alternative to using hashes and arrays to hold data with fixed structure. They also provide one an easy way to create value objects.

Basic Usage

The struct constructor returns a class that accepts the specified attributes in the constructor and provides accessors for them:

Person = Struct.new(:name, :age)

Use it just like any other class:

bob = Person.new('Bob', 50)
bob.name # => "Bob"

Simple equality, instances are equal to another instance of the same struct (or a subclass) with the same values.

bob2 = Person.new('Bob', 50)
bob == bob2 # => true
old_bob = Person.new('Bob', 100)
bob == old_bob # => false
similar_bob = Struct.new(:name, :age).new('Bob', 50)
bob == similar_bob # => false

Implements

Enumerable
to iterate over all values:

# Enumerable
bob.each { |f| puts f }
# => "Bob"
# => 50

Hash-like access using strings or symbols for the property keys:

bob['name'] # => "Bob"
bob[:name] # => "Bob"

Array-like access using indexes:

bob[0] # => "Bob"
bob[1] # => 50
AnnualSales = Struct.new(:jan, :feb, :mar, :apr, :may, :jun,
:jul, :aug, :sep, :oct, :nov, :dec)
sales_2014 = AnnualSales.new(10, 20, 10, 30, 40, 50,
20, 10, 0, 5, 10, 20)
# Get sales in February and December
sales_2014.values_at(2, 11) # => [10, 20]
# Get sales this summar (June, July, August)
sales_2014.values_at(5..7) # => [50, 20, 10]

Between the ability to iterate through the values, position preservation, and indexed access, Structs provide functionality similar to named tuples in python and other languages.

If you're using ruby >= 2.0.0 you can turn a struct into a hash via

#to_h
:

bob.to_h # => {:name=>"Bob", :age=>50}

Easily extensible:

Person = Struct.new(:name, :age) do
def old?
age > 18
end
end
bob = Person.new('Bob', 50)
p bob.old? # => true

Since structs are just regular classes you can also just subclass it:

class Person < Struct.new(:name, :age)
def greet
"Hi #{name}"
end
end

This approach is slightly less optimal than the block method shown earlier. The subclass approach creates a class just to sub-class it which is rather useless. The block approach incorporates the methods into the class definition the struct constructor uses directly.

Alternatives

OpenStruct, Hashie, and ActiveModel are among the most popular of several dozen other projects that provide similar functionality. I put together a small benchmark comparing these implementations based on a post on stackoverflow, see Struct vs. OpenStruct vs. Hashie vs. ActiveModel. It's not terribly scientific but demonstrates that structs are really fast,

OpenStruct
is really slow and Hashie and ActiveModel are close enough to not matter for most purposes. As always, choose the best structure to model your domain before worrying about performance.