I am sitting in the airport right now, waiting for my twice delayed flight to board. The security man took my Scope. He does not want my teeth to be clean or my breath to be fresh. So be it.
Here’re two classes you may or may not have seen before. Unlike, say, a small bottle of refreshing mouth wash, you can definitely take these guys on an airplane.
Struct
Struct is like Ruby on Rails for classes. Scaffolding. It lets you quickly cobble together a new class with some gettable-settable attributes. You can then instantiate your struct’d class and go nuts. You don’t even need to require anything—Struct is right there, ready and willing, all the time.
When I was little we had a black lab named Tucker. He was a dog with four legs and two eyeballs.
>> Animal = Struct.new(:name, :legs, :eyeballs) => Animal >> tucker = Animal.new('dog', 4, 2) => #<struct Animal name="dog", legs=4, eyeballs=2> >> tucker.legs => 4 >> tucker.legs = 3 => 3 >> tucker.legs => 3
Wow, that’s quick. We didn’t have to set up a class definition or fumble with attr_accessor. But there’s more.
>> tucker[0] => "dog" >> tucker['name'] => "dog" >> tucker[:name] => "dog" >> tucker[:name] = 'doggy' => "doggy" >> tucker.name => "doggy"
It’s like Array and Hash got married and had a little classling. Okay, that’s not true, but it sure looks that way from the outside. I wonder if that means Struct objects are enumerable?
>> tucker.is_a? Enumerable => true >> tucker.methods.grep /each/ => ["each_with_index", "each", "each_pair"]
How unexpected. And it’s ordered, too—the order you passed your keys when creating your struct’d class is maintained. In this case the order is name, legs, then eyeballs. Always.
>> tucker.each_pair { |k,v| puts "key: #{k}, value: #{v}" } key: name, value: dog key: legs, value: 4 key: eyeballs, value: 2 => #<struct Animal name="dog", legs=4, eyeballs=2> >> tucker.each { |v| puts v } dog 4 2 => #<struct Animal name="dog", legs=4, eyeballs=2>
For reference:
>> (tucker.methods & Enumerable.instance_methods).sort => ["all?", "any?", "collect", "detect", "each_with_index", "entries", "find", "find_all", "grep", "include?", "inject", "map", "max", "member?", "min", "partition", "reject", "select", "sort", "sort_by", "to_a", "zip"]
Since a struct’d class is not actually a Hash or an Array, there are differences. A Struct instantiated object has no keys—it has members.
>> tucker.members => ["name", "legs", "eyeballs"] >> tucker.values => ["doggy", 4, 2]
Also: you can not add new members arbitrarily as you can with an Array or a Hash. However many members you instantiate your struct’d class with is however many you’re stuck with. As a result: no merge, update, or concat (+). This is a class, after all. Not a fancy faux-primitive.
If you need to turn a Struct object into a Hash, here’s an easy way:
>> hash = Hash[*tucker.members.zip(tucker.values).flatten] => {"name"=>"doggy", "eyeballs"=>2, "legs"=>4}
Maybe wrap that in a to_hash if you’d like. For to_a, just use values.
It’s worth noting that since everything in Ruby is an object, we can inherit straight from a call to Struct.new. Maybe we want to add some questions?
class Primate < Struct.new(:name, :legs, :eyeballs) def legs? (legs && legs.to_i > 0) ? true : false end end
Imagine the possibilities. Fake-out ActiveRecord objects in your tests! Mock hashes that are super indifferent! Create resources which can only be described as active! Amaze your friends!
OpenStruct
OpenStruct is a bit more liberal than Struct, with less structure. Like that hippie cousin everyone has. (in my family it’s me)
Usage: pass a hash to OpenStruct.new, receive an object with full-on getters and setters. Because this behavior is a bit suspect we must explicitly require OpenStruct (ostruct) from the standard library. Like YAML.
>> require 'ostruct' => true >> scope = OpenStruct.new(:brand => 'Scope', :owner => 'P&G', :flavor => 'Cinnamon Ice') => #<OpenStruct brand="Scope", owner="P&G", flavor="Cinnamon Ice"> >> scope.brand => "Scope" >> scope.brand = 'Scoped' => "Scoped" >> scope.brand => "Scoped"
Unlike Struct, we can add new members to an OpenStruct object whenever we’d like.
>> scope.tasty => nil >> scope.tasty = true => true >> scope.tasty => true
This is accomplished through some method_missing-like magic. Unfortunately, this means all your NoMethodError exceptions go away.
>> scope.something_fake => nil
Not the biggest deal. That’s how hashes work when you try to access a non-existent key, after all. And there’s always good ol’ respond_to:
>> scope.respond_to? :something_fake => false
So that’s all pretty cool. I’ve been playing with this class in my code lately. Mainly in tests to fake out ActiveRecord stuffs, as I suggested with Struct. There is one major disadvantage to OpenStruct: it’s not enumerable. Less structure, see. You can not each, you can not members, you can’t even values. Well. I mean. Out of the box you can’t.
Enum-o-Struct
What we’re going to do is make OpenStruct enumerable by adding the each method, the members method, and including the Enumerable module.
require 'ostruct' class EnumoStruct < OpenStruct include Enumerable def members methods(false).grep(/=/).map { |m| m[0...-1] } end def each members.each do |method| yield send(method) end self end end
Cryptic? Hardly.
We want an each method. How are we going to enumerate over an OpenStruct? Well, we could always just pass the return value of each getter-method to whatever block is passed to our each method. If that’s our approach, it looks like we need a way to find all the members. All the getters.
While an OpenStruct object may have a ton of arbitrary methods, it has setters which end in an equal sign and almost always correspond to a getter. The members method here finds all methods ending in = on an object by grabbing an array of methods excluding those defined on parent/ancestor classes (methods(false)) and creating an array of methods which match a regular expression, using grep. It then strips the last character, the =, from each found method, returning our array of members.
The members from our scope example: [“flavor”, “owner”, “brand”]
The Enumerable module depends on your class implementing each to work its magic. The concept is simple: go through each of the member methods and call the method, passing its value into the block passed to each using yield. In this case, send(method) is the same as, for example, self.flavor or self.brand.
Notice we return self in our each method: it’s polite for each to return self so that calls to it may be chained.
In action:
>> require 'enumostruct' => true >> scope = EnumoStruct.new(:brand => 'Scope', :owner => 'P&G', :flavor => 'Cinnamon Ice') => #<EnumoStruct brand="Scope", owner="P&G", flavor="Cinnamon Ice"> >> scope.each { |v| puts "value: #{v}" } value: Cinnamon Ice value: P&G value: Scope => #<EnumoStruct brand="Scope", owner="P&G", flavor="Cinnamon Ice">
Oh that’s delicious. Remember, now we have all the Enumerable methods.
>> scope.map { |v| v } => ["Cinnamon Ice", "P&G", "Scope"] >> scope.detect { |v| v =~ /&/ } => "P&G"
Hey, I liked that each_pair method that Struct gives you. Let’s add it before we go.
class EnumoStruct < OpenStruct def each_pair members.each do |method| yield method, send(method) end self end end >> scope.each_pair { |k,v| puts "key: #{k}, value: #{v}" } key: flavor, value: Cinnamon Ice key: owner, value: P&G key: brand, value: Scope => #<EnumoStruct brand="Scope", owner="P&G", flavor="Cinnamon Ice">
Same as each, really. The only difference is we pass the name of the current member into the block as the first argument. Cake.
Finally, let’s be really cheap. Let’s add [] and []= to our Enum-o-Struct. Are you in?
class EnumoStruct < OpenStruct def [](member) send(member) end def []=(member, value) send("#{member}=", value) end end >> scope['owner'] => "P&G" >> scope[:website] = 'http://www.getclose.com' => "http://www.getclose.com" >> scope.website => "http://www.getclose.com" >> scope[:negative] => nil
Now don’t go too crazy, okay? Always monkeypytch with caution.
Bye!
Ah, we’ve accomplished so much. My only hope is you can truly find a use for one of these humble classes. They are not flammable and pose no threat to your fellow passengers.
And I bet your Tucker would have done better on Dancing With The Stars.
Great article.
I see the fruits of our experimentation :)
I like your blog. So many good articles. Here I find things I don’t knew -> Struct or OpenStruct.
Thank you.
Hi,
cool, I knew both before, but adding methods to them never occured to me…
Anyway, I’d expect members to return something here:
Any ideas why it doesn’t?
Stephan: Use o = EnumoStruct.new. You’re defining a new class and then instantiating the old one – you need to use the EnumoStruct class you just created.
OpenStruct is cool, but watch for gotchas with freeze and using attribute names that are already defined methods.
Oh dear, apparently I’m too used to opening existing classes already…
I had problems getting the members-thing to work:
And then I discovered marshal_dump:
That does it for me.
how about this instead
class OpenStruct def table; @table; end end
so…
o = OpenStruct.new o.a = 1 o.b = 2 o.table.each_pair do etc…
Hmm, if I place all those methods in one class the two [] methods return nil which makes me think where the enumerable is included makes a big difference. Has any one got all these methods to work in one class?
Cheers
Chime in.