Skip to content

Ruby Hash vs Struct

Ruby has a versatile Hash. New Ruby programmers use it all the time. I use it all over the codebase. They are so generic that anywhere we want flexibility in passing data across, we hash it. Also, connecting with the JS world, hashes seem a good fit for free-floating JSON objects.

Even though Hash is a great fit for many use cases, it still lacks in certain areas. At that moment, we may consider using class. Well, Ruby provides a middle ground. Welcome, Struct.

Frankly, I was introduced to OpenStruct before Struct and I thought it's better to go with OpenStruct because it is schemaless just like Hash. That might be an attractive feature but OpenStruct comes with a performance penalty so better avoid using it whenever possible. Use Hash or Struct instead of OpenStruct. Anyway, let's get back to Struct.

Basics of using Struct

Define a struct with attributes we want to read/write

Person = Struct.new(:name, :email, :activated)

amy = Person.new(name: 'Amy', email: 'amy@example.com', activated: false)
# -> <struct Person name="Amy", email="amy@example.com", activated=false>

mohan = Person.new(name: 'Mohan')
# -> <struct Person name="Mohan", email=nil, activated=nil>

[amy, mohan].each do |person|
  puts "Hi 👋, I'm #{person.name}"
end

# Outputs
# Hi 👋, I'm Amy
# Hi 👋, I'm Mohan

In this example, we defined a Person that is a Struct with attributes :name, :email and :activated.

Then we created two instances of Person using .new method and passing values to attributes. We can skip some or all of the attribute values and those will be nil.

To read, we simply call the method of the same name as the attribute person.name

We can also update the values of attributes, such as

mohan.email = 'mohan@example.com'
amy.activated = false

We can't create a new attribute once a struct is defined. So we won't be able to do amy.age = 25. It will give NoMethodError error.

Advantages of using Struct instead of Hash

What are the possibilities with Struct that give them an advantage over Hash? Let's go through each with example

Custom methods

We can define any method on a Struct. The constructor of Struct takes a block inside which we can define methods. These methods will be available to us in all instances of that struct.

Counter = Struct.new(:value) do
  def increment(by: 1)
    self.value += by
  end

  def decrement(by: 1)
    self.value -= by
  end
end

c = Counter.new(value: 12) # -> <struct Counter value=12>
c.increment                # -> 13
c.decrement(by: 10)        # -> 3

Default values

Using initialize method, we can create a Struct that has default values. Let's look at our Person example, we can provide a default value of activated as false.

Person = Struct.new(:name, :email, :activated) do
  def initialize(name:, email:, activated: false)
    super(name:, email:, activated:)
  end
end

rom = Person.new(name: 'Rom', email: 'rom@example.com')
# -> <struct Person name="Rom", email="rom@example.com", activated=false>

As you can see, in the above example, we got false instead of nil for activated. We defined a constructor for the struct and provided a default value for activated. This is a bit of a boilerplate and I wish Ruby had a better way to provide defaults.

Convert to hash anytime

We can convert a Struct to Hash anytime using .to_h. It also has default conversion for Array, String, Set and Enumerator. Let's try out these for our Person.

rom = Person.new(name: 'Rom', email: 'rom@example.com')

rom.to_s
# -> "#<struct Person name=\"Rom\", email=\"rom@example.com\", activated=false>"

rom.to_h
# -> {:name=>"Rom", :email=>"rom@example.com", :activated=>false}

rom.to_a
# ["Rom", "rom@example.com", false]

rom.to_set
# -> <Set: {"Rom", "rom@example.com", false}>

rom.to_enum
# -> <Enumerator: ...>

rom.to_enum.each { |a| puts a }
# Outputs
# Rom
# rom@example.com
# false

Defined shape

Struct has a defined shape and as we can provide methods to access or update data, they are better suited for replacement of hashes where we know what shape of data expect. It will also eliminate excessive code about checking for nil values in a hash and providing short circuits (defaults).

ram = {name: 'Ram', email: 'ram@example.com'}
sasuke = Person.new(name: 'Sasuke', email: 'sasuke@example.com')

ram_activated = ram[:activated] || false
sasuke_activated = sasuke.activated

In this example, we had to use || false to get the expected value for ram's activated attribute as it is not defined in the hash and there is no way to set that automatically. Whereas, when we are using a struct such as Person, we got the expected default from .activated. If we have to do this for a hash at multiple places in the code, then that will get messy to maintain. Struct gives us better alternatives to work with.

Conclusion

So should I always use Struct? What about class?

My take is to use class for project-wide code but use Struct when a well-defined shape is needed to be passed around without explicitly calling its constructor from different places in the code. In short, I treat Struct as an unannounced shape whereas a class is a well-announced standalone shape. In actual projects, I see that within a class I make internal Structs to work with. Also, as a return value, I use Struct in place of a hash.

Further reading

  • You can read more about the comparison of different types and their performances in this well-written article by Dragon Astronauts.
  • Feel free to go through the API doc of Struct on the ruby-doc