How to make generic #== method?

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 #== :slight_smile:

TIA,

-- Zakaria
   z4k4ri4@bigfoot.com Yahoo!: z4k4ri4
   http://zakaria.is-a-geek.org
   http://pemula.linux.or.id

Zakaria wrote:

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 ?

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).

"Zakaria" <zakaria@suarametro.com> schrieb im Newsbeitrag
news:20040617211611.GA4734@zakbox.local...

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 #== :slight_smile:

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

=> #<Foo:0x10194988>

g=Foo.new

=> #<Foo:0x1018f918>

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 }

=> {#<Foo:0x10194988 @foo=nil>=>1}

h[g]

=> 1

Regards

    robert

Thank you Joel for the answer.
This is what I ended up with

···

On Fri, Jun 18, 2004 at 10:15:44AM +0900, Joel VanderWerf wrote:

Zakaria wrote:
>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 ?

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).

----------------------------------------------------------------------------
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 = {:@a => 1, :@b => 'ab', :@c => ['some', 4],
      :@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
   z4k4ri4@bigfoot.com Yahoo!: z4k4ri4
   http://zakaria.is-a-geek.org
   http://pemula.linux.or.id

Moin!

Joel VanderWerf wrote:

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! :slight_smile:

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

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

···

On Fri, Jun 18, 2004 at 10:15:44AM +0900, Joel VanderWerf wrote:

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

Zakaria wrote:

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

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

Gavin

···

On Saturday, June 19, 2004, 12:28:21 AM, Florian wrote:

Moin!

Joel VanderWerf wrote:

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! :slight_smile:

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

Zakaria wrote:

> 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.

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?

> 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.

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 :slight_smile:

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

Gavin

Wassallam,

-- Zakaria
   z4k4ri4@bigfoot.com Yahoo!: z4k4ri4
   http://zakaria.is-a-geek.org
   http://pemula.linux.or.id

···

On Fri, Jun 18, 2004 at 04:06:32PM +0900, Gavin Sinclair wrote:

> 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.

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

Yonder:

http://rubyforge.org/snippet/browse.php?by=lang&lang=17

Yours,

Tom

···

On Fri, 2004-06-18 at 12:16, Zakaria wrote:

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

URL? I know I'm lazy :slight_smile:

Zakaria wrote:
....

URL? I know I'm lazy :slight_smile:

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
---

/Christoph

Lennon Day-Reynolds <rcoder@gmail.com> writes:

> 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.

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.