1

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'
Community
  • 1
  • 1
oddlyzen
  • 33
  • 5
  • Again, I do not see the reason not to change `self.to.class` to just `self.to`. – hirolau Dec 10 '13 at 16:02
  • Before I posted the question, I tried eliminating the `.class` call and still no dice... – oddlyzen Dec 10 '13 at 16:08
  • Ok, why did it not work? Try to print the class of `self.to` before the case statement and see that you are not getting any other class you are not expecting, maybe a `String`? – hirolau Dec 10 '13 at 16:11
  • I tried that before I posted the question... `pry(main)> tf.end_date` `12960000` `RuntimeError: InvalidType` Ugh. Sorry. – oddlyzen Dec 10 '13 at 16:42

3 Answers3

5

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
oddlyzen
  • 33
  • 5
hirolau
  • 13,451
  • 8
  • 35
  • 47
  • I admit, the code sample I used is slightly ambiguous. I am running into this because of `ActiveSupport`'s extensions to `Float` and `Integer` in relation to Time. I wanted the type structure to handle initialization parameters of several variants—`1.year` *or* `1.year.from_now`. The actual code is has been added above for reference. I hope that clarifies. – oddlyzen Dec 10 '13 at 15:49
  • Please add real code with and example or two to you original post. In strange cases it might be an idea to create lambdas and use them in the case-statement. – hirolau Dec 10 '13 at 15:52
  • 1
    In you example you can just leave out the `.class` and the code should work fine. – hirolau Dec 10 '13 at 15:59
  • I agree that `case obj` rather than `case obj.class` should work, @hirolau, but it does not in this instance. Is this just a Ruby 1.8.7 thing? I have to admit, I've been using newer versions for a while and had to revisit a project that was on 1.8.7 when this was encountered. – oddlyzen Dec 10 '13 at 16:33
  • "...the case equality operator asks whether the left hand argument is an instance of the class (or any subclass)" Okay, if that is the case, shouldn't `Numeric === Integer` or `ActiveRecord::Base === MyModel`... it seems to follow, since `Class === Object && Object === Class` or `Kernel === Object && Object === Kernel`. Or am I traveling too far up the inheritance tree into the land of abstractions that were not meant to be un-abstracted? :) Thanks, BTW. – oddlyzen Dec 10 '13 at 17:12
  • I like your use of lambdas to clarify and encapsulate the problem. In retrospect, your answer makes sense and is correct, I just happened to have the 'a-ha!' moment when I read the update @stefan added regarding the semantics of 'is a class a type of class?'... If I could accept both answers as 'correct', I would. Thanks again for your time. – oddlyzen Dec 10 '13 at 17:22
1

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
BroiSatse
  • 44,031
  • 8
  • 61
  • 86
  • Thanks, @BroiSatse. I thought about that, too, but I think the reason this is so confusing might be because of some of the monkey-patching, so I wanna steer clear of ratcheting up the patching a notch. ;-) – oddlyzen Dec 10 '13 at 16:10
  • I should add that it just seems to logically follow that if `kind_of?` works with a `Class === instance`, then a class-level `kind_of?` should check type inclusion via `ancestors`. – oddlyzen Dec 10 '13 at 16:23
1

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.

Community
  • 1
  • 1
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • I agree that `case obj` should work, but it does not. Is this just a Ruby 1.8.7 thing? I have to admit, I've been using newer versions for a while and had to revisit a project that was on 1.8.7. – oddlyzen Dec 10 '13 at 16:26
  • @MarkCoates please explain "does not work", what's your input and your expected result? – Stefan Dec 10 '13 at 16:39
  • @MarkCoates thanks for posting the actual input, I've updated my answer. – Stefan Dec 10 '13 at 17:10
  • your extrapolation on the semantics made the lightbulb flicker on. Thanks for your answer. It definitely makes more sense now, even into the upper-levels of abstraction in the Ruby type system. Cheers! +1 – oddlyzen Dec 10 '13 at 17:19
  • `ActiveSupport::Duration` is tricky. It delegates all messages to the underlying value using `method_missing`. That's why `5.months.class` returns `Fixnum`. – Stefan Dec 10 '13 at 17:32
  • That would be the reason for my confusion—and exactly what I discovered while digging deeper. Three cheers for monkey patching, lol :-P Thanks again! – oddlyzen Dec 10 '13 at 17:34