views:

224

answers:

12

I had an argument with a colleague about the best way to assign a variable in an if..else block. His orignal code was :

@products = if params[:category]
  Category.find(params[:category]).products
else
  Product.all
end

I rewrote it this way :

if params[:category]
  @products = Category.find(params[:category]).products
else
  @products = Product.all
end

This could also be rewritten with a one-liner using a ternery operator (? :) but let's pretend that product assignment was longer than a 100 character and couldn't fit in one line.

Which of the two is clearer to you? The first solution takes a little less space but I thought that declaring a variable and assigning it three lines after can be more error prone. I also like to see my if and else aligned, makes it easier for my brain to parse it!

+1  A: 

Seems to me that the second one would be more readable to the typical programmer. I'm not a ruby guy so I didn't realize that an if/else returns a value.... So, to take me as an example (and yes, that's my perspective :D), the second one seems like a good choice.

joeslice
You say yourself that you don't know Ruby, so why should we take you as an example? Do you write your code to cater to people who don't know any programming language and speak only Tibetan?
Chuck
+1  A: 

First if using ternary, second if not.

The first one is near impossible to read.

Nate Noonen
Yes but do yo do a multiline ternary if it's too long to fit in a line? Doesn't seem right to me!
Pierre Olivier Martel
Why is it near impossible to read? Do you have much experience writing functional Ruby code, or is this because you're trying to read Ruby as another language?
Chuck
@Chuck: it is "near impossible to read" for everyone who see it for the first time.
klew
Near impossible to read is purely an opinion, obviously. I rarely program in Ruby (though I like it a lot), and it looks perfectly clean to me.
RHSeeger
One of my readability pet peeves is separating declaration from assignment when it doesn't need to be. IMHO variables should be declared as late as possible and assigned as close to their declaration as possible. Here the separation is unnecessary, and is fixed by the whitespace cleanup in the accepted answer.
Nate Noonen
+1  A: 

I'm not a Ruby person either, but Alarm Bells are instantly ringing for scope of the second command, will that variable even be available after your if block ends?

blissapp
Yes, it will. It's an instance variable.
Chuck
+1 Good question for sure.
joeslice
In ruby, if you prefix your variable with @, it makes the variable an instance variable that will be available outside the block.
Pierre Olivier Martel
In addition to that, Ruby does not have block scope (block in the traditional sense, if/then/else in this case).
molf
+13  A: 

As a Ruby programmer, I find the first clearer. It makes it clear that the whole expression is an assignment with the thing assigned being determined based on some logic, and it reduces duplication. It will look weird to people who aren't used to languages where everything is an expression, but writing your code for people who don't know the language is not that important a goal IMO unless they're specifically your target users. Otherwise people should be expected to have a passing familiarity with it.

I also agree with bp's suggestion that you could make it read more clearly by indenting the whole if-expression so that it is all visually to the right of the assignment. It's totally aesthetic, but I think that makes it more easily skimmable and should be clearer even to someone unfamiliar with the language.

Just as an aside: This sort of if is not at all unique to Ruby. It exists in all the Lisps (Common Lisp, Scheme, Clojure, etc.), Scala, all the MLs (F#, OCaml, SML), Haskell, Erlang and even Ruby's direct predecessor, Smalltalk. It just isn't common in languages based on C (C++, Java, C#, Objective-C), which is what most people use.

Chuck
Yeah but isn't it more error prone? Especially if over time the if..else blocks grows longer. Ruby is very permissive (dont get me started about optional parenthesis) but that does not make it always better.
Pierre Olivier Martel
@Pierre: By that logic, functions must also be more error prone because they can also grow larger. Or for that matter, the line containing the assignment could also grow (I have actually seen such things). Ruby code is inherently somewhat error-prone, but I don't see how functional if-expressions are more error-prone than imperative ones.
Chuck
Python too :) although the syntax is sligthly different: `products = Category.find(params["category"]).products() if params["category"] else Products.all()`
badp
+1  A: 

I would say that second version is more readable for people non familiar with that structure in ruby. So + for it! On the other side, first contruction is more DRY.

As I look on it a little bit longer, I find first solution more attractive. I'm a ruby programmer, but I didn't use it earlier. For sure I'll start to!

klew
+5  A: 

encapsulation...

@products = get_products

def get_products
  if params[:category]
    Category.find(params[:category]).products
  else
    Product.all
  end
end
Derick Bailey
That's clean... And it would work in most languages.
Pierre Olivier Martel
If he use it only once I wouldn't recommend adding new method.
klew
What if the assignment to `@products` was the only thing that happened in the original method? Seems you're dodging the question. ;-)
molf
molf - if that's the case, he's got a command / query violation and should refactor the code to what i wrote.
Derick Bailey
@Derick; nonsense. Your example would still return data to the caller in addition to changing the state of the object (*and* issue a database query while at it). Your refactoring doesn't change a thing. Ruby's implicit `return` is not your friend if you prefer strict CQS.
molf
+6  A: 
badp
`all` in this case would run a db query, so the second is not a great idea. If you were using DataMapper, `all` would be lazily evaluated, so this would only do one query with that framework.
BaroqueBobcat
TBH I find your second example less readable than the OP's first block with "wrong" whitespace (which btw is still a valid statement).
Bart van Heukelom
I wouldn't indent it *that* way (it's too far from the usual way of writing conditionals to read at a glance), but I agree that indenting it all right of the equals sign is a good idea for readability.
Chuck
@chuck Is this better?
badp
I definitely think so. That reads very clearly to me, even at a glance.
Chuck
+1  A: 

Assuming your models look like this:

class Category < ActiveRecord::Base
  has_many :products
end
class Product < ActiveRecord::Base
  belongs_to :category
end

you could do something even more crazy, like this:

#assuming params[:category] is an id
@products = Product.all( params[:category] ? {:conditions => { :category_id => params[:category]}} : {})

Or, you could use the sexy, lazily loaded named_scope functionality:

class Product < ActiveRecord::Base
  ...

  #again assuming category_id exists
  named_scope :all_by_category, lambda do |cat_id|
    if cat_id
      {:conditions => {:category_id => cat_id}}
    end
  end

  #if params[:category] is a name, and there is a has and belongs to many
  named_scope :all_by_category, lambda do |cat_name|
    if cat_name
      {:joins => :categories, :conditions => ["categories.name = ?",cat_name]}
    end
  end
  ...
end

used like

@products = Product.all_by_category params[:category]
BaroqueBobcat
That's a cool way to turn the problem around! But I wanted the debate to be more around the syntax than the actual functionality of the code. But I'll make good note of it!
Pierre Olivier Martel
of the two options you presented, I prefer the first one.
BaroqueBobcat
I think your named scope is confusing. I would expect `Product.all_by_category(nil)` to return only those products that belong to no categories, instead of *all* products.
molf
I was trying to build a named_scope that acted like Daniel Ribeiro's `retrieve_products`, but you are right, its behavior is confusing.My thoughts were a) I'd like the finders used to both be on the Product class, since that is what the action is about. b) I generally prefer to push as much behavior into the model as possible.
BaroqueBobcat
+1  A: 

I usually find the second one cleaner. When face with small methods that essentially do this, I usually leave the extra variable out by Replacing Nested Conditional with Guard Clauses, which leaves something like this:

def retrieve_products(params)
  if params[:category]
    return Category.find(params[:category]).products
  end
  Product.all
end

In ruby I usually go a little extra mile to make it two lines, if it fits nicely:

def retrieve_products(params)
  return Category.find(params[:category]).products if params[:category]
  Product.all
end

In this case, as the conditional case is much more verbose, I'd reverse it into:

def retrieve_products(params)
  return Product.all unless params[:category]
  Category.find(params[:category]).products
end
Daniel Ribeiro
+2  A: 
@products =
if params[:category]
  Category.find(params[:category]).products
else
  Product.all
end

Is another option, it both avoids repeating @products and keeps the if aligned with else.

matsadler
I've never seen this before, but it's creative!
molf
+2  A: 

Just another approach:

category = Category.find(params[:category]) if params[:category]
@products = category ? category.products : Product.all
Matías Flores
A: 

If one is browsing through the code, I'd say the second code block (yours), is definitely the one I find easiest to quickly comprehend.

Your buddy's code is fine, but indentation, as bp pointed out, makes a world of difference in this sense.

Andres