Steve Lane
Steve Lane

Reputation: 769

"Proper" idiom for handling multiple types in ruby arguments

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

Answers (5)

evilstreak
evilstreak

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

steenslag
steenslag

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

Shoe
Shoe

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

David Grayson
David Grayson

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

lassej
lassej

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

Related Questions