okliv
okliv

Reputation: 3959

Is this use of case statement a bad practice?

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

Answers (2)

Cary Swoveland
Cary Swoveland

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

David Grayson
David Grayson

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

Tangential comment

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

Related Questions