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

340 views Asked by At

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'
3

There are 3 answers

6
Stefan On BEST ANSWER

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.

2
BroiSatse On

If one of the operands is a class, it is checking whether the second one is this class instance. If they are both classes it will return false unless at least one of them is Class.

Note that Class is both a class and an instance of itself - it's probably biggest ruby weird TBH, but it makes perfect sense.

The reason I would vote for keeping this logic is that we can write those nice case statements without adding .class to objects.

Summary:

ClassName === object    <=>    object.kind_of? ClassName

However, if you really want to override this use:

class Class
  def ===(object)
    return object == self if object.is_a? Class
    super
  end
end
6
hirolau On

I do not really see the problem here. For classes, the case equality operator asks whether the left hand argument is an instance of the class (or any subclass). So Fixnum === Fixnum really asks: "Is the Fixnum class itself a subclass of Fixnum?" No it is not.

Is the class Class itself a class? Class === Class, yes it is.

The point of the operator is that you should not need to go look for the class. What is wrong with using the case statement without the .class method in the beginning?

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

If you have a more complex example you can write your own lambdas to make the case statement easier:

field_is_number =  -> x {[Fixnum, Float].include? x.class}
field_is_time   =  -> x {[Date, DateTime, Time].include? x.class}

case field.class
  when field_is_number
    field + other_field
  when field_is_time
    field
  else
    puts 'WAT?'
end