Brevity vs. Clarity
Today’s TADFALAICKIU: balancing brevity and clarity.
I’m a big fan of the ternary operator. (That’s the bizarre little foo ? bar : baz
construct which Ruby, and others, borrowed from C.) It lets you express simple conditionals very concisely. However, the poor ternary operator gets a lot of flak. It is easily abused, and some people have even gone so far as to swear that any use of it is an evil use.
If that’s you, then I guess we’ll have to agree to disagree. :)
I’ll admit that I’m still learning the balancing act. I’ve been going through some older code of mine and discovering things like the following:
1 2 3 4 5 |
def smart_overview_url(options) @project ? project_overview_url(options.merge(:project_id => @project.id)) : overview_url(options) end |
Although certainly not one of the evilest invocations of the ternary operator, it is undeniably destined for some circle of Hell. Ternary Operator Rule #1: if you can’t fit the entire condition on a single line, use an explicit if-then-else construct.
1 2 3 4 5 6 7 |
def smart_overview_url(options) if @project project_overview_url(options.merge(:project_id => @project.id)) else overview_url(options) end end |
There, isn’t that cleaner? Other rules of thumb:
- If you need to nest a ternary operator inside another ternary operator, you’re almost certainly doing it wrong. Nested ternaries are hard to read, and that’s bad. Break it out. Be verbose.
- If you need to parenthesize any of the component parameters of the operator, you’re almost certainly doing it wrong. Parentheses add line noise to a sequence that already has quite a bit. Break it out. Be verbose.
So, when is it ok to use the ternary op? Try these examples:
1 2 3 4 5 6 |
# trivial conditions increment = x < y ? 1 : -1 # simple type conversions value = record.respond_to?(:something) ? record.something : nil # unfolding hashes element = hash[:here] ? hash[:here][:there] : nil |
Note that in the case where something is either a result of some operation or nil, you can also use the &&
operator in Ruby:
1 2 3 4 |
# simple type conversions value = record.respond_to?(:something) && record.something # unfolding hashes element = hash[:here] && hash[:here][:there] |
That works because &&
does not (necessarily) return a boolean value—it returns the first false/nil value, or the last value if none are false/nil.
Reader Comments
Thanks Jamis, I’ll put the && operator to use immediately!
5 Jan 2007
Jamis, congratulations for yet another great article! Your site is on my “must read daily” section.
I have to disagree on the usage of the && operator though, it is a very interesting usage but, considering the article title “Brevity vs. Clarity”, it seems to be a lot more on the Brevity side :-).
5 Jan 2007
Silvio, that brings up a good point because the && is very clear to me … because I’m used to it. While there are certainly rules of thumb for clarity, sometimes it’s very subjective. It’s difficult (and important) to strike a brevity/clarity balance that fits for your expected audience.
5 Jan 2007
Silvio, as Dave said, familiarity aids readability. I’m curious, though: how would you rewrite the “hash[:here] && hash:here” example? Would you really use a full-blown if-statement?
Too much syntax can be just as bad as too little for readability. You need to strike a good balance.
5 Jan 2007
The && property we’re talking about here is called Short circuit evaluation. If someone wants to do some more reading.
5 Jan 2007
I have to say that I rarely use the ternary operator. I find that almost always it breaks the flow reading of a piece of code. I always have to read it twice to find out if the ? is a ternary operator or a ? at the end of a query method.
Of course in ruby an if statement has a value so you can do:
element = if hash[:here] then hash:here else nil end increment = if x < y then 1 else -1 end
which with syntax highlighting is infinitely more readable than the ternary (IMHO). The presence of the ‘if’ immediately tells you that it is a conditional expression, rather than having to work out if the ? is a query or not. Sick example:
element = arr.empty? ? “empty” : arr.first element = if arr.empty? then “empty” else arr.first end
6 Jan 2007
Let’s try that again with line breaks (comment systems – grrrrr)
element = if hash[:here] then hash:here else nil end
increment = if x < y then 1 else -1 end
and
element = arr.empty? ? “empty” : arr.first
vs.
element = if arr.empty? then “empty” else arr.first end
6 Jan 2007
Bah. You’re right Jamis. Blast ! I hereby find myself guilty of excessive ternary usage.
Please keep up these TADFALAICKIU articles… they are brilliant.
6 Jan 2007
value = record.something rescue nil
6 Jan 2007
Jamis and Dave, you are right about the familiarity, being fairly new to Ruby and coming from a C/Java background, the && operator always sounds like it will return a boolean (or 1/0) back. It will take me a while to get used to this.
Answering your question, I would stick with the ternary operator as, for me, it is concise and explicit. Alain solution also sounds very sweet, though it is a little generic as any raised exception would be caught by the rescue, if believe.
6 Jan 2007
Isn’t it better to use exceptions for exceptional conditions, rather than flow control? I guess it depends on whether you expect record.something to have a value or not.
6 Jan 2007
John, I’m with you. If you expect that
record
may well not respond to thesomething
method, usingrescue
to handle that case is a bad idea. Exceptions are for exceptional conditions, those cases that shouldn’t occur, but might. Also, as Silvio pointed out, using rescue in that case can hide a host of unexpected exceptions, and introduce bugs in your code.6 Jan 2007
Why not use the statement modifier syntax?
That last one could even read like this, which I believe is more descriptive:
7 Jan 2007
What? You don’t like the ternary operator? Back when I was a n00b coder, I loved it ;)
7 Jan 2007
Daniel, you’re right, that will implicitly set the variable to nil if the condition is false…but in general I shy away from the implicit assignment of local variables. Feels like too much magic, but that’s just my preference.
7 Jan 2007
Another way to do the hash:
element = hash.fetch(:here, {})[:there]
Not quite as clean as some already mentioned, but thought I’d throw it out there.
8 Jan 2007
Jamis, a great series, and interesting discussion on this point. A few thoughts…
1. I tend to agree with you and John about reserving the
rescue
usage for true exceptions.2. A couple co-workers and I discussed this post. We have been in the habit of using the if modifier proposed by Daniel above, but after reading your comment, it dawned on me that the implicit nil value we were relying on is really an implicit non-assignment (rather than assignment to nil), which could also quickly kill the ‘magic’ you alluded to. Consider:
element = 'something' # (later)... element = hash[:here][:there] if hash[:here]
In this case,
element
is still'something'
. We generally intended it to be nil after the assignment (unless we really meant “only run this assignment ‘if’, otherwise keep any previous value”). So we decided to reserve our use of the if qualifier for circumstances where we really mean “only run this logic if __”, and use your nifty && usage if we really mean “make an assignment/reassignment and use nil if you can’t get to the value”.3. The if/then/else did feel better to me than the multiline ternary, particularly in your example where it is wrapped thinly in the method. For some reason though, a direct assignment in context (not in a method as above…which often is a better idea) “feels” better as a three line ternary:
url_i_want_to_use = @project ? project_overview_url(options.merge(:project_id => @project.id)) : overview_url(options)
Putting the
url_i_want =
assignment explicitly in each branch seemed decidedly un-DRY, yet assigning it to the return of a longer if statement actually seemed to lose some of the clarity in the ternary, albeit a 3 line ternary.10 Jan 2007
Oops, code did not format as expected, but think you know what I meant (retry…)
url_i_want_to_use = @project ? <br/> project_overview_url(options.merge(:project_id => @project.id)) : <br/> overview_url(options)
10 Jan 2007