2

Is there a Ruby method for comparing two objects based on whether all of their instance variables are equal? The method would behave like this code.

class Coordinates
  attr_reader :x, :y
  def initialize(x, y)
    @x = x
    @y = y
  end
end

coordinates1 = Coordinates.new(0, 0)
coordinates2 = Coordinates.new(0, 0)
coordinates3 = Coordinates.new(1, 0)

compare(coordinates1, coordinates1) # => true
compare(coordinates1, coordinates2) # => true
compare(coordinates1, coordinates3) # => false

Does this method or something similar exist?

0

2 Answers 2

7

There is no built-in method for this, but you could quite easily write one. However, I think you're asking an XY question.

Here is what I think the question is supposed to say:

How should I define a method to check that two Coordinates instances are equal?

And here's my answer:

Define a custom == method:

class Coordinates
  attr_reader :x, :y
  def initialize(x, y)
    @x = x
    @y = y
  end

  def ==(other)
    return super unless other.is_a?(Coordinates)

    x == other.x && y == other.y
  end
end

...But in the spirit of StackOverflow, here's some meta-programming to check whether all instance variables have the same name and value:

# returns true if all objects have the same instance variable names and values
def compare(*objects)
  objects.map do |object|
    object.instance_variables.map do |var_name|
      [var_name, object.instance_variable_get(var_name)]
    end
  end.uniq.count == 1
end
Sign up to request clarification or add additional context in comments.

2 Comments

I think you should be overriding the === method, and not ==. See ruby-doc.org/core-2.7.0/BasicObject.html#method-i-3D-3D
@ReggieB I disagree. It is perfectly fine to override == in cases like this. From the documentation you linked, it says: "Typically, this method is overridden in descendant classes to provide class-specific meaning."
3

Case 1

class A
  def initialize(x,y)
    @x = x
    @y = y
  end
  def m
    @x = 5
    @y = 6
  end
end

a1 = A.new(1,2)
   #=> #<A:0x00005d22a3878048 @x=1, @y=2> 
a1.m
a1 #=> #<A:0x00005d22a3878048 @x=5, @y=6> 

a2 = A.new(3,4)
   #=> #<A:0x00005d22a38b5330 @x=3, @y=4> 
a2.m
a2 #=> #<A:0x00005d22a38b5330 @x=5, @y=6> 

Then,

a1.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) }
  #=> true

tells us that the values of @x and the values of @y are the same for both instances.

Case 2

Now let's change the code so that another instance variable is added conditionally.

class A
  def initialize(x,y)
    @x = x
    @y = y
  end
  def m
    @z = 3 if @x == 3
    @x = 5
    @y = 6
  end
end

a1 = A.new(1,2)
   #=> #<A:0x000057d1fd563c78 @x=1, @y=2> 
a1.m
a1 #=> #<A:0x000057d1fd27f200 @x=5, @y=6> 

a2 = A.new(3,4)
   #=> #<A:0x000057d1fd57cb38 @x=3, @y=4>
a2.m
a2 #=> #<A:0x000057d1fd2f9e10 @x=5, @y=6, @z=3> 

At this point are all instance variables of one of these instances equal to the corresponding instance variable of the other instance? No, because a2 has an additional instance variable, @z. Therefore,

a1.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) }
  #=> true

gives the wrong answer, for obvious reasons. Perhaps we could test as follows:

a1.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) } &&
a2.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) } 
 #=> true && false => false

This has a gotcha, however, if @z equals nil.

Case 3

class A
  def initialize(x,y)
    @x = x
    @y = y
  end
  def m
    @z = nil if @x == 3
    @x = 5
    @y = 6
  end
end

a1 = A.new(1,2)
   #=> #<A:0x000057d1fd2d18e8 @x=1, @y=2> 
a1.m
a1 #=> #<A:0x000057d1fd2d18e8 @x=5, @y=6> 

a2 = A.new(3,4)
  #=> #<A:0x000057d1fd46b460 @x=3, @y=4> 
a2.m
a2
  #=> #<A:0x000057d1fd46b460 @x=5, @y=6, @z=nil> 

a1.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) } &&
a2.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) }
  #=> true && true => true

We obtain this incorrect result because:

class A
end

A.new.instance_variable_get(:@z)
  #=> nil

We therefore must confirm that if one instance has an instance variable named e, so does the other instance, and that each pair of instance variables with the same name are equal. One way to do that is as follows:

(a1.instance_variables.sort == a2.instance_variables.sort) &&
a1.instance_variables.all? { |e|
  a1.instance_variable_get(e) == a2.instance_variable_get(e) }    
  #=> false && true => false

See Enumerable#all?, Object#instance_variables and Object#instance_variable_get.

5 Comments

If the instance variables have different names (e.g. If we're comparing two completely different classes) then is this method expected to return true or false? shrug
@Tom, doesn't the question's title and example make it cIear that the two instances being compared are from the same class?
I suppose so... But there's nothing in your method that assures this ;)
@Tom, can you elaborate? I thought it was straightforward. Are you looking to raise an exception within the method if a1.class != a2.class?
My point was that the requirements of the method are quite fuzzy (and it was almost certainly an XY question, I think!) -- there are a myriad of ways that the method could work. For example, is it invalid if you compare a class with a subclass? What happens if two instances of the same class have different instance variables? (Instance variables may be set dynamically in a method, not the initialiser!) "Having the same instance variables" is a very fuzzy/ill-defined check for equality, so there's no concrete answer to the problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.