Skip to main content
โšก Calmops

Hash vs Struct in Ruby: When to Use Each

Introduction

Ruby’s Hash and Struct are both used to group related data, but they serve different purposes. Hashes are flexible key-value stores; Structs are lightweight, named data containers with a fixed set of fields. Knowing when to use each leads to cleaner, more expressive code.

Hash: Flexible Key-Value Storage

A Hash maps keys to values. Keys can be any object, but symbols are most common:

# Create a hash
person = { name: "Alice", age: 30, email: "[email protected]" }

# Access values
puts person[:name]   # => "Alice"
puts person[:age]    # => 30

# Add/update
person[:role] = "admin"
person[:age]  = 31

# Delete
person.delete(:email)

# Iterate
person.each { |key, value| puts "#{key}: #{value}" }

# Check existence
person.key?(:name)   # => true
person.key?(:phone)  # => false

Hash Limitations

Hashes don’t give you method-style access:

h = Hash.new
h[:low] = 32.0
h[:low]   # => 32.0
h.low     # => NoMethodError: undefined method 'low' for Hash

You also get no type safety โ€” any key can be added at any time, and typos in key names fail silently:

config = { timeout: 30 }
config[:timout]  # => nil  (typo โ€” no error!)

Struct: Named, Typed Data Containers

Struct creates a class with named fields, accessor methods, and useful built-in methods. It’s ideal for structured data where you know the fields upfront.

# Define a Struct
Point = Struct.new(:x, :y)

p1 = Point.new(3, 4)
puts p1.x      # => 3
puts p1.y      # => 4
puts p1.to_a   # => [3, 4]
puts p1.to_h   # => {:x=>3, :y=>4}

# Structs support equality by value
p2 = Point.new(3, 4)
puts p1 == p2  # => true  (value equality, not identity)

Struct with Methods

Structs accept a block for defining instance methods:

require 'date'

Reading = Struct.new(:date, :low, :high) do
  def mean
    (high + low) / 2.0
  end

  def range
    high - low
  end

  def to_s
    "#{date}: low=#{low}, high=#{high}, mean=#{mean.round(1)}"
  end
end

r = Reading.new(Date.parse("2026-03-29"), 12.0, 24.0)
puts r.mean    # => 18.0
puts r.range   # => 12.0
puts r.to_s    # => "2026-03-29: low=12.0, high=24.0, mean=18.0"

Struct as a Value Object

Money = Struct.new(:amount, :currency) do
  def to_s
    "#{currency} #{format('%.2f', amount)}"
  end

  def +(other)
    raise ArgumentError, "Currency mismatch" unless currency == other.currency
    Money.new(amount + other.amount, currency)
  end

  def *(factor)
    Money.new(amount * factor, currency)
  end
end

price    = Money.new(10.00, "USD")
tax      = Money.new(0.80, "USD")
total    = price + tax
doubled  = total * 2

puts price.to_s   # => "USD 10.00"
puts total.to_s   # => "USD 10.80"
puts doubled.to_s # => "USD 21.60"
puts price == Money.new(10.00, "USD")  # => true

Struct vs Hash: Side-by-Side

# Hash approach
temperature_hash = { date: Date.today, low: 15.0, high: 28.0 }
puts temperature_hash[:low]   # => 15.0
puts temperature_hash.low     # => NoMethodError

# Struct approach
Temperature = Struct.new(:date, :low, :high)
temp = Temperature.new(Date.today, 15.0, 28.0)
puts temp.low    # => 15.0  (method access)
puts temp[:low]  # => 15.0  (also supports hash-style access)
puts temp.to_h   # => {:date=>..., :low=>15.0, :high=>28.0}

Struct with keyword_init

Ruby 2.5+ supports keyword_init: true for named argument construction:

Person = Struct.new(:name, :age, :email, keyword_init: true)

alice = Person.new(name: "Alice", age: 30, email: "[email protected]")
puts alice.name   # => "Alice"

# Order doesn't matter with keyword_init
bob = Person.new(email: "[email protected]", name: "Bob", age: 25)
puts bob.email    # => "[email protected]"

# Missing fields default to nil
charlie = Person.new(name: "Charlie")
puts charlie.age  # => nil

Struct vs Data (Ruby 3.2+)

Ruby 3.2 introduced Data โ€” an immutable value object similar to Struct but frozen:

# Data: immutable, no setters
Point = Data.define(:x, :y)

p = Point.new(x: 3, y: 4)
puts p.x    # => 3
p.x = 5     # => NoMethodError: undefined method 'x='

# Struct: mutable by default
MutablePoint = Struct.new(:x, :y)
mp = MutablePoint.new(3, 4)
mp.x = 10   # OK

Use Data when you want immutable value objects (coordinates, money, dates). Use Struct when you need mutable fields.

Practical Use Cases

Struct for CSV/Tabular Data

require 'csv'

SalesRecord = Struct.new(:date, :product, :quantity, :revenue, keyword_init: true) do
  def revenue_per_unit
    revenue.to_f / quantity.to_i
  end
end

records = CSV.foreach('sales.csv', headers: true).map do |row|
  SalesRecord.new(
    date:     row['date'],
    product:  row['product'],
    quantity: row['quantity'].to_i,
    revenue:  row['revenue'].to_f
  )
end

total_revenue = records.sum(&:revenue)
top_product   = records.max_by(&:revenue)
puts "Top product: #{top_product.product} (#{top_product.revenue})"

Hash for Dynamic/Unknown Keys

# Hash is better when keys are dynamic or unknown at design time
def parse_config(file)
  YAML.load_file(file)  # returns a Hash โ€” keys vary per config file
end

# Or when building a response object with variable fields
def api_response(status, **data)
  { status: status, timestamp: Time.now }.merge(data)
end

Struct for API Response Parsing

GithubRepo = Struct.new(:name, :full_name, :stars, :language, keyword_init: true) do
  def popular?
    stars > 1000
  end
end

# Parse API response
data = JSON.parse(response.body)
repo = GithubRepo.new(
  name:      data['name'],
  full_name: data['full_name'],
  stars:     data['stargazers_count'],
  language:  data['language']
)

puts repo.popular? ? "#{repo.name} is popular!" : "#{repo.name} is growing"

Comparison Table

Feature Hash Struct
Field access h[:key] s.field or s[:field]
Dynamic keys Yes No (fixed at definition)
Method access No Yes
Value equality Yes Yes
Iteration each each_pair, members
Immutable version .freeze Data (Ruby 3.2+)
Keyword init N/A keyword_init: true
Custom methods No Yes (via block)
Memory Higher (flexible) Lower (fixed layout)

The Effective Ruby Recommendation

From Effective Ruby (Item 10): Prefer Struct over Hash for structured data. When you know the fields upfront, Struct gives you:

  • Method-style access (more readable)
  • Value equality out of the box
  • A place to add methods
  • Better documentation (field names are explicit)
  • Slightly better performance

Resources

Comments