Search code examples
arraysrubyunion

Ruby #union on Array reacts very strangely


I like to use the Array#union or #| method to return an array of objects where duplicates are removed. I have a custom implementation of eql? on my class. There is a really strange behaviour if I do that. Up to 8 elements the union is just working fine but with more elements the duplicates are not removed. Is that actually a bug of ruby or do I miss something?

class A
  attr_accessor :name

  def initialize(name)
    self.name = name
  end

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

as = names.map { |name| A.new(name) }
bs = names.map { |name| A.new(name) }

as | bs
=>
[#<A:0x00007fe503692388 @name="a">,
 #<A:0x00007fe503692310 @name="b">,
 #<A:0x00007fe5036922e8 @name="c">,
 #<A:0x00007fe5036922c0 @name="d">,
 #<A:0x00007fe503692298 @name="e">,
 #<A:0x00007fe503692270 @name="f">,
 #<A:0x00007fe503692248 @name="g">,
 #<A:0x00007fe503692220 @name="h">,
 #<A:0x00007fe5036921f8 @name="i">,
 #<A:0x00007fe5036921d0 @name="j">,
 #<A:0x00007fe5036921a8 @name="k">,
 #<A:0x00007fe503692180 @name="l">,
 #<A:0x00007fe5035732e0 @name="a">,
 #<A:0x00007fe5035732b8 @name="b">,
 #<A:0x00007fe503573290 @name="c">,
 #<A:0x00007fe503573268 @name="d">,
 #<A:0x00007fe503573240 @name="e">,
 #<A:0x00007fe503573218 @name="f">,
 #<A:0x00007fe5035731f0 @name="g">,
 #<A:0x00007fe5035731c8 @name="h">,
 #<A:0x00007fe5035731a0 @name="i">,
 #<A:0x00007fe503573178 @name="j">,
 #<A:0x00007fe503573150 @name="k">,
 #<A:0x00007fe503573128 @name="l">]

as[0..7] | bs[0..7]
=>
[#<A:0x00007fe503692388 @name="a">,
 #<A:0x00007fe503692310 @name="b">,
 #<A:0x00007fe5036922e8 @name="c">,
 #<A:0x00007fe5036922c0 @name="d">,
 #<A:0x00007fe503692298 @name="e">,
 #<A:0x00007fe503692270 @name="f">,
 #<A:0x00007fe503692248 @name="g">,
 #<A:0x00007fe503692220 @name="h">] 

Solution

  • You need to implement both eql? and hash methods with accordance to @engineersmnky's comment regarding Object#eql?, that eql? expects the instance hashes to be the same

    class A
      attr_accessor :name
    
      def initialize(name)
        @name = name
      end
    
      def eql?(other)
        other.name.eql?(@name)
      end
    
      def hash
        [@name].hash
      end
    end
    
    #declare inclusive range from 'a' to 'm'
    
    names = 'a'..'m'
    
    as = names.map { |name| A.new(name) }
    bs = names.map { |name| A.new(name) }
    
    as | bs
    
    
    
    

    Outputs:

    [#<A:0x0000558a74860020 @name="a">,
     #<A:0x0000558a7485bf98 @name="b">,
     #<A:0x0000558a7485bf70 @name="c">,
     #<A:0x0000558a7485bf48 @name="d">,
     #<A:0x0000558a7485bf20 @name="e">,
     #<A:0x0000558a7485bef8 @name="f">,
     #<A:0x0000558a7485bed0 @name="g">,
     #<A:0x0000558a7485bea8 @name="h">,
     #<A:0x0000558a7485bd90 @name="i">,
     #<A:0x0000558a7485bd40 @name="j">,
     #<A:0x0000558a7485bd18 @name="k">,
     #<A:0x0000558a7485bcf0 @name="l">,
     #<A:0x0000558a7485bcc8 @name="m">]