Ruby set with custom class - ruby

I'm trying to correctly implement some custom classes in order to use them in a set. Custom class B contains an array instance variable that holds objects of class A. Here's a MWE:
#!/usr/bin/env ruby
require 'set'
class A
attr_reader :a
def initialize(a)
#a = a
end
def hash
#a.hash
end
def eql?(other)
#a == other.a
end
end
class B
attr_reader :instances
def initialize
#instances = Array.new
end
def add(i)
#instances.push(i)
end
def hash
#instances.hash
end
def eql?(other)
#instances == other.instances
##instances.eql?(other.instances)
end
end
s = Set.new
b1 = B.new
b1.add(A.new(4))
b1.add(A.new(5))
b2 = B.new
b2.add(A.new(4))
b2.add(A.new(5))
s.add(b1)
s.add(b1)
s.add(b2)
puts s.size
The output is 2, expected is 1 since b1 and b2 are objects constructed with the same values.
If I use eql? instead of == in the implementation of eql? in class B, then the output is correct. According to the definition of == in the ruby docs, shouldn't the use of == be correct here? Where is my error in understanding this?

TL;DR it's because:
Array#== compares elements via ==
Array#eql? compares elements via eql?
According to the definition of == in the ruby docs, shouldn't the use of == be correct here?
If you compare the arrays via Array#== the corresponding items will also be compared via ==: (emphasis added)
Two arrays are equal if they contain the same number of elements and if each element is equal to (according to Object#==)
Example:
[A.new(4)] == [A.new(4)]
#=> false
it fails because the elements are not equal according to ==:
A.new(4) == A.new(4)
#=> false
If I use eql? instead of == in the implementation of eql? in class B, then the output is correct.
Array#eql? works, because it compares corresponding items via eql?: (emphasis added)
Returns true if self and other are the same object, or are both arrays with the same content (according to Object#eql?).
Example:
[A.new(4)].eql? [A.new(4)]
#=> true
because of:
A.new(4).eql? A.new(4)
#=> true
In order to get == working, you have to implement A#==:
class A
# ...
def eql?(other)
#a == other.a
end
alias_method :==, :eql?
end
A.new(4) == A.new(4) #=> true
[A.new(4)] == [A.new(4)] #=> true

Related

Is self not equal to the instance variable?

Learning Ruby here and this is my first endeavor into OOP and below is my complete code which makes a hash class. I'm having trouble understanding what is happening behind the scenes in the union method. When I change self.to_a.each { |key| joined_set.insert(key) } to #store.to_a.each { |key| joined_set.insert(key) } the hash joined_set becomes an array of arrays containing the keys and values of #store while it just contains the keys if I use just self and not #store. How does this discrepancy arise? Is self not equal to the instance variable?
class MyHashSet
def initialize
#store = {}
end
def insert(el)
#store[el] = true
end
def include?(el)
return true if #store[el]
false
end
def delete(el)
if #store[el]
#store.delete(el)
return true
else
return false
end
end
def to_a
#store.keys
end
def union(set2)
joined_set = self.class.new
self.to_a.each { |key| joined_set.insert(key) }
set2.to_a.each { |key| joined_set.insert(key) }
joined_set
end
end
The more specific reason you're getting different results is that self.to_a is equal to #store.keys. Why? because that's how you defined to_a:
def to_a
#store.keys
end
#store.keys and #store.to_a are very different from each other; #store is a ruby Hash, and Hash#to_a returns an array of arrays, with each subarray being a key-value pair, like [[key1, value1], [key2, value2]]; Hash#keys, on the other hand, just returns an array of keys.
self is not equal to the instance variable.
self is equal to the current object, which, in this case, would be the current instance of the MyHashSet class.
so #store is an attribute of self in this case.
if you had an attr_accessor for #store, then #store would be equal to self.store

Can I refer to an object's attribute by string in Ruby?

If I have a ruby class called Node:
class Node
attr_accessor :a, :b, :c
attr_reader :key
def initialize(key)
#key = key
end
def [](letter)
if letter == 'a'
return self.a
elsif letter == 'b'
return self.b
elsif letter == 'c'
return self.c
end
end
end
How can I optimize def [](letter) so I won't have repetitive code? More specifically, how can I access an attribute of an object (that is a ruby symbol :a, :b, or :c) by using a corresponding string?
You can use send, which invokes a method dynamically on the caller, in this case self:
class Node
def [](key)
key = key.to_sym
send(key) if respond_to?(key)
end
end
Note that we check that self has the appropriate method before calling it. This avoids getting a NoMethodError and instead results in node_instance[:banana] returning nil, which is appropriate given the interface.
By the way, if this is the majority of the behavior of your Node class, you may simply want to use an OpenStruct:
require 'ostruct'
node_instance = OpenStruct.new(a: 'Apple', b: 'Banana')
node_instance.a #=> 'Apple'
node_instance['b'] #=> 'Banana'
node_instance.c = 'Chocolate'
node_instance[:c] #=> 'Chocolate'

How to remove duplicates from array with custom objects

When I call first_array | second_array on two arrays that contain custom objects:
first_array = [co1, co2, co3]
second_array =[co2, co3, co4]
it returns [co1, co2, co3, co2, co3, co4]. It doesn't remove the duplicates. I tried to call uniq on the result, but it didn't work either. What should I do?
Update:
This is the custom object:
class Task
attr_accessor :status, :description, :priority, :tags
def initiate_task task_line
#status = task_line.split("|")[0]
#description = task_line.split("|")[1]
#priority = task_line.split("|")[2]
#tags = task_line.split("|")[3].split(",")
return self
end
def <=>(another_task)
stat_comp = (#status == another_task.status)
desc_comp = (#description == another_task.description)
prio_comp = (#priority == another_task.priority)
tags_comp = (#tags == another_task.tags)
if(stat_comp&desc_comp&prio_comp&tags_comp) then return 0 end
end
end
and when I create few instances of Task type and drop them into two different arrays and when I try to call '|' on them nothing happens it just returns array including both first and second array's elements without the duplicates removed.
No programming language for itself can be aware if two objects are different if you don't implement the correct equality methods.
In the case of ruby you need to implement eql? and hash in your class definition, as these are the methods that the Array class uses to check for equality as stated on Ruby's Array docs:
def eql?(other_obj)
# Your comparing code goes here
end
def hash
#Generates an unique integer based on instance variables
end
For example:
class A
attr_accessor :name
def initialize(name)
#name = name
end
def eql?(other)
#name.eql?(other.name)
end
def hash
#name.hash
end
end
a = A.new('Peter')
b = A.new('Peter')
arr = [a,b]
puts arr.uniq
Removes b from Array leaving only one object
Hope this helps!
The uniq method can take a block that defines what to compare the objects on. For example:
class Task
attr_accessor :n
def initialize(n)
#n = n
end
end
t1 = Task.new(1)
t2 = Task.new(2)
t3 = Task.new(2)
a = [t1, t2, t3]
a.uniq
#=> [t1, t2, t3] # because all 3 objects are unique
a.uniq { |t| t.n }
#=> [t1, t2] # as it's comparing on the value of n in the object
I tried the solution from fsaravia above and it didn't work out for me. I tried in both Ruby 2.3.1 and Ruby 2.4.0.
The solution I've found is very similar to what fsaravia posted though, with a small tweak. So here it is:
class A
attr_accessor :name
def initialize(name)
#name = name
end
def eql?(other)
hash.eql?(other.hash)
end
def hash
name.hash
end
end
a = A.new('Peter')
b = A.new('Peter')
arr = [a,b]
puts arr.uniq
Please, don't mind that I've removed the # in my example. It won't affect the solution per se. It's just that, IMO, there wasn't any reason to access the instance variable directly, given a reader method was set for that reason.
So...what I really changed is found inside the eql? method, where I used hash instead name. That's it!
If you look at the Array#| operator it says that it uses the eql?-method, which on Object is the same as the == method. You can define that by mixin in the Comparable-module, and then implement the <=>-method, then you'll get lots of comparison-methods for free.
The <=> operator is very easy to implement:
def <=>(obj)
return -1 if this < obj
return 0 if this == obj
return 1 if this > obj
end
Regarding your 'update', is this what you are doing:
a = Task.new # => #<Task:0x007f8d988f1b78>
b = Task.new # => #<Task:0x007f8d992ea300>
c = [a,b] # => [#<Task:0x007f8d988f1b78>, #<Task:0x007f8d992ea300>]
a = Task.new # => #<Task:0x007f8d992d3e48>
d = [a] # => [#<Task:0x007f8d992d3e48>]
e = c|d # => [#<Task:0x007f8d988f1b78>, #<Task:0x007f8d992ea300>, \
#<Task:0x007f8d992d3e48>]
and then suggesting that e = [a, b, a]? If so, that's the problem, because a no longer points to #<Task:0x007f8d988f1b78>. All you can say is e => [#<Task:0x007f8d988f1b78>, b, a]
I took the liberty to rewrite your class and add the methods that needs to be overwritten in order to use uniq (hash and eql?).
class Task
METHODS = [:status, :description, :priority, :tags]
attr_accessor *METHODS
def initialize task_line
#status, #description, #priority, #tags = *task_line.split("|")
#tags = #tags.split(",")
end
def eql? another_task
METHODS.all?{|m| self.send(m)==another_task.send(m)}
end
alias_method :==, :eql? #Strictly not needed for array.uniq
def hash
[#status, #description, #priority, #tags].hash
end
end
x = [Task.new('1|2|3|4'), Task.new('1|2|3|4')]
p x.size #=> 2
p x.uniq.size #=> 1

How to compare a class to any other arbitrary class via include? method

I have implemented comparable and enumerable so that I can use comparisons and include:
Given the simple class below:
class Card
include Comparable
include Enumerable
attr_accessor :value
def initialize(v)
#value = v
end
def <=>(other)
#value <=> other.value
end
def each
yield #value
end
end
Then:
c = Card.new(1) #so right now we have #value at 1
Neither of these include methods work though:
[1,3,4,5].include?(c)
c.include?([1,3,4,5])
Is it at all possible to use the include method to do this? I know I can do it another way, but i'd like to do it "ruby like"! (Assuming this is even the ruby like way...) I'm just getting into ruby from java and c++
Thanks in advance!
If you stare at your code long enough, you'll see. You implement a spaceship operator that assumes that other is a Card. But you invoke it on Fixnums! You need to do a little type checking there:
class Card
include Comparable
include Enumerable
attr_accessor :value
def initialize(v)
#value = v
end
def <=>(other)
if other.is_a?(Card)
#value <=> other.value
else
#value <=> other
end
end
def each
yield #value
end
end
c = Card.new(1)
[1,3,4,5].include?(c) # => true
c.include?([1,3,4,5]) # => false # because 1 does not contain an array [1, 2, 3, 4, 5]

What's the right way to implement equality in ruby

For a simple struct-like class:
class Tiger
attr_accessor :name, :num_stripes
end
what is the correct way to implement equality correctly, to ensure that ==, ===, eql?, etc work, and so that instances of the class play nicely in sets, hashes, etc.
EDIT
Also, what's a nice way to implement equality when you want to compare based on state that's not exposed outside the class? For example:
class Lady
attr_accessor :name
def initialize(age)
#age = age
end
end
here I'd like my equality method to take #age into account, but the Lady doesn't expose her age to clients. Would I have to use instance_variable_get in this situation?
To simplify comparison operators for objects with more than one state variable, create a method that returns all of the object's state as an array. Then just compare the two states:
class Thing
def initialize(a, b, c)
#a = a
#b = b
#c = c
end
def ==(o)
o.class == self.class && o.state == state
end
protected
def state
[#a, #b, #c]
end
end
p Thing.new(1, 2, 3) == Thing.new(1, 2, 3) # => true
p Thing.new(1, 2, 3) == Thing.new(1, 2, 4) # => false
Also, if you want instances of your class to be usable as a hash key, then add:
alias_method :eql?, :==
def hash
state.hash
end
These need to be public.
To test all your instance variables equality at once:
def ==(other)
other.class == self.class && other.state == self.state
end
def state
self.instance_variables.map { |variable| self.instance_variable_get variable }
end
Usually with the == operator.
def == (other)
if other.class == self.class
#name == other.name && #num_stripes == other.num_stripes
else
false
end
end

Resources