How to make generic #== method?

Z

Zakaria

Hi,

Currently I got many simple class which is just a simple record-like.
An example
-----------------------------------
class LoopTag # :nodoc:
attr_reader :param
attr_reader :contents

def initialize(param)
@param, @contents = param, []
end

def ==(other)
return false unless other.class == self.class
return other.param == @param && other.contents == @contents
end
end
------------------------------------
and I got a couple more class like this.

Is there a way to make a generic #== or some Module that I can include ?

I surely need it to do assert_equal on unit test.
You know, I just waste a couple hour tracking bug that doesn't exists
because I forgot to implement #== :)

TIA,


-- Zakaria
(e-mail address removed) Yahoo!: z4k4ri4
http://zakaria.is-a-geek.org
http://pemula.linux.or.id
 
J

Joel VanderWerf

Zakaria said:
Hi,

Currently I got many simple class which is just a simple record-like.
An example
-----------------------------------
class LoopTag # :nodoc:
attr_reader :param
attr_reader :contents

def initialize(param)
@param, @contents = param, []
end

def ==(other)
return false unless other.class == self.class
return other.param == @param && other.contents == @contents
end
end

One way I've done it is this:

module ContentEquality
def hash
content.hash
end

def eql?(other)
content.eql? other.content
end

def ==(other)
# self.class == other.class and # optional
content == other.content
end
end

Just include the module and define a content method that returns
something like and array of objects that are significant for comparison
purposes:

class LoopTag
include ContentEquality

def content
[@param, @contents]
end
end

Another way might be to iterate over a list of instance variables (or
all of 'em, if that's what you want).
 
Z

Zakaria

Zakaria said:
Hi,

Currently I got many simple class which is just a simple record-like.
An example
-----------------------------------
class LoopTag # :nodoc:
attr_reader :param
attr_reader :contents

def initialize(param)
@param, @contents = param, []
end

def ==(other)
return false unless other.class == self.class
return other.param == @param && other.contents == @contents
end
end
One way I've done it is this:
module ContentEquality
def hash
content.hash
end
def eql?(other)
content.eql? other.content
end
def ==(other)
# self.class == other.class and # optional
content == other.content
end
end
Just include the module and define a content method that returns
something like and array of objects that are significant for comparison
purposes:
class LoopTag
include ContentEquality
def content
[@param, @contents]
end
end
Another way might be to iterate over a list of instance variables (or
all of 'em, if that's what you want).

Thank you Joel for the answer.
This is what I ended up with
----------------------------------------------------------------------------
module AttrsEquality
def ==(other)
self.class === other && attrs == other.attrs
end

def attrs
res = {}
instance_variables.each {|n| res[n.intern] = instance_variable_get(n) }
res
end
end
----------------------------------------------------------------------------

with unit-test
----------------------------------------------------------------------------
class TC_AttrsEquality < Test::Unit::TestCase
class T1
include AttrsEquality
def initialize(a, b, c, d)
@a, @b, @d, @c = a, b, d, c
end
end

def test_attrs
t = T1.new(1, 'ab', ['some', 4], {'x' => 1, 3 => 'y'})
x = {:mad:a => 1, :mad:b => 'ab', :mad:c => ['some', 4],
:mad:d => {'x' => 1, 3 => 'y'}}
assert_equal(x, t.attrs)
end

def test_equal
t1 = T1.new(1, 'ab', ['some', 4], {'x' => 1, 3 => 'y'})
t2 = T1.new(1, 'ab', ['some', 4], {'x' => 1, 3 => 'y'})
assert(t1 == t2, 'equal')
end
end
----------------------------------------------------------------------------

I remove the eql? and hash method because
1) I don't need it
2) It doesn't work if the class has hash attribute because
{'a' => 1}.hash != {'a' => 1}.hash
3) I'm not really understand the functionality to test it

Could someone enlighten me why Hash#hash doesn't result the same
and where .hash and .eql? used ?

PS: Is there any place where I could post this snippet,
so others could use it?

Wassallam,


-- Zakaria
(e-mail address removed) Yahoo!: z4k4ri4
http://zakaria.is-a-geek.org
http://pemula.linux.or.id
 
G

Gavin Sinclair

Zakaria said:
I remove the eql? and hash method because
1) I don't need it

You probably need hash, and should define it (see below). About eql?, I'm
under the understanding that eql? and == should always return the same
thing.
2) It doesn't work if the class has hash attribute because
{'a' => 1}.hash != {'a' => 1}.hash

Now that is weird. I've a faint recollection of it being mentioned on
ruby-talk before though, so perhaps its not a bug.
3) I'm not really understand the functionality to test it

Could someone enlighten me why Hash#hash doesn't result the same
and where .hash and .eql? used ?

#hash is used to generate a hash code, which is used to put an object into
a hash (i.e. a Hash object). If two objects have the same #hash value,
then a Hash thinks they're probably the same, but it's not guaranteed, so
it double-checks with #eql?. However, if two objects have *different*
#hash values, then a Hash considers them to be definitely different.

Bottom line, if you (re)define #== in an object, you really really should
(re)define #hash as well, to make sure they are consistent.
PS: Is there any place where I could post this snippet,
so others could use it?

As it happens, I've already written a detailed consideration of Joel's
code for the not-yet-formally-announced project 'addlib', so that people
can reuse it without having to manage the code themselves.

Until then, however, you might take a look at the snippet repository on
RubyForge.

Joel's code is better, however. What yours adds is the implicit
definition of #contents (in Joel's terminology). So why not:

module AttrsEquality
include ContentEquality
def contents
res = {}
instance_variables.each {|n| res[n.intern] = instance_variable_get(n) }
res
end
end

Gavin
 
R

Robert Klemme

Zakaria said:
Hi,

Currently I got many simple class which is just a simple record-like.
An example
-----------------------------------
class LoopTag # :nodoc:
attr_reader :param
attr_reader :contents

def initialize(param)
@param, @contents = param, []
end

def ==(other)
return false unless other.class == self.class
return other.param == @param && other.contents == @contents
end
end
------------------------------------
and I got a couple more class like this.

Is there a way to make a generic #== or some Module that I can include ?

I surely need it to do assert_equal on unit test.
You know, I just waste a couple hour tracking bug that doesn't exists
because I forgot to implement #== :)

module MemberEquivalence
def ==(o)
instance_variables.each do |var|
return false unless instance_variable_get(var) ==
o.instance_variable_get(var)
end

true
end

def hash
h = 0

instance_variables.each do |var|
val = instance_variable_get(var)
h ^= val.hash unless val.nil?
end

h
end

alias :eql? :==
end

class Foo
include MemberEquivalence

attr_accessor :foo, :bar
end

f=Foo.new
=> # said:
g=Foo.new
=> # said:
f == g => true
f.foo = "x" => "x"
f == g => false
f.foo = nil => nil
f == g => true
f.hash => 0
g.hash => 0
f.eql? g => true
h={ f => 1 }
=> {# said:
=> 1

Regards

robert
 
F

Florian Gross

Moin!

Joel said:
module ContentEquality
def hash
content.hash
end

def eql?(other)
content.eql? other.content
end

def ==(other)
# self.class == other.class and # optional
content == other.content
end
end

That's cool, I think I'm going to use it in the future! :)
Another way might be to iterate over a list of instance variables (or
all of 'em, if that's what you want).

Hm, maybe a default .content could be provided by the module; it could
look like this:

module ContentEquality
def content
instance_variables.map { |name| instance_variable_get(name) }
end
end

Regards,
Florian Gross
 
P

Paul Brannan

One way I've done it is this:

module ContentEquality
def hash
content.hash
end

def eql?(other)
content.eql? other.content
end

def ==(other)
# self.class == other.class and # optional
content == other.content
end
end

I like this, and since content is ordered, it's possible to write this
too:

module ContentEquality
def <=>(other)
content.each_with_index do |member, idx|
result = member <=> other.content[idx]
return result if result != 0
end
return 0
end

include Comparable
end

The only thing I don't like is that content doesn't carry any
information about what it holds, so it's entirely possible to
inadvertently compare apples and oranges.

Paul
 
G

Gavin Sinclair

Joel VanderWerf wrote:
That's cool, I think I'm going to use it in the future! :)
Hm, maybe a default .content could be provided by the module; it could
look like this:
module ContentEquality
def content
instance_variables.map { |name| instance_variable_get(name) }
end
end

Because this discards the instance variable names, some pathological
cases would give false positives. Better to make #content return a
hash instead.

Gavin
 
Z

Zakaria

Zakaria wrote:
Now that is weird. I've a faint recollection of it being mentioned on
ruby-talk before though, so perhaps its not a bug.

Could someone explain why?
#hash is used to generate a hash code, which is used to put an object into
a hash (i.e. a Hash object). If two objects have the same #hash value,
then a Hash thinks they're probably the same, but it's not guaranteed, so
it double-checks with #eql?. However, if two objects have *different*
#hash values, then a Hash considers them to be definitely different.
Bottom line, if you (re)define #== in an object, you really really should
(re)define #hash as well, to make sure they are consistent.

They only needed if I put the object as the hash key right?
As it happens, I've already written a detailed consideration of Joel's
code for the not-yet-formally-announced project 'addlib', so that people
can reuse it without having to manage the code themselves.

I hope I could contribute.
Until then, however, you might take a look at the snippet repository on
RubyForge.

URL? I know I'm lazy :)
Joel's code is better, however. What yours adds is the implicit
definition of #contents (in Joel's terminology). So why not:

I think Robert Klemme version is better and I believe it's faster.
And I also like the name, MemberEquivalence.
OK going to sleep now

Wassallam,


-- Zakaria
(e-mail address removed) Yahoo!: z4k4ri4
http://zakaria.is-a-geek.org
http://pemula.linux.or.id
 
L

Lennon Day-Reynolds

2) It doesn't work if the class has hash attribute because
Now that is weird. I've a faint recollection of it being mentioned on
ruby-talk before though, so perhaps its not a bug.

This makes sense, as both hashes are mutable. Their contents may be
the same, but they shouldn't be mistaken for the same object, as
either one's value could change at any time.

Consider the following:

---
h = Hash.new
g = Hash.new

hh = h.hash
gh = g.hash

puts "Hashes for 'h' and 'g' are " + (hh == gh ? "equal" : "not equal")

h[1] = true
g[1] = false

puts "Hash for 'h' has " + (hh == h.hash && "not ") + "changed"
puts "Hash for 'g' has " + (gh == g.hash && "not ") + "changed"
---

If you run the code above, the output should be:

---
Hashes for 'h' and 'g' are not equal
Hash for 'h' has not changed
Hash for 'g' has not changed
---

Using an object as a hash key, you want to check identity, not
equality. That way, even if your object is mutable, and you use it as
a key later, it will bee associated with the same value (assuming your
mapping hasn't changed, of course).

Lennon
 
C

Christoph

Zakaria wrote:
....
URL? I know I'm lazy :)

Also checkout [ruby-talk :48114] (+ google) and
http://www.rubygarden.org/ruby?CompareByValue.

Personally I would tend to implement such a
class as a subclass of a Struct class ...

class LoopTag < Struct.new:)param,:contents)
def initialize(param)
super(param,[])
end
...
end


Just for the heck of it here is a cleaned up version
of the same problem I wrote two years ago, which is
sort (of) recursion and thread safe. There is an
obvious similarity to the idea of Joel content method
returning a Hash.


---
module CompareByValue
def ==(other)
if not instance_of?(other.class)
false
elsif equal?(other)
true
elsif id < other.id
CompareByValue.comp?(self,other)
else
CompareByValue.comp?(other,self)
end
end
end

class << CompareByValue
class Seen < Hash
Pair = Struct.new:)l,:r)

def initialize(l,r)
store(Pair.new(l.id,r.id),true)
end

def seen?(l,r)
self[Pair.new(l.id,r.id)]
end

def default(key)
store(key,true)
nil
end

def remove(l,r)
delete Pair.new(l.id,r.id)
end
end

# symbol mangling
SEEN_COMPS = "CBV#{id}_comps".intern
NUMS_CALLS = "CBV#{id}_calls".intern

def comp?(l,r)
return true if thread_local_seen?(l,r)
begin
Thread.current[NUMS_CALLS] +=1
return Comp.new.comp?(l,r)
rescue
Thread.current[SEEN_COMPS].remove(l,r)
raise
ensure
if (Thread.current[NUMS_CALLS] -= 1).zero?
Thread.current[SEEN_COMPS].clear
end
end
end

def thread_local_seen?(l,r)
unless comps = Thread.current[SEEN_COMPS]
Thread.current[NUMS_CALLS] = 0
Thread.current[SEEN_COMPS] = Seen.new(l,r)
false
else
comps.seen?(l,r)
end
end

class Comp < Hash
def comp?(l,r)
return true if l.equal?(r)
vars = l.instance_variables.sort!
return false unless vars == r.instance_variables.sort!

store(l.id,r.id)
vars.each do |name|
ll = l.instance_eval(name)
rr = r.instance_eval(name)
if CompareByValue === ll
return false unless ll.instance_of?(rr.class)
if rr_seen_id = self[ll.id]
return false unless rr_seen_id == rr.id
else
return false unless comp?(ll,rr)
end
else
return false unless ll == rr
end
end
return true
end
end
end
 
G

George Ogata

Lennon Day-Reynolds said:
This makes sense, as both hashes are mutable. Their contents may be
the same, but they shouldn't be mistaken for the same object, as
either one's value could change at any time.

So are Arrays and Strings (and plently else), but they can be hashed.
That's what the #rehash method is for, isn't it?

One possible reason I can think of is that the default_procs should
probably be compared too, but we can't compare procs in general.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,767
Messages
2,569,570
Members
45,045
Latest member
DRCM

Latest Threads

Top