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
Related
I'm attempting to adapt the method-chaining example cited in this posting (Method chaining and lazy evaluation in Ruby) to work with an object that implements the Enumerable class (Implement a custom Enumerable collection class in Ruby )
Coffee class:
class Coffee
attr_accessor :name
attr_accessor :strength
def initialize(name, strength)
#name = name
#strength = strength
end
def <=>(other_coffee)
self.strength <=> other_coffee.strength
end
def to_s
"<name: #{name}, strength: #{strength}>"
end
end
Criteria class:
class Criteria
def initialize(klass)
#klass = klass
end
def criteria
#criteria ||= {:conditions => {}}
end
# only show coffee w/ this strength
def strength(strength)
criteria[:strength] = strength
self
end
# if there are multiple coffees, choose the first n=limit
def limit(limit)
criteria[:limit] = limit
self
end
# allow collection enumeration
def each(&block)
#klass.collection.select { |c| c[:strength] == criteria[:strength] }.each(&block)
end
end
CoffeeShop class:
class CoffeeShop
include Enumerable
def self.collection
#collection=[]
#collection << Coffee.new("Laos", 10)
#collection << Coffee.new("Angkor", 7)
#collection << Coffee.new("Nescafe", 1)
end
def self.limit(*args)
Criteria.new(self).limit(*args)
end
def self.strength(*args)
Criteria.new(self).strength(*args)
end
end
When I run this code:
CoffeeShop.strength(10).each { |c| puts c.inspect }
I get an error:
criteria.rb:32:in block in each': undefined method '[]' for #<Coffee:0x007fd25c8ec520 #name="Laos", #strength=10>
I'm certain that I haven't defined the Criteria.each method correctly, but I'm not sure how to correct it. How do I correct this?
Moreover, the each method doesn't support the limit as currently written. Is there a better way to filter the array such that it is easier to support both the strength and limit?
Other coding suggestions are appreciated.
Your Coffee class defines method accessors for name and strength. For a single coffee object, you can thus get the attributes with
coffee.name
# => "Laos"
coffee.strength
# => 10
In your Criteria#each method, you try to access the attributes using the subscript operator, i.e. c[:strength] (with c being an Instance of Coffee in this case). Now, on your Coffee class, you have not implemented the subscript accessor which resulting in the NoMethodError you see there.
You could thus either adapt your Criteria#each method as follows:
def each(&block)
#klass.collection.select { |c| c.strength == criteria[:strength] }.each(&block)
end
or you could implement the subscript operators on your Coffee class:
class Coffee
attr_accessor :name
attr_accessor :strength
# ...
def [](key)
public_send(key)
end
def []=(key, value)
public_send(:"#{key}=", value)
end
end
Noe, as an addendum, you might want to extend your each method in any case. A common (and often implicitly expected) pattern is that methods like each return an Enumerator if no block was given. This allows patterns like CoffeeShop.strength(10).each.group_by(&:strength).
You can implement this b a simple on-liner in your method:
def each(&block)
return enum_for(__method__) unless block_given?
#klass.collection.select { |c| c.strength == criteria[:strength] }.each(&block)
end
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
I've implemented the Comparable module in hopes of using it with a hash like so:
class Author
include Comparable
attr_reader :name
def initialize(name)
#name = name
end
def <=>(other)
name.downcase <=> other.name.downcase
end
end
class Post
attr_reader :body
def initialize(body)
#body = body
end
end
anthony = Author.new('anthony')
anthony2 = Author.new('anthony')
p anthony == anthony2 # => true
hash = {}
hash[anthony] = [Post.new("one"), Post.new("two")]
p hash
# => {#<Author:0x007fa7481ae6f8 #name="anthony">=>[#<Post:0x007fa7481ade10 #body="one">, #<Post:0x007fa7481add70 #body="two">]}
posts = hash[anthony2]
p posts
# => nil
My initial goal was that I could ask for hash values with either anthony or anthony2. I thought that because anthony == anthony2 but clearly that's not true. Just two questions:
How does a hash figure out if a key is == to itself?
Is there another data structure / ruby class I should be using here or should I implement my own?
The Comparable module is used for ordering. If you want to deal with equivalence for the purpose of hashing you have a little more work to do.
From the documentation on Hash:
Two objects refer to the same hash key when their hash value is identical and the two objects are eql? to each other.
So you'll need to extend it a bit more. Since your #name.downcase is really important, I've added a variable to capture it to reduce how much computation is required. Repeatedly downcasing the same thing is wasteful, especially when used for comparisons:
class Author
include Comparable
attr_reader :name
attr_reader :key
def initialize(name)
#name = name
#key = #name.downcase
end
def hash
#key.hash
end
def eql?(other)
#key.eql?(other.key)
end
def <=>(other)
#key <=> other.key
end
end
Now this works out:
Author.new('Bob') == Author.new('bob')
# => true
As well as this:
h = { }
h[Author.new('Bob')] = true
h[Author.new('bob')]
# => true
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'
When writing big classes with loads of instance variables, writing ==, eql? and hash methods is a big hassle. Is there a way to make a "template class" to automate this process? Or with any other ways.
Example:
class Template
def ==(other)
//Some way of comparing all of self's instance variables to other's.
end
def eql?(other)
self == other
end
def hash
//Some way of XORing the instance variables of self
end
end
class Test < Example
attr_reader :foo
attr_reader :bar
def initialize(foo, bar)
#foo = foo
#bar = bar
end
end
a = Test.new "123", "456"
b = Test.new "123", "456"
a == b
> true
Test = Struct.new(:foo, :bar)
a = Test.new "123", "456"
b = Test.new "123", "456"
a == b
# => true
You could define your fields so that you are able to reflect on them later on. Assuming all instance variables always exist and you want to use all of them in similar fashion, Ruby already provides you with enough reflection to pull it off, about this way
class MyFineClass
attr_reader :foo, :bar # and loads of stuff
def initialize(...)
# tons of those ivars initialized
end
def ==(other)
other.is_a?(self.class) &&
# this assumes all instance variables to have attr_readers
instance_variables.all? { |ivar| send(ivar) == other.send(ivar) }
# ...and if they don't, you need instance_variable_get, like this
#instance_variables.all? { |ivar| instance_variable_get(ivar) == other.instance_variable_get(ivar) }
end
end
In case you want more control over how the fields should be treated, you could add a notion of "field" and a little metaprogramming
class MyFineClass
#fields = []
def self.fields; #fields; end
def self.fields(field_name)
attr_reader field_name
#fields << field_name
end
field :foo
field :bar
# and loads more
def initialize(...)
# again, tons of those ivars initialized
end
def ==(other)
other.is_a?(self.class) && self.class.fields.all? { |f| send(f) == other.send(f) }
end
end
Next you would of course pull fields stuff and == to separate module and include that to MyFineClass. Got the idea? Develop that module a little further and it might start looking a little like certain bits in ActiveModel or DataMapper. :-)
Ruby Facets provides the Equitable module, which provides almost exactly what you're looking for.
Your example would become:
class Test
include Equitable(:foo, :bar)
end
If you don't want to use the whole gem, you can grab the source - it's pretty light.