# Thunder-Struct

I just found out about this super handy built-in class in Ruby called a Struct, and I wanted to share.

## What is a Struct

Very simply put, a Struct is a data object that is simulates a quick way to declare a class. A common need in a program is to glue a bunch of related data together. You can think of our options for that as a sort of continuum.

Let’s say, for example, that you have a student grade that you want to keep track of. You could define a class to explicitly lay out your data structure.

class Grade

@assignment = assignment
end
end



Or, seeing all of the typing, you could violently overreact and go in the opposite direction.

jerrys_grade = ["Physics Quiz #3", 85, "B"]


For a simple data object like this where there aren’t really methods to define yet, I think a lot of people’s first thought is to try to be more in the middle of the spectrum.

jerrys_grade = {
assignment: "Physics Quiz #3",
}


But there’s another, even awesomer (yeah, I said it) way: the Struct.

Grade = Struct.new(:assignment, :number_grade, :letter_grade)



## What Do I Do With It?

Structs are cool because they give you a lot of flexibility in how you access and use the data without giving up any explicit-ness.

jerrys_grade.assignment
# => "Physics Quiz #3"
# => 85
# => "B"

# Jerry must have done some extra credit


They also come pre-built with some of the features that you’d have to build yourself if you used an actual class definition.

# Equality
# => true

# Can be enumerated
jerrys_grade.each { |datapoint| puts datapoint }
# => Physics Quiz #3
# => 95
# => A

# Or enumerated like a hash with key and value
doras_grade.each_pair { |key, value| puts "#{key}: #{value}" }
# => assignment: Physics Quiz #3

# Can be converted to an array or hash
# => ["Physics Quiz #3", 95, "A"]


And if you need your own methods, you can add them via a block when creating them.

Grade = Struct.new(:assignment, :number_grade) do
when (0..64)
"F"
when (65..69)
"D"
when (70..79)
"C"
when (80..89)
"B"
when (90..100)
"A"
else
"WTF"
end
end
end


Neat, right? I bet right now, you’re feeling… inde-struct-ible?

But now, you might have the same questions that I did once I got to this point.

## What’s the Benefit?

Why would I want to use a Struct instead of a Class or a Hash? When is one better than the other? It’s all a matter of what you’re trying to do.

### Static vs. Dynamic Attributes

You can’t easily add attributes to a Struct, so in applications where your keys/attributes need to be dynamic or you don’t know what they’ll be ahead of time (think “word-counter”), you might be better suited with a Hash. On the other hand, if you know exactly what keys you’ll need, the object-based “dot access” looks nice, and having a constructor can save you a lot of time typing. It can be a good, clean way of signaling your design intent. A good example of having well-defined attributes is if you work with an Address.

# This is nice and clear, and you don't have to retype the keys
Address = Struct.new(:street, :city, :postal_code, :country)

"211 Main St.",
"Savanna, IL",
61074,
"USA"
)

# This is ok, but more typing, harder to discern intent,
# and more prone to typos and misinterpretation
more_typing = {
street: "211 Main St.",
city: "Savanna, IL",
postal_code: 61074,
country: "USA"
}


### Less Error Prone

Structs tend to be more rigid, which can help protect from errors and uncaught typos.

whoops_i_made_a_typo = {
stroot: "211 Main St.",
# Forgot that I commented out this one: city: "Savanna, IL",
postal_code: 61074,
country: "USA"
}
# And no errors get thrown

# And if I access a key that doesn't exist
# => nil

# But if we construct an Address with too many keys:
"123 Fake St.",
"Fake, FK",
12345,
"USA",
"BLARGLFLURB"
)
# => ArgumentError: Struct size differs

# And if we try to access a key that doesn't exist?
the_worlds_greatest_place["potato"]
# => NoMethodError: undefined method 'potato'


### They’re Fast

Structs can also be much faster to create than hashes.

require 'benchmark'

Benchmark.bm 10 do |bench|
bench.report "Hash" do
1_000_000.times do { name: "Ryan", coolness: 27 } end
end

bench.report "Struct" do
Dev = Struct.new(:name, :coolness)
1_000_000.times do Dev.new("Ryan", 27) end
end
end

#                  user     system      total        real
# Hash         0.750000   0.010000   0.760000 (  0.762069)
# Struct       0.270000   0.000000   0.270000 (  0.272200)


### They’re Compact

And, because they don’t have all of the methods that come with Hashes, they end up using less memory. This isn’t a huge savings, but maybe it counts if you’re working in a constrained environment or with a bazillion data points.

require 'objspace'

Dev = Struct.new(:name, :age)
p = Dev.new("Ryan", 25)
q = { name: "Ryan", age: 25 }

puts ObjectSpace.memsize_of(p)
# => 40
puts ObjectSpace.memsize_of(q)
# => 232


However, if you need some of those methods, maybe you’re better off with a Hash.

## But Don’t Go Crazy

If you have a lot of methods and custom functionality, or if the object is a larger part of your application, it’s probably better to stick with our good old friend, the Class. While the upside of Structs is that they are quick and easy to create on the fly, that’s also their downfall. Having too many structs, or ones that are too large, can be hard to read and might miss out on the benefits of defining a Class the normal, idiomatic way. Just like most things in programming, it all comes down to trade-offs.

## One Last Thing: OpenStructs

I thought I should add this in because, when I initially started researching, I thought OpenStructs were the same as regular structs. Some StackOverflow answers can be misleading. OpenStructs get created in a similar manner to regular ones, except for the fact that they’re essentially anonymous.

require 'ostruct'

me = OpenStruct.new(name: "Ryan", blog: "assert_not magic?")


The main difference is that you can dynamically add attributes to this kind.

me.favorite_food = "chorizo breakfast burrito"


If you tried that with a regular struct, you’d get a NoMethodError.

What’s the trade-off here? You don’t get to name it. Also, OpenStructs are outrageously slow and memory intensive . Check out these benchmarks.

## Wrap-Up

So that’s it. I thought it was a super handy tool — kind of like a NamedTuple in Python, but with even nicer syntax! Now we’re one trick closer to Ruby mastery!

More Resources:

Author: Ryan Palo | Tags: ruby tricks design-intent struct |

Like my stuff? Have questions or feedback for me? Want to mentor me or get my help with something? Get in touch! To stay updated, subscribe via RSS