Search code examples
rubyclasstypesswitch-statementcomparison-operators

Comparison of Classes using the `===` (subsumption operator)


The fact that TypeOfClass === TypeOfClass is false strikes me as counter-intuitive. In the following code, even if field.class is the same class, it evaluates to false:

case field.class
when Fixnum, Float
  field + other_field
when Date, DateTime, Time
  field
else
  puts 'WAT?'
end

I did this:

Fixnum === Fixnum # => false
Class === Class   # => true

I found another thread:

Integer === 3 # => true
Fixnum === 3  # => true
3.class       # => Fixnum

I fail to find a reason for this language design. What were the language designers thinking when they baked in this behavior?

I think this is relevant to the answer provided in another thread. It is not unnatural to assume that Numeric === Integer since an Integer is a more specific type of Numeric. But, it isn't:

Numeric === Integer #=> false

I think case statements or === requires caution. If this operator is what we think it is , then, a Numeric should be a Numeric, an Integer should be a Numeric, etc.

Does anyone have an explanation of why this feature doesn't extend to classes? It seems like it would be easy enough to return true if the compared class is a member of the class' ancestors.

Based on an answer submitted below, the code was originally classifying Time (as extended by ActiveSupport:CoreExtensions::Integer::Time and ActiveSupport:CoreExtensions::Float::Time):

Timeframe = Struct.new(:from, :to) do
  def end_date
    case self.to
    when Fixnum, Float
      self.from + self.to
    when Date, DateTime, Time
      self.to
    else  
      raise('InvalidType')
    end
  end
end

and in the console, I get:

tf = Timeframe.new(Time.now, 5.months)
# => #<struct Timeframe from=Tue Dec 10 11:34:34 -0500 2013, to=5 months>
tf.end_date
# => RuntimeError: InvalidType
#  from timeframe.rb:89:in `end_date'

Solution

  • That's Module#=== and its intended behavior:

    mod === obj → true or false

    Case Equality—Returns true if anObject is an instance of mod or one of mod’s descendants. Of limited use for modules, but can be used in case statements to classify objects by class.

    It simply returns obj.kind_of? mod:

    Fixnum === Fixnum      #=> false
    Fixnum.kind_of? Fixnum #=> false
    
    Class === Class        #=> true
    Class.kind_of? Class   #=> true
    
    String === "foo"       #=> true
    "foo".kind_of? String  #=> true
    

    3 is both, an Integer and a Fixnum because of its class hierarchy:

    3.kind_of? Integer     #=> true
    3.kind_of? Fixnum      #=> true
    3.class.ancestors      #=> [Fixnum, Integer, Numeric, Comparable, Object, Kernel, BasicObject]
    

    Numeric is not an Integer, it's a Class:

    Numeric.kind_of? Integer  #=> false
    Numeric.kind_of? Class    #=> true
    

    But 3, (2/3) and 1.23 are all Numeric:

    3.kind_of? Numeric               #=> true
    Rational(2, 3).kind_of? Numeric  #=> true
    1.23.kind_of? Numeric            #=> true
    

    Bottom line: for case statements, just use case obj instead of case obj.class.

    Update

    You are getting this error because 5.months doesn't return an Integer, but a ActiveSupport::Duration:

    Integer === 5.months                 #=> false
    ActiveSupport::Duration === 5.months #=> true
    

    Calling your method with 5.months.to_i or adding ActiveSupport::Duration to your classes should fix it.