How to make generic #== method?

Discussion in 'Ruby' started by Zakaria, Jun 17, 2004.

  1. Zakaria

    Zakaria Guest

    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
    Yahoo!: z4k4ri4
    http://zakaria.is-a-geek.org
    http://pemula.linux.or.id
    Zakaria, Jun 17, 2004
    #1
    1. Advertising

  2. 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).
    Joel VanderWerf, Jun 18, 2004
    #2
    1. Advertising

  3. Zakaria

    Zakaria Guest

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


    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
    Yahoo!: z4k4ri4
    http://zakaria.is-a-geek.org
    http://pemula.linux.or.id
    Zakaria, Jun 18, 2004
    #3
  4. 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
    Gavin Sinclair, Jun 18, 2004
    #4
  5. "Zakaria" <> schrieb im Newsbeitrag
    news:...
    > 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

    => #<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
    Robert Klemme, Jun 18, 2004
    #5
  6. 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! :)

    > 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
    Florian Gross, Jun 18, 2004
    #6
  7. Zakaria

    Paul Brannan Guest

    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


    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
    Paul Brannan, Jun 18, 2004
    #7
  8. 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! :)


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


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

    Gavin
    Gavin Sinclair, Jun 18, 2004
    #8
  9. Zakaria

    Zakaria Guest

    On Fri, Jun 18, 2004 at 04:06:32PM +0900, Gavin Sinclair wrote:
    > 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 :)

    > 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
    Yahoo!: z4k4ri4
    http://zakaria.is-a-geek.org
    http://pemula.linux.or.id
    Zakaria, Jun 18, 2004
    #9
  10. Zakaria

    Tom Copeland Guest

    Tom Copeland, Jun 18, 2004
    #10
  11. > > 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
    Lennon Day-Reynolds, Jun 18, 2004
    #11
  12. Zakaria

    Christoph Guest

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

    /Christoph
    Christoph, Jun 18, 2004
    #12
  13. Zakaria

    George Ogata Guest

    Lennon Day-Reynolds <> 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.
    George Ogata, Jun 19, 2004
    #13
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Murat Tasan
    Replies:
    1
    Views:
    8,030
    Chaitanya
    Feb 3, 2009
  2. Replies:
    2
    Views:
    426
  3. jtl.zheng
    Replies:
    5
    Views:
    362
    jtl.zheng
    Jul 25, 2006
  4. minlearn
    Replies:
    2
    Views:
    445
    red floyd
    Mar 13, 2009
  5. Pen Ttt
    Replies:
    4
    Views:
    138
    Pen Ttt
    Sep 24, 2010
Loading...

Share This Page