Reputation: 3194
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!
Upvotes: 48
Views: 56656
Reputation: 16759
This could also be rewritten with a one-liner using a ternary operator (
? :
) but let's pretend that product assignment was longer than 100 characters and couldn't fit in one line.
Let's pretend not – because in many cases, it pays off for readability to optimize variable names and condition complexity for length so that the assignment incl. condition fits into one line.
But instead of using the ternary operator (which is unnecessary in Ruby and hard to read), use if … then … else
. The then
(or a ;
) is necessary when putting it all into one line. Like this:
cat = params[:category]
@products = if cat then Category.find(cat).products else Product.all end
For readability and self-documenting code, I love it when my programs read like English sentences. Ruby is the best language I found for that, so far.
Upvotes: 0
Reputation: 149
I think the best code is :
@products = Category.find(params[:category])&.products.presence || Product.all
The "&" after de find ensure that the method "products" will not evaluate if category is nil.
Upvotes: 1
Reputation: 34903
I don't like your use of parentheses in your first block. Yes, I'm a LISP user, but I believe I make a fair point when I say the first might look confusing in the middle of other code, maybe around other if
blocks.
How about...
@products = (if (params[:category])
((Category.find params[:category]).
products)
else
(Product all)
end
)
(^ Tongue in cheek, to exacerbate the issues with @badp's answer)
Upvotes: 0
Reputation: 32388
Yet another approach would be using a block to wrap it up.
@products = begin
if params[:category]
Category.find(params[:category]).products
else
Product.all
end
end
which solves the assignment issue. Though, it's too many lines for such "complex" code. This approach would be useful in case that we would want to initialize the variable just once:
@products ||= begin
if params[:category]
Category.find(params[:category]).products
else
Product.all
end
end
This is something you can't do with the rewritten code and it is correctly aligned.
Upvotes: 6
Reputation: 3458
As an alternative to the syntax in badp's answer, I'd like to propose:
@products =
if params[:category]
Category.find(params[:category]).products
else
Product.all
end
I claim this has two advantages:
It does take an extra line of code, which I would normally dislike, but in this case it seems worthwhile to trade vertical minimalism for horizontal minimalism.
Disclaimer: this is my own idiosyncratic approach and I don't know to what extent it is used elsewhere in the Ruby community.
Edit: I should mention that matsadler's answer is also similar to this one. I do think having some indentation is helpful. I hope that's enough to justify making this a separate answer.
Upvotes: 69
Reputation: 495
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.
Upvotes: 0
Reputation: 69
Just another approach:
category = Category.find(params[:category]) if params[:category]
@products = category ? category.products : Product.all
Upvotes: 6
Reputation: 237010
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.
Upvotes: 22
Reputation: 10150
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]
Upvotes: 2
Reputation: 189
@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
.
Upvotes: 2
Reputation: 11813
I don't like your use of whitespace in your first block. Yes, I'm a Pythonista, but I believe I make a fair point when I say the first might look confusing in the middle of other code, maybe around other if
blocks.
How about...
@products = if params[:category] Category.find(params[:category]).products
else Product.all
end
@products = if params[:category]
Category.find(params[:category]).products
else
Product.all
end
You could also try...
@products = Product.all #unless a category is specified:
@products = Category.find(params[:category]).products if params[:category]
...but that's a bad idea if Product.all
actually is a function-like which could then be needlessly evaluated.
Upvotes: 17
Reputation: 72858
encapsulation...
@products = get_products
def get_products
if params[:category]
Category.find(params[:category]).products
else
Product.all
end
end
Upvotes: 16
Reputation: 14967
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!
Upvotes: 1
Reputation: 1370
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?
Upvotes: 1
Reputation: 1371
First if using ternary, second if not.
The first one is near impossible to read.
Upvotes: 1
Reputation: 3454
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.
Upvotes: 0