cult3

Working with Structs in Ruby

Apr 15, 2015

Table of contents:

  1. What is a Struct?
  2. Creating a new Struct with attributes
  3. Creating a Struct with methods
  4. When would you use a Struct?
  5. When would you not use a Struct?
  6. What is an OpenStruct?
  7. Conclusion

Last week we looked at using Hashes in Ruby. A Hash is a dictionary of keys and values. This can be useful for passing around structured data that is unknown or could potentially have many key / value pairs.

However it’s sometimes the case that you need to be a little more explicit with structured data.

A Struct is a way to create a very simple class object without the overhead of actually creating a new class.

In today’s tutorial we’re going to be looking at Structs in Ruby, what are they and when you should use them.

What is a Struct?

A Struct is essentially a very simple class that allows you to encapsulate attributes and accessor methods without having to explicitly define a class.

Often when you are passing around data, a simple Array or Hash will be adequate for your needs.

However, Arrays and Hashes lack the explicit attributes and accessor methods of a class.

Creating a new Struct with attributes

You can create a new Struct like this:

Computer = Struct.new(:name, :os)

The new method on the Struct class accepts a list of symbols that will act as the Struct’s attributes. By calling Struct.new you will be returned a new Class that you can save as a constant.

You can then use the Struct as a regular object:

laptop = Computer.new('MacBook', 'OS X')
# => #<struct Computer name="MacBook", os="OS X">

The Struct will have accessor methods for the symbols you passed in on instantiation:

laptop.name
# => "MacBook"

You can also use the square bracket notation to access the attributes:

laptop[:os]
# => "OS X"

You can change the values of the attributes by using the accessor methods or by using square bracket notation:

laptop.name = 'Dell'
laptop[:os] = 'Winblows'

However, if you try to set an attribute that is not defined on the object, you will get an error:

laptop.age = '2 years'
# NoMethodError: undefined method 'age=' for #<struct Computer name="Dell", os="Winblows">

You can return the values of the object as an Array by calling the values method or the to_a method:

laptop.values
# => ["Dell", "Winblows"]

laptop.to_a
# => ["Dell", "Winblows"]

You can also return the object as a Hash by calling the to_h method:

laptop.to_h
# => {:name=>"Dell", :os=>"Winblows"}

Two objects will be the same if they are the same struct subclass and have equal attributes:

other = Computer.new('Dell', 'Winblows')
# => #<struct Computer name="Dell", os="Winblows">

laptop == other
# => true

Creating a Struct with methods

You can also define methods when you create a new Struct by passing a block:

require 'date'

Person =
  Struct.new(:name, :dob) do
    def age
      Date.today.year - dob
    end
  end

Person.new('Jess', 1987).age
# => 28

In this example we’ve created a new Person Struct that has a method age that can calculate the person’s age from the given date of birth.

When would you use a Struct?

So a Struct is like a very simple class definition that can encapsulate attributes and methods.

So when would you use a Struct?

The big benefit of a Struct over a Hash or an Array is the fact that it gives meaning to the data.

For example, imagine you had the following list of coordinates as an Array:

locations = [[40.748817, -73.985428], [40.702565, 73.992537]]

This Array isn’t very explicit and so it’s a bit ambiguous that the values represent longitude and latitudes of locations.

Instead we could use a simple Struct to give meaning to these values:

Location = Struct.new(:longitude, :latitude)

You can now access the attributes of the object using accessor methods, rather than the index of an Array:

location = Location.new(40.748817, -73.985428)

location.longitude
# => 40.748817

You should use a Struct when the data you are working with is closely related. For example, the attributes of a location are closely related and should be encapsulated as an object.

Another benefit of using a Struct over a Hash is that a Struct will require specific attributes, whereas a Hash will accept anything.

For example, imagine we are working with structured data about books in a library. If were were using a Hash we might do something like this:

book = {}
# => {}

book[:tile] = 'Zero to One'
# => "Zero to One"

The Hash doesn’t care that we spelled the title attribute incorrectly.

If we were using a Struct, we would of got an error:

Book = Struct.new(:title)

book = Book.new
# => #<struct Book title=nil>

book[:tile] = 'Zero to One'
# NameError: no member 'tile' in struct

This is important when you are modelling known things in the domain and you expect an error if the attribute is incorrect.

When would you not use a Struct?

So we’ve looked at when you would use a Struct, but when would you not use a Struct?

When you define a new Struct, you need to know all of the attributes that you are defining. Once the Struct has been defined, these attributes can’t be changed.

This is perfect for modelling known things in the Domain of your application, however, it’s not very useful if those attributes are likely to change.

On the other hand, if you have data that is only loosely related such as config items or data options, it’s probably best that you don’t try to artificially encapsulate that data as a Struct. In this case it would be better to use a regular Hash.

A Hash is useful for passing options to an method, storing configuration items or working with loosely related data that will need to be mutated.

So you basically have a Hash on one side, a Class on the other side. A Struct sits in the middle of this spectrum.

What is an OpenStruct?

Ruby also has another similar data structure to Hash and Struct known as an OpenStruct.

An OpenStruct sits in-between the Hash and Struct on the Hash / Struct / Class spectrum:

require 'ostruct'

person = OpenStruct.new
person.name = 'Philip'
person.location = 'UK'

The first thing to notice is that we need to require ostruct in order to use OpenStructs.

Secondly, when you create a new OpenStruct, you don’t have to define it’s attributes. As you can see in the example above, I can create a new OpenStruct and arbitrarily set it’s attributes.

The benefit of using an OpenStruct over a Hash is if you want to work with something that looks and feels more like an object. In other words, it has accessor methods. However, OpenStructs are not good for performance, and so you will more than likely want to replace them with Structs or Classes once you have your design in place.

The benefit of using a Struct over an OpenStruct is that you can define the attributes of the thing you are modelling and you can also define methods as we saw earlier. This is great when you want to model a known thing of your domain, but either you don’t need the overhead of a class or you haven’t settled on your implementation just yet.

Conclusion

Structs are a very useful aspect of the Ruby language. You will often see them dealing with little niggly bits of code that needs encapsulation but do not warrant the overhead of a Class.

When learning a new programming language it’s important to understand idioms like Structs. Structs are usually used for a specific purpose and so being able to understand and recognise their usage will put you in a better position to understand someone else’s code.

Syntax is usually not the most important thing to learn and remember now that every answer is just a Google search away.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.