This is the old RCRchive. It's available only for
reading and reference. To submit RCRs for Ruby 1.9/2.0, and to participate
in discussions about them, please visit
the new RCRchive.
RCR 293: Easy definition of key and sort attributes of a class
Submitted by Robert
(Fri Feb 18 07:02:01 UTC 2005)
Abstract
Often you want only a subset of the attributes of a Struct or other class be used for evaluation of equivalence and hash keys. Same applies for sort attributes. This RCR suggests to add two methods to Module that make this easier.
Problem
When creating a Struct all fields are used for sorting, comaprison and hashing while sometimes you just want a subset of these fields. Struct automatically includes all fields in comparison and hash while a user branded class does not automatically override #hash, #== and #eql?.
Proposal
Add two methods key_attributes and sort_attributes to class Module that generate #==, #eql? and #hash from key attributes and #<=> from sort attributes.
Analysis
The problem occurs with modest frequency and is general enough to be placed in the std lib. No imcompatibilities.
Implementation
class Module
def key_attributes(*fields)
code = ""
code << "def ==(o) " << fields.map {|f| "self.#{f} == o.#{f}" }.join(" && ") << " end\n"
code << "def eql?(o) " << fields.map {|f| "self.#{f}.eql?(o.#{f})" }.join(" && ") << " end\n"
code << "def hash() " << fields.map {|f| "self.#{f}.hash" }.join(" ^ ") << " end\n"
# puts code
class_eval code
fields
end
def sort_attributes(*fields)
code = fields.inject("def <=>(o)\n") {|s,f| s << "cmp = self.#{f} <=> o.#{f}; return cmp unless cmp == 0\n" } << "0\nend"
# puts code
class_eval code
fields
end
end
Foo = Struct.new(:name, :age, :info)
class Foo
key_attributes :name, :age
sort_attributes :name, :info, :age
end
f = [
Foo.new( "x", 1, "aaa" ),
Foo.new( "x", 1, "bbb" ),
Foo.new( "x", 2 , "aaa" ),
Foo.new( "y", 1 , "aaa"),
]
p f.sort
f.each do |f1|
f.each do |f2|
puts "#{f1.inspect} == #{f2.inspect} : #{f1==f2}"
puts "#{f1.inspect} eql? #{f2.inspect} : #{f1.eql? f2}"
puts "#{f1.inspect} equal? #{f2.inspect} : #{f1.equal? f2}"
end
puts
end

I think these are fair, but I maybe they are better just a requirable add-ons?
FYI, I've added these methods to Ruby Facets.
Thanks.
~Trans
It's funny, I thought about this exact thing and googled to see if anyone had implemented this or if it was already in the language and I just wasn't aware...and I stumble into the RCR.
Yes. I have to strongly advocate this. Particularly on the sorting issue. It's a lot cleaner than using the Comparable mixin and definining <=> for specifying sort criteria for a class.
Particularly with how often one works with an array of objects. (Which, yes...in Ruby is technically any time that you have an array period.)
I was looking at this again and thinking the implementation might be a perfect candidate for parameterized modules.
T.
Change the hashing to linear congruential random number generator (LCR), for example:
"def hash()\n h=1\n" << fields.map {|f| " h=((h * 31) ^ self.#{f}.hash) & 0xFFFF_FFFF" }.join("\n") << "\n h\nend\n"
(suggested by Hugh Sasse)
In the linear congruential version above, I now
think that we DO need % instead of & , and that
the number should be 0x7FFF_FFFF to fit in Fixnum
on most present-day systems (1 bit less than word size).
|
| Strongly opposed | 0 |
| Opposed | 0 |
| Neutral | 1 |
| In favor | 5 |
| Strongly advocate | 2 |
|


RCRchive copyright © David Alan Black, 2003-2005.
Powered by Ruby on Rails.
FYI, I've added these methods to Ruby Facets.
Thanks.
~Trans
It's funny, I thought about this exact thing and googled to see if anyone had implemented this or if it was already in the language and I just wasn't aware...and I stumble into the RCR.
Yes. I have to strongly advocate this. Particularly on the sorting issue. It's a lot cleaner than using the Comparable mixin and definining <=> for specifying sort criteria for a class.
Particularly with how often one works with an array of objects. (Which, yes...in Ruby is technically any time that you have an array period.)
I was looking at this again and thinking the implementation might be a perfect candidate for parameterized modules.
T.
Change the hashing to linear congruential random number generator (LCR), for example:
"def hash()\n h=1\n" << fields.map {|f| " h=((h * 31) ^ self.#{f}.hash) & 0xFFFF_FFFF" }.join("\n") << "\n h\nend\n"
(suggested by Hugh Sasse)
In the linear congruential version above, I now think that we DO need % instead of & , and that the number should be 0x7FFF_FFFF to fit in Fixnum on most present-day systems (1 bit less than word size).