10
OCT2008
All About Struct
I build small little data classes all the time and there's a reason for that: Ruby makes it trivial to do so. That's a big win because we all know that what is a trivial data class today will be tomorrow's super object, right? If I start out using a simple Array
or Hash
, I'll probably end up redoing most of the logic at both ends eventually. Or I can start with the trivial class and grow it naturally.
The key to all this though is that I don't write those classes myself! That's what Ruby is for. More specifically, you need to learn to love Struct
. Allow me to show you what I mean.
Imagine I need a basic class to represent a Contact
. Ruby gives us so many shortcuts that the class could be very small even without Struct
:
class Contact
def initialize(first, last, email)
@first = first
@last = last
@email = email
end
attr_accessor :first, :last, :email
end
You could shorten that up more with some multiple assignment if you like, but that's the basics. Now using Struct
is even easier:
Contact = Struct.new(:first, :last, :email)
To be fair, that's not 100% the same as the previous code. My original class required that three arguments get passed to the constructor whereas Struct
is more lenient:
p Contact.new(*%w[James Gray james@grayproductions.net])
# >> #<struct Contact first="James",
# last="Gray",
# email="james@grayproductions.net">
p Contact.new(*%w[James Gray])
# >> #<struct Contact first="James", last="Gray", email=nil>
p Contact.new("James")
# >> #<struct Contact first="James", last=nil, email=nil>
p Contact.new
# >> #<struct Contact first=nil, last=nil, email=nil>
As you can see, all arguments to the constructor Struct
builds for you are optional. It will just fill in the passed values, from left to right. This may or may not be an advantage for your needs.
Now lets look once more at how I built that Struct
:
ClassName = Struct.new(...)
Struct::new()
builds and returns Class
objects. If you then assign that to a constant, you can pretty much treat it like any other class you build. Ruby usually just handles the constant assignment for you.
You can forgo the assignment to a constant if you pass the first argument as a constant name in String
form:
Struct.new("Contact", :first, :last, :email)
This would not define a top-level Contact
, but instead a Struct::Contact
. Given that, your name must be unique among all Struct
s defined when using this approach.
Getting back to our fledgling Contact
, it's important to note that it does have all the getter and setter methods for the attributes:
c = Contact.new
p c
# >> #<struct Contact first=nil, last=nil, email=nil>
c.first = "James"
c.last = "Gray"
p c
# >> #<struct Contact first="James", last="Gray", email=nil>
p c.last
# >> "Gray"
Again, Struct
always defines both getter and setter for all attributes. That may or may not work for you.
You can also set values using a Hash
like syntax:
c[:email] = "james@grayproductions.net"
p c["email"]
# >> "james@grayproductions.net"
String
and Symbol
keys are interchangeable.
Another awesome feature is that Struct
gives you other ways to go through this data. Here are three of my personal favorites:
p c.members
# >> ["first", "last", "email"]
p c.values
# >> ["James", "Gray", "james@grayproductions.net"]
c.each_pair do |name, value|
puts "#{name}: #{value}"
end
# >> first: James
# >> last: Gray
# >> email: james@grayproductions.net
We get a lot of functionality for free. That's obvious. But eventually you are always going to want to add your own. In times past, the following was a common idiom for that:
class Contact < Struct.new(:first, :last, :email)
# ...
end
This is a neat example because it shows how flexible Ruby is. The parent Class
for a Class
definition doesn't have to be just a constant name. It can actually be any code that results in a Class
object. Then we can just inherit from it and add to it as we like.
However, projects like Rails have shown the error of this approach. Because Rails is often dynamically reloading code, Class
definitions will be rerun. That means the Struct
call will happen again, resulting in a fresh parent Class
object (which happens to have the exact same behavior). Ruby will see the new parent for an existing definition and choke with an error:
superclass mismatch for class Contact (TypeError)
The good news is that this isn't much of an issue because Struct
plans for it. It's prepared to accept a block during definition and the contents of that block will be evaluate within your new Class
. Thus it's trivial to add methods:
Contact = Struct.new(:first, :last, :email) do
def to_hash
Hash[*members.zip(values).flatten]
end
end
# ...
p c.to_hash
# >> {"last"=>"Gray",
# "first"=>"James",
# "email"=>"james@grayproductions.net"}
There is a gotcha when defining methods this way though. Struct
cheats on the internal implementation and doesn't actually place values in real instance variables. Thus, you will need to stick to accessing your data through the method interface:
Contact = Struct.new(:first, :last, :email) do
def full
"#{first} #{last}".strip
end
end
# ...
p c.full
# >> "James Gray"
If Struct
isn't dynamic enough for you, you may want to examine the standard OpenStruct
library. It's essentially a Hash
with a method interface, allowing you to change attributes as needed. You can also initialize it with a Hash
, if needed:
require "ostruct"
name = OpenStruct.new(:first => "James", :last => "Gray")
p name.last
# >> "Gray"
name.suffix = "II" # add an attribute
p name.suffix
# >> "II"
Sadly, OpenStruct
is missing most of the niceties of Struct
. Because of that, I don't feel it buys you much over a Hash
.
Keep Struct
in mind next time you need a simple data object. It's hardly any effort to setup, it comes fully loaded with options, and it can grow as your needs do.
Comments (8)
-
Adam Sanderson October 10th, 2008 Reply Link
Great post,
Struct
is incredibly handy. You have a typo in the first example though.@last
and@email
should not= first
;)I would suggest that people avoid
OpenStruct
in long running applications unless they really need the functionality. I found that it caused a rather severe memory leak in our Rails application. Switching to aStruct
orHash
fixed everything up.-
Fixed. Thanks.
-
-
Nice! One more trick up the sleeve.
Essentially though theConstant=Struct.new{}
gives us another idiom for defining classes - which will prove confusing for a few people. I love it!Once upon a time (we're talking the long ago of internet time here) I used
OpenStruct
a lot instead of mocks. It's still lounging deep in some of my configuration code.
Actually with the use of mocks I further reducedOpenStruct
to the cases where I want to retrieve something from the mocked instance (just a contrived example):m=OpenStruct.new m.expects(:something).returns("something_else") testable_target.does_something_with(m) assert_equals("expected",m.foo)
-
James,
Thanks for the post. I like the bit about creating a
Struct
with a block to define methods. In looking at the generated documentation at ruby-doc.org, I don't see this functionality.Is this an omission in the docs? How did you come across such a nice juicy tidbit?
Thank you.
-
It's mentioned in the reference portion of the Pickaxe.
-
Wow, so I just answered my own question reading the
ostruct
source. You can do this with any class. Learning something new is cool. Learning something cool and general is hot! (as Paris Hilton likes to say) ;-)Cheers and thanks for the great blog.
-
-
I find myself doing this quite a bit:
class Foo < Struct.new(:a, :b, :c) def initialize(*args) super extra_initialization end end
It's not the prettiest, but it gives you all that
Struct
goodness without the limitation of no initializer and without duplicated arguments. -
nice post. I happily found the information I was looking for—how to add a method. I then got the added bonus of the method I wanted to add being
to_hash
, as per your example :).