Reputation: 769
So I am quite new to Ruby and still learning idioms. I still have a strong static-typing mindset, so maybe one problem is that I'm over-typing. Anyway, my situation is this.
I have an object called a Gene,with :name and :id. I have another object called a Genotype, which maintains an array of Gene.
I'd like to check whether a given Genotype contains a given Gene. I'd like to able to pass Genotype.has_gene? a gene name, a gene id, or an actual Gene. In the former cases, the routine will match on name OR id, whichever is passed. If a full Gene is passed, the routine will insist on a match on both values.
My logic is to check whether the passed value is an Integer, in which case I assume it's an id; else check if it's a String and assume it's a name; else check whether it's a Gene; else complain and bail.
The code looks like this:
def has_gene?( gene )
if gene.is_a? Integer
id = gene
name = ""
elsif gene.is_a? String
id = nil
name = gene
elsif gene.is_a? Gene
id = gene.id
name = gene.name
else
raise "Can't intepret passed data as gene information"
end
name_valid = false
id_valid = false
@gene_specs.each do |current_gene_spec|
current_gene = current_gene_spec.gene
name_valid = name.empty? || name == current_gene.name
id_valid = id.nil? || id == current_gene.id
break if name_valid && id_valid
end
return name_valid && id_valid
end
Something feels wrong here but I can't pin it down. It seems to lack Ruby's famous conciseness :-)
Thoughts?
Upvotes: 0
Views: 980
Reputation: 231
Ruby has methods to coerce values into basic objects, eg Kernel#Array. We can use this to coerce a value into a sensible array:
Array(nil) # => []
Array(10) # => [10]
Array("hello") # => ["hello"]
Array([1, 2, 3]) # => [1, 2, 3]
This allows us to write very Rubyish methods that are flexible in the type of inputs they take without spending a lot of space checking types. Consider this contrived example:
def say_hello(people)
people = [people] if people.is_a?(Person)
people.each { |p| puts "Hello, #{p.name}" }
end
def say_hello(people)
Array(people).each { |p| puts "Hello, #{p.name}" }
end
For your case, rather than adding a Gene
method to Kernel
, I suggest adding a class method to do coercion:
class Gene
def self.coerce(geneish)
case geneish
when Gene
geneish
when Integer
new(id: geneish)
when String
new(name: geneish)
else
raise ArgumentError, "Can't coerce #{geneish.inspect} into a Gene"
end
end
end
def has_gene?(gene)
gene = Gene(gene)
name_valid = false
id_valid = false
@gene_specs.each do |current_gene_spec|
current_gene = current_gene_spec.gene
name_valid = gene.name.empty? || gene.name == current_gene.name
id_valid = gene.id.nil? || gene.id == current_gene.id
break if name_valid && id_valid
end
return name_valid && id_valid
end
I've left the rest of your has_gene?
method intact, though several of the other answers here have good suggestions for using some Enumerable
methods to clean it up.
Upvotes: 3
Reputation: 80075
You could write a method which accepts a block, allowing the user of the class to specify what to look for:
Gene = Struct.new(:name, :id)
class Genotype
def initialize
@arr=[]
end
def add(gene)
@arr << gene
end
def any?(&block)
@arr.any?(&block)
end
end
gt = Genotype.new
gt.add Gene.new('a',0)
gt.add Gene.new('b',1)
p gt.any?{|g| g.name == "john"} #false
p gt.any?{|g| g.values == ["b",1]} #true
Upvotes: 0
Reputation: 76280
Ruby is not only dynamically typed, but it adheres to the duck typing paradigm which states:
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
What does it means? It means that in Ruby you shouldn't really care (unless in specific situation) if an Object is of the exact X
class. You should care if it behaves like an X
object.
How do you check it? The most famous method to check if a specific object behaves like you want to is to use the #respond_to?
. This method will check if a method can be called on an object.
From there you can just check if the object responds to a method in the form of #to_x
where x
is the name of the class (even custom classes) and just call it to convert any type into the class you are going to need. So for example if you expect only a string to be used inside a method you can do:
def a_method( string )
unless (string.respond_to? :to_str) // trigger error
string = string.to_str
// use string
end
This way, if I'm defining a special type Duck
like:
class Duck
def to_str
// internally convert Duck to String
end
...
end
I can pass it to your function:
obj = Duck.new
a_method( obj )
and it would work like expected by both me and the designer of a_method
, without even knowing each other.
Upvotes: 0
Reputation: 87486
Here is how I would simplify it. You could also use duck-typing if you want, but I think it would make the code more complicated.
def genes
@gene_specs.collect &:gene
end
def has_gene?(x)
case x
when Integer
genes.any? { |g| g.id == x }
when String
genes.any? { |g| g.name == x }
when Gene
genes.include?(x) # assumes that Gene#== is defined well
else
raise ArgumentError, "Can't intepret passed data as gene information"
end
end
By default, Ruby will compare objects by identity (i.e. their location in memory), but for the Gene class you might want to do something different like this:
class Gene
def ==(other)
return false unless other.class == Gene
id == other.id
end
end
It pays off to spend some time studying the methods in Ruby's Enumerable module.
Upvotes: 1
Reputation: 6494
Even though ruby doesn't enforce types for method parameters, it's still a bad practice to allow multiple types for one parameter unless there are good reasons. It would be clearer if you provided three separate methods:
def has_gene?( gene)
...
end
def has_gene_with_id?( id)
...
end
def has_gene_with_name?( name)
...
end
Upvotes: 1