Reputation: 3959
I have some code like this:
case Product.new.class # ActiveRecord instance class => Product
when Module
'this condition will always be true'
when Product
'i need this to be true, but first condition is always true, so it never happens'
end
Here, when Module
is always true
. Why? Is this unexpected behaviour?
Upvotes: 2
Views: 1242
Reputation: 110725
Ah, such a seemingly simple queston. But is it?
Here Product
is some class, say:
Product = Class.new
Since
Product.new.class
#=> Product
your case statement can be simplified to
case Product
when Module
'this condition will always be true'
when Product
'i need this to be true, so it never happens'
end
Recall that the case
statement uses the method ===
to determine which object to return, meaning that your case
statement is equivalent to
if Module === Product
'this condition will always be true'
elsif Product === Product
'i need this to be true, so it never happens'
end
Let's see how the two logical expressions evaluate:
Module === Product #=> true
Product === Product #=> false
Note this is syntactic sugar for
Module.===(Product) #=> true
Product.===(Product) #=> false
Examine the docs for the method Module#=== to see how it works: it returns true
if Product
is an instance of Module
or of one of Module
's descendents. Well, is it?
Product.class #=> Class
Class.ancestors #=> [Class, Module, Object, Kernel, BasicObject]
It is! Now what about:
Product === Product
Does Product
have a method ===
?:
Product.methods.include?(:===)
#=> true
Where did it come from (we didn't define it, after all)? Let's first look at:
Product.ancestors
#=> [Product, Object, Kernel, BasicObject]
Does Object
have a method ===
? Checking the docs we see that it does: Object#===.1
So Object#===
is invoked. Right? Let's just confirm that:
Product.method(:===).owner
#=> Module
Whoops! It comes from Module
(which is both a module and a class), not from Object
. As we saw above, Product
is an instance of Class
and Class
is a subclass of Module
. Note also that here ===
is an instance method of Class
(and of Module
)2:
Class.instance_method(:===).owner
#=> Module
So Product
is in a quandary. Should it use Module#===
, an instance method supplied by it's parent (Class
), who inherited it from Module
, or should it go with Object#===
, that it inherits from its superclass, Object
? The answer is that precedence is with the former.
This is at the heart of Ruby's "object model". I will say no more about that, but I hope I have provided readers with some tools they can use to figure out what's going on (e.g., Object#method and Method#owner.
Since Product === Product
uses Module#===
just as Module == Product
does, we determine if the former returns true
by answering the question, " is Product
an instance of Product
or of one of Product
's decendents?". Product has no descendents and
Product.class #=> Class,
so the answer is "no", meaning Product === Product
returns false
.
Edit: I see I forgot to actually answer the question posed in the title. This calls for an opinion I suppose (a SO no-no), but I think case
statements are the greatest thing since sliced bread. They are particularly useful when one needs to compare various values to a reference value (e.g., the contents of a variable or to a value returned by a method) using ===
or ==
. For example (see Fixnum#===, where ===
is equivalent to ==
--note the typo in the docs, Regexp#=== and Range#===):
str =
case x
when 1 then 'cat'
when 2,3 then 'dog'
when (5..Float#INFINITY) then 'cow'
else 'pig'
end
result =
case obj
when String
...
when Array
...
end
case str
when /\d/
...
when /[a-z]/
...
end
Beyond that, however, I often use a case statement
in place of if..elsif..else..end
just because I think it`s tidier and more esthetically pleasing:
case
when time == 5pm
feed the dog
when day == Saturday
mow the lawn
...
end
1 In fact, this method is available to all objects, but is not generally invoked because the method is also defined for a descendant.
2 To be thoroughly confusing, Class also has a triple-equals class method:
Class.method(:===).owner #=> Module
.
Upvotes: 7
Reputation: 87486
This is not a bug. In Ruby, the class whose name is Class
is a subclass of the class whose name is Module
. Every instance of Class
is therefore also a Module
.
This is documented here, in the left column where it says "Parent":
http://www.ruby-doc.org/core-2.2.0/Class.html
You asked if this is a bad practice. Yes. It is a bad practice because the first case in your case statement is guaranteed to run, so it means that all the other cases of the case statement are unreachable code that can never run. Unreachable code is a bad practice. It would be better to write your code like this:
case something
when Product
# we know that it is a Product
when Customer
# we know that it is a Customer
else
# handle other cases
end
It is very weird that you are showing us some code that does not work, and then asking if it is a "bad practice". Obviously the code is a bad practice if it doesn't work at all in any situations! You need to first get your code to work. After it is working, you can try to think about different versions of the code and evaluate each one to see if it is "bad practice" or "good practice", but that assumes that those different versions actually work.
Suppose you had instead asked how to write a method in Ruby that multiplies a number by 4.
Here is some code that doesn't work, so we won't even discuss if it is bad practice or not:
def foo(x)
x * 3
end
Here is some code that works, but it is bad practice in all kinds of ways:
def foo(x)
return x + x+ x + x - x + x*1
end
Here is some code that works, and is good practice:
def foo(x)
x * 4
end
Upvotes: 3