markOnSoftware

The Power of Ruby's Struct

May 25, 2017

I like Structs. They are simple and useful. They provide the same functionalities like classes do. I normally use a Struct if I only need to have accessor methods because it fits the use-case very well.

# Creating a Struct
Person = Struct.new(:first_name, :last_name)

person = Person.new("Bart", "Simpson")
person.first_name # Bart
person.last_name  # Simpson

It’s simple, concise and elegant. Now, here is a class version of that example.

class Person
  attr_accessor :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name  = last_name
  end
end

person = Person.new("Kent", "Beck")
person.first_name # Kent
person.last_name  # Beck

Using classes, it took us several lines to actually match our Struct example. While the difference is so trivial, using Struct is still a good margin of improvement and saved us ample time to write.

With Struct, we can also add a method by supplying a block.

Point = Struct.new(:x, :y) do
  def coordinates
    [x, y]
  end
end

point = Point.new(1, 2)
point.coordinates # [1, 2]

That’s pretty cool, you say? But is there a really useful way to use Structs? I am pretty sure that there a lot of use-cases that structs are a better option than classes but let me show you one use-case where Struct really shines.

Let us assume that we are trying to call an external API that returns a list of restaurants nearby given our x and y coordinates.

# Assume that this is an external API call to some restaurant service.
restaurants = Net::HTTP.get("api/v1/restaurants/nearby?x=17&y=18").body
first_result = restaurants.first_result

first_result # [ { id: 1, name: "McDonalds" }, { id: 2, name: "Pizza Hut" } ]

As you can imagine, the API returns an array of hash and this is how you are supposed to access the results.

restaurants.each do |restaurant|
  restaurant[:id]   # 1
  restaurant[:name] # McDonalds
end

At first glance, it seems that the code is written and can be understood easily. But imagine that one restaurant object contains ten(10) additional attributes. That’s where things start to become complicated. In theory, there is really nothing wrong with this except that by using a Struct, this code becomes more idiomatic. Let’s create a Restaurant struct with an id and a name.

Restaurant = Struct.new(:id, :name)

Now, let’s build a collection of Struct from the external API.

restaurants = Net::HTTP.get("api/v1/restaurants/nearby?x=17&y=18").body
restaurants = restaurants.map do |restaurant|
  Restaurant.new(restaurant[:id], restaurant[:name])
end

restaurants # now shows a collection of struct objects representing a restaurant

So if we were to list all restaurants, we can use it like this.

restaurants.each do |restaurant|
  restaurant.id   # 1
  restaurant.name # McDonalds
end

We could have instantiated the restaurant struct inside the #each method but I prefer not to because with that approach, it is fairly easy to introduce a duplication somewhere else.

Happy reading!


Mark Chavez

Hi, I'm Mark Chavez. Creator of Get Things Done, yamda, public_apis, js_issues, bitcoin_index and a whole bunch of open-source projects. Join me on my adventures as I unfold the good and bad bits about software writing. You can also follow me on twitter and github for more goodies. Also, I love #oss!