Combinations of if/else can seriously damage readability and maintainability.
I (all the time) face verbose code that pretend to be readable and that requires lots of attention just to find out you are setting a single variable across 10-20 lines.
Example:
if (cond1)
{
myVar = 1;
}
else if (cond2)
{
if (cond3)
{
myVar = 10;
}
else
{
myVar = 100;
}
...
}
else
{
myVar = defaultValue;
}
* huge number of lines* not obvious (until we read all) that we setup a single variable
* combination of conditions difficult to trace for each given value
* high risk of forgetting cases...
Instead of
myVar =
(cond1) ? 1 : // comment case blabla
(cond2) && (cond3) ? 10 : // comment case bla
(cond2) && (!cond3) ? 100 : // comment case blablabla
...
defaultValue; // comment default
* one line per given value* operation on a single variable obvious
* match between conditions and values obvious
* high visibility on all the cases
* even more readable with proper vertical alignment
* a comment can be added on each line to document the case
[sorry for the multiple edits: I had a hard time getting { } properly display, they ate up the new lines]
if(cond1){
myVar = 1;
}
else if(cond2 && cond3){
myVar = 10;
}
else if(cond2 && !cond3){
myVar = 100;
}
else {
myVar = 4;
}
But when I encounter this kind of situations I have other problems than code formatting anyway:with 3 conditions you have 2^3 possibilities to check: are you really really sure (!cond1 && !cond2 && cond3) should give you defaultValue ? Do you really want 1 even if !cond2 ? etc...
When possible these conditions should be avoided in the first place (depending on context of course)
[0] https://stackoverflow.com/questions/14426366/what-is-an-idio...
if cond1 then
myVar = 1
else if cond2 and cond3 then
myVar = 10
else if cond2 and not cond3 then
myVar = 100
else
myVar = 4
end if
The VB.Net style also makes it easier to identify mismatched blocks because the "enders" are named differently. (One can also put the assignment on the same line as the conditional, but I generally don't recommend it.)Some may complain it's slightly verbose, but few if any will claim it's not clear.
myVar =
cond do
cond1 -> 1
cond2 && cond3 -> 10
cond2 && !cond3 -> 100
true -> 4
end
ML languages have similar constructs.That’s a problem. One I’ve seen all too often, but a problem, nonetheless.
myVar =
(!cond1) && (!cond2) && (!cond3) ? myValue_0 : // case 0 0 0 blabla
(!cond1) && (!cond2) && (cond3) ? myValue_1 : // case 0 0 1 bla
(!cond1) && (cond2) && (!cond3) ? myValue_2 : // case 0 1 0 blablabla
(!cond1) && (cond2) && (cond3) ? myValue_3 : // case 0 1 1
(cond1) && (!cond2) && (!cond3) ? myValue_4 : // case 1 0 0
(cond1) && (!cond2) && (cond3) ? myValue_5 : // case 1 0 1
(cond1) && (cond2) && (!cond3) ? myValue_6 : // case 1 1 0
(cond1) && (cond2) && (cond3) ? myValue_7 : // case 1 1 1
defaultValue; // uninteresting default (null, -1...)In case of more complex data generation you could use supplier pattern and combine it with strategy.
No need for any if else statements at all.
Sure it requires a lot more of work but is future proof and clear.
Foo(int x){return x+1} becomes Foo(Supplier x){return x() + 1}
Thats my cause.
(Does not make me any less right)
(You might consider reflecting on your tone and message. It presupposes bad faith on the other party, is dismissive and condescending, and you are probably less right than you think you are.)
It sounds interesting, that was all.
Yes, that's why for selections we usually use switch/case. But of course every programmer worth his money just knows that ?: is right associative and that '&&' is on precedence level 11 while '?:' is on 13. If that's not "clever code", I don't know what is...
I would hope every programmer worth their money are very comfortable with it, because chaining like this is a very common syntactic pattern in other people's code:
cond1 ? val1 : cond2 ? val2 : cond3 ? val3 : other
It is made less readable if you turn it into a right-nested mess of parentheses, so that is rarely seen.Just as nobody thinks of 'if..else' as right associative, but it actually is, and everyone uses that fact without thinking about it.
Same for '?:'. The associativity is just a formality that nobody thinks about.
> '&&' is on precedence level 11 while '?:' is on 13
On this I agree. I routinely put parentheses around complex conditions on the left side of ternaries for this reason.
I do remember the precedence, but I think leaving the parentheses out can raise a little doubt in the reader's mind, because it is common to use logical operators for control flow in languages like JavaScript, and control flow is at the same conceptual level as '?:'.
let x =
if cond1 { expr1 }
else if cond2 && cond3 { expr2 }
else { expr3 };
It’s more verbose this way (‘?’ Vs ‘else if’) but there’s no question of readability because it’s just if/else. You can format it however you like, and add statements into the blocks later if you need to, too.Rust also has the match statement, which is cleaner whenever your conditions are mutually exclusive.
Some would say top-level blocks returning the last value in the block is an anti-pattern, because functions which aren't meant to return a value end up leaking the value of the last thing called in the function, which might be another function, which called another function. Or it might be in various branches of an 'if', which aren't being examined for being an acceptable return value.
Perl does this, (like ECMAScript's 'do'), and while it's usefully concise sometimes, for API-level functions I think it's poor to accidentally leak values to the caller, that should never escape. The safe way to deal with this is an explicit void return at the end of API functions, but that's ugly and hard to remember.
I think JavaScript made the right choice in requiring explicit return from functions with blocks to return a value, with 'undefined' returned if nothing explicit is. Accidents are avoided.
Rust has taken an interesting approach of requiring a return type to be specified, which stops accidental leaks at least. Respect.
Rust will also only return the last expression in a block if it doesn't end in a semicolon. This can be a bit subtle when you're reading a long function, but combined with explicitly specified return types its hard to mess up while writing code. Because of the choice about that semicolon, its an explicit choice whether you want a block to evaluate into that expression or not. And for functions you can always just use an explicit return statement if you want anyway.
This will ignore the last result and return Nil instead:
sub foo ( $_ --> Nil ) {…}
This only works with literals, constants and Nil.If you specify a type instead, it only enforces that the result is of that type.
Keep in mind this is only a stage 0 proposal, and thus is not really part of the language. This is how statements behave when entered into the repl, e.g. the browser dev console.
Ruby has an if/else structure like the Rust/Lisp thing, as well. Tasty.
1. It's rare, so not many programmers have the pattern matching built up to read it easily.
2. Chained if/else statements have the benefit of indentations helping to show structure. The moment you add newlines to help show structure, a chain of ?:?:?:'s becomes much easier to read.
Most ?: expressions I see are uses inline, such as foo(a ? b : c). When you do that, you sometimes have to mentally unwind "ok so if a... what's a? why a? ok, so if a, then foo(b), else foo(c)". Putting the if(a) up front means you're already thinking about it by the time you get to the function call. So I reserve using ?: for when the difference between the two outcomes is minimal.
If it's very common where you work, then run, don't walk, away...
If you haven't encountered it often enough that it's familiar, than I think you probably don't read much code outside a small bubble.
The Linux kernel has another set of rules that differ in numerous ways (e.g. not wrapping single-line statements under a conditional in braces). Their rules are not strictly better or worse, just different and define their set of common idioms.
Another thing that the MD said to me during my interview was "code ought to be boring, testing can be interesting".
Besides switch/case do not apply to a mix of true/false conditions but to the different values of a single variable.
Funny :-) Thanks.
If you chain or nest them.
> Funny :-)
I'm glad I could brighten your day a little bit.
This language-design question is essentially a solved problem, but few new languages (outside the pure-ish functional ones) make the effort.
Trivial example in OCaml:
let imply v = match v with
(true,x) -> x
| (false,x) -> true;;
[0] https://caml.inria.fr/pub/docs/oreilly-book/html/book-ora016... int logic()
{
if (cond1)
{
return 1;
}
if (cond2)
{
if (cond3)
{
return 10;
}
return 100;
}
// ...
return defaultValue;
}
Since it's a function, it's also apparent that you are only touching one variable: myVar = logic();
I love the ternary operator, but I find it's harder to step it in a debugger though.Traditional if/else conditionals express clarity by embedding the decision for myVar and visually show how it would be assigned in various cases.
As @hotBacteria said:
if(cond1){
myVar = 1;
} else if(cond2 && cond3){
myVar = 10;
} else if(cond2 && !cond3){
myVar = 100;
} else {
myVar = 4;
}
// Now return myVar
return myVar;
I’ll take this syntax over short circuiting any day.One liners and mammoth tertiaries that are too long can lead to conflicts and more problematic merges/diffs/etc.
One operation per line makes repos and merging, as well as blaming, reading and reviewing, easier to parse.
Style is a matter of taste, just like writing English: there are synonyms and idioms for every idea that you want to express, so use that freedom to choose the clearest communication. Like English, programming code is meant to communicate to other humans, so don't artificially limit your vocabulary in the name of consistency.
Consistency is a hobgoblin.
A foolish consistency is the hobgoblin of little minds.
The whole quote is important. Cutting parts out changes the meaning of the whole thing.
switch(true) {
cond1: return 1;
cond2 && cond3: return 10;
//etc
}
Not sure I'd recommend it but it works in a few languages :)Elixir has cond that does this nicely without resorting to strange switch statements:
https://elixir-lang.org/getting-started/case-cond-and-if.htm...
const pick = (param1, ...) => {
if (cond1) return 1;
if (cond2) return 10;
return 100;
};Or sometimes for multiple condition branches I use bitwise operators to create integer bit mask with couple of bits, then switch(), or array indexer, with all possible 4-8-16 values.
Your version is more error prone because contains more code than necessary. Note how it’s just “else” in original version, and manual negate statement, (cond2) && (!cond3), in your version. Easy to screw up when modifying the code at some later point. When you then need to replace cond3 with cond4, forget about second branch and the algorithm will break.
if a { v = 1 }
else if b && c { v = 10 }
else if b && !c { v = 100 }
else { v = d }The problem is that if one of the conditions changes, for example due to a variable renaming, you likely have to change the alignment on all lines.
Aside from being very tedious even if you're a lone coder, this creates noise in diffs that makes automatic merges fail much more frequently.
I mean, this whole thread reads like a holy war about code style - something the Go developers EXPLICITLY want to avoid because discussions about code style are a waste of time.
Everyone reading the above segment of code will have a different opinion on how to format it, while the real question should be "What does this code do". I mean this is highlighted in the preface of the presentation: "When you or I say that a foreign codebase is unreadable, what I think what we really mean is, I don’t understand it."
When I read this code I don't understand it, and it's not because of the formatting per se. Playing with the formatting does not make the purpose of the code clearer.
This whole thread where people argue about how to best format a structure is missing the point of the article completely.
?
This is not always possible, but when it is it's much easier to follow the flow of code and it reduces the number of lines of the condition tree.
enum situation { boring, clear, cleaver }
selector(context)
if context.proves(cond1)
return cleaver
if context.proves(cond2)
return clear
return boring
adjudicator(context)
switch( selector(context) )
cleaver: make_something_cleaver()
clear: make_something_clear()
default: make_something_stupid_simple()
Not completely the same situation, though, as there are no assignation here. For simple cases (`v = e0 ? e1 : e2`), ternary operator sure is fine, but for more complex cases, `some_var = selector(context)` tend to be a clearer path. That will also be better rendered in your API as the function fine documentation will have more chance to be extracted properly.Thus, it would be layed out like an else-if chain, but without the extra verbiage, particularly the repeated assignment.
(I can’t put in a proper example from my iPad, as it wants to capitalize all the lines, etc)
Disclaimer: Ruby was very influential to me in the mid 2000s, even if I never wrote any for pay. The if (else-if...) statement in Ruby works like a ternary chain in C based languages.
if(cond1) myVar = 1;
else if(cond2 && cond3) myVar = 10;
else if(cond2 && !cond3) myVar = 100;
else myVar = 4;Personally, I find that ternary example you posted to be terrible code. We are not using parchment anymore, there is nothing to be gained by saving a couple of lines.
It's not about the line count, it's about expressing the main idea clearly and succinctly, which this code does.
My nitpick would be with the language constructs themselves. The ternary operator is simply a crutch for lack of an if-then expression returning a value.
if a < b then -1 elsif a > b then 1 else 0
Or can add some white space to make the structure a little more clear: if a < b then
-1
elsif a > b then
1
else
0
end
Expressions like the ternary operator micro-optimize by replacing tokens recognizable as natural language words with arbitrary punctuation. But that's a language level complaint, which you generally don't have any control over once you've started the project or when editing existing code.It's far from "arbitrary" --- and I suspect that this dogmatic, misguided way of (not) thinking about it is responsible for the majority of the complaints and aversion, since as others have mentioned, it is completely equivalent in structure to if/else!
What do I mean by "far from arbitrary"? Well... what is the character used in basically all Latin (and even some widely-used non-Latin) languages to denote a question? I've already used that character twice in this post so far, and you should've found the previous two sentences to be pretty clear, so stop pretending the ternary operator is something scary and "unreadable" and see it for what it is: it is literally asking a question!
understand ? done() : keep_thinking();We ask questions with ? but we don't offer alternative answer to the question with :. If anything, maybe it should be ; or even .?
I disagree. I find no clarity in using nested ternaries, but I understand it's a matter of opinion.
x = if | a < b -> -1
| a > b -> 1
| otherwise -> 0
(and `cond` in lisps)Thats very subjective. As a beginner dev, the ternary series is almost un-understandable for me. While the if/else blocks are far easier to glance at and digest.
if a < b: x = -1
else if a > b: x = 1
else: x = 0
Which a lot of people seem to hate for some reason, but I always appreciated the compromise between terseness and clarity. x = (-1 if a < b else
1 if a > b else
0)
This emphasizes the possible values that x may be assigned to.This is not what the OP is talking about - we are not using parchment anymore, but we still have the same brains, being able to fit more code in our head with less i/o allows is a more holistic view.
There is a balance in between terse and verbose that produces a reasonable length of code for what it is doing, and that is subjective, to the individual and the code - so you are never going to agree on everything. But I say fuck no to people who want to lower everything to the common denominator, that is mediocrity, I don't want my grandma to be able to read the code if it makes macro comprehension horrible to the point of making it unmaintainable spaghetti.
When I first went from Java to Scala, I found that while you can perform the same logic in 10 times less lines of code, each line is also 10 times as powerful and complex. A random example I found in code from back then:
private def fetchDevices = (for (device <- devices.find()) yield device.getAs[String]("deviceId").get).toList
Super concise but at the same time the code doesn't actually do much as far as I can decode now, it's a lot of type wrangling.Yup, and you will find bad examples at both ends of the spectrum. And sometimes it doesn't even have anything to do with terse vs verbose, it's just bad code, not because of that one part of code but because of the context requiring it - just like in your example, I don't know scala, but if it's doing a lot of "type wrangling" then the real problem is probably further upstream from that point in the code, and no amount of terseness or verbosity is going to make it more clear - we are only discussing one dimension of the many subjective and objective dimensions that make code clear or confusing.
On that same vein, if a ternary operation is going to throw someone for a loop, I've got some bad news about their career in programming.
Because when something is expressed in 10 lines explicitly, generally you can simply skim over the code to get an idea of what's going on without having to go into the details.
"Smart and clever" code, like using nested ternaries, demands you to focus and try to figure out what is going on.
Which is why important details get missed when reviewing verbose code.
>On that same vein, if a ternary operation is going to throw someone for a loop, I've got some bad news about their career in programming.
You're right about someone having and issue if they can't understand this on it's own. What was the last application you worked in where that single line is all you had to understand to accomplish whatever task you were working on?
Exactly.
The bigger constructs expressed in functions, classes, etc, are where the attention should really be.
The smaller ideas expressed in single lines of code are, should be, insignificant in terms of cognitive load.
There are some exceptions to this, in particular very high performant code. We all know that "premature optimization...".
It's a question of trade offs but you cant entirely ignore one or the other
You don't read ten lines, you decode them. If it's ten simple lines, decoding them is easy. If it's a nested ternary, it's a lot more cognitive load - unless you're used to nested ternary expressions.
However, I'd say it's a fair assertion to make that most people aren't used to reading nested ternaries.
And I think that's what Go (and this presentation) is about; you shouldn't need to get used to a certain code style to be able to decode it. You should be able to open up a file and not be surprised. If I were to come across a nested ternary, my first reaction is a raised eyebrow and a "wtf?". The wtfs / hour is one of the metrics that Go language is based on.
Parchment, no, but do you not value vertical terseness when evaluating a function for code clarity?
That pattern is the expression equivalent of if..; elsif..; elsif..; elsif...
Writing it out in long form using actual if statements doesn't add much clarity, and costs in verbosity, as the OP says, macro-clarity versus micro-clarity.
I agree with the sibling to this comment, though, that a ternary is more readable with newlines and indentation:
return (a < b ? -1
: a > b ? 1
: 0);
That's the style I use, except for extremely short and ternaries where the verbosity adds nothing, like (A > B ? A : B).In my view it becomes more complex to understand when there's a ternary inside the first branch, because then it's equivalent to if...(if...else...)...else... At that point I'd consider using if statements, if there is no reason to stay with an expression.
I think it's even clearer like so:
return a < b ? -1:
a > b ? 1:
0;
Conditions/guards on the left, values on the right. So I emphatically disagree with the OP that nested ternary expressions are never ok. In most sane languages with proper precedence and evaluation order, they work great when formatted as above. I say sane languages, because the nested ternary doesn't work the way you'd expect in JavaScript...I use the parantheses the way I do, because editors will auto-indent the code that way. In other words if I press <tab> in Emacs, things won't move around in my example, so I know it's indented properly.
However, within that style, some people do prefer to put the operator at the end of a continuation line, and some prefer it at the beginning.
Is this the lowest level of decision making in something like a large drawing application, or some CAD or financial package, then for the love of God, take the concise approach ! If you consistently take the longer approach, may God provide mercy on your soul (and a very large monitor) when you get to vector multiplication or matrix math.
If you're writing 3 business rules in something that's important and needs reliability and therefore should not have complexity ? Then it might be better to write it out.
(in both cases, because of the potential for stupid mistakes, I'd add tests)
But there's no single solution for all situations. High complexity software ? Concise will help out more. Low complexity software ? Write it out for clarity.
I'd give you multiple upvotes if I could for this.
Absolutely agree with all your points.
I'd add further, that if it's 3 important business rules, then sometimes expanding it further to have well-named functions and well-named variables is well worth doing, even if the business rules are trivial logic:
// This rule was recommended by the accountant on 2019-05-06
// and must be reviewed by the CFO before release.
function receipt_needs_itemised_tax_record(amount: Money): bool {
return amount >= 1.00;
}
// Show itemised tax records on receipts that need it.
if (receipt_needs_itemised_tax_record(receipt.total_paid)) {
...
}
Versus: // Writing this and other low-level code in 25 lines per function
// does not make the 10kloc rendering library easier to understand.
function transform_pixel(bg: RGB, fg: RGB, opacity): RGB {
opacity = clamp(opacity, 0.0, 1.0);
let blend_bg = 1.0 - opacity, blend_fg = opacity;
return RGB { r: clamp_rgb(bg.r * blend_bg + fg.r * blend_fg),
g: clamp_rgb(bg.g * blend_bg + fg.g * blend_fg),
b: clamp_rgb(bg.b * blend_bg + fg.b * blend_fg) };
}When they are, you basically get something that looks visually like a decision tree. This can read really nicely in situations where declarative style code fits better - for example embedding nested ternarys in JSX is quite a popular pattern for this reason.
The caveat is, you have to be 'used to' reading the ? and : symbols and instantly mapping them to if/else, but I certainly don't think getting used to this in a short time frame is beyond expectation for someone who's not already, i.e. new team members etc.
Six-months-ago-myself thought so as well. Turns out six-months-ago-myself is a goddamn moron.
It's a balancing act which I have found to be one of the more fulfilling aspects of my work. Having people of different skill levels review my code has made this much easier.
One thing that I do a lot though is format code in a way that certain patterns appear for those of us that like to skim code. This generally means avoiding ternary operations except for very simple things where that pattern is easily recognized. But when to do that depends on the team(s) involved.
But as I say above, I think in certain situations they can be more readable for everyone.
If they don't then they need to learn.
You can't say don't use specific language features because some devs can't be bothered to learn them.
“You can't say don't use specific language features because some devs can't be bothered to learn them.”
Would you say the same about C++? In my view it’s a good practice to use only a subset of a language consistently and not use all features. I have seen JavaScript code where I had to do research for half an hour before I could figure out what it really meant.
My view on this has flip flopped several times during my career. I think every spec has some dusty corners that most people don’t know about, but when a situation calls for it knowing about some obscure features can make your code far more readable. Sometimes it’s better in the long run to train your team about a feature they might not know about rather than code for the lowest common denominator. This thread is a great example - I spent my first decade of programming scared of chained terneries. One day I spent a couple of hours goofing around with them, playing with different ways I could write my code and internalising their semantics. Now they seem fine. I wish I’d taken the time to do that years ago. Ten years being afraid saved me 2 hours of time learning.
One of the best programmers I worked with reads the specs of tools he uses for fun. He says specs seem daunting but you can read them in far less time than you think and there’s always some fascinating stuff in there. My HTML knowledge got way better working with him - and its funny seeing how many tools struggle with correct HTML because the authors didn’t bother actually learning it.
Some more examples: html void elements, html/body tag auto insertion, JS tagged break/continue, C/JS/etc’s comma operator.
If something is so detailed that it gets too long to be quickly readable I extract it into a separate properly-named function.
I'm definitely drawn to using all the tricks of a language and I feel like it would be a great way to show off how smart I am, but I'm not sure its worth it for sacrificing comprehension and ease of adding stuff in later. Maybe if everyone that will ever work or use that code is top-notch, that would be a dream.
It's not whether some constructs are parts of the language (languages can have any crap in), it's whether some constructs are bad, confusing, and should be avoided (whether you know what they do or not).
Is there never going to be any level of depth in a ternary statement which you will think is perhaps too great, and want to switch to some more verbose syntax?
You could say that most programmers are crap, but those crap programmers seem to make the world work, so they must be doing something right :-)
Because they are
> but those crap programmers seem to make the world work
For certains value of "work". Most software is crap too and it's getting worse not better.
if (cond) {
var1 = x;
} else if (cond2) {
var2 = y;
} else {
var1 = z;
}
Did you notice the second block assigned to var2 instead of var1? Best case it’s a gotcha for anyone reading the code later. It might be a bug by the original author; we can’t tell at a glance if this behaviour was intentional. Ternaries remove this problem. The intent of the author is clearer, and (with proper formatting) there’s no gotchas for anyone reading the code later. var1 = if cond {
x
} else if cond2 {
y
} else {
z
}
Which in my opinion is strictly better than using a ternary, if the language supports it.My favorite part is that its easy to add extra statements into the conditional blocks if you want. Doing that with ternaries requires either using the comma operator, repeating the condition on another line or refactoring the whole expression back out into if-else chains. All of those options are bad.
When in Java, I've gotten into the habit of using the final initializer to mitigate these kinds of issues, like this:
final T var1;
if (cond) {
var1 = x;
} else if (cond2) {
var2 = y;
} else {
var1 = z;
}
In this specific case, the compiler will catch the lack of initialization in the second block. It's also very helpful with a complicated/nested conditionals to guarantee that you initialize the variable through every code path.Here's a blog post I'm becoming more and more persuaded by as time goes on:
https://medium.com/javascript-scene/nested-ternaries-are-gre...
return (
a < b ?
-1 :
a > b ?
1 :
0)But it doesn't mirror the if/else-if/else structure. Rather the mirrored semantics looks as follows:
if(a < b)
return -1;
else
if(a > b)
return 1;
else
return 0;
It's interesting that for all the claims in this thread about how obvious the ternary operator is, almost every single person got it wrong.Is there a functional difference in this example? No. There isn't. Is there a functional difference in any example? I don't think so, but I'm not 100% sure. And the nice thing about not being clever, I don't have to worry about it.
But why should that be the main thing to be concerned about and prioritise? In what way are they "misleading"? Is it really the state of the industry where a new person cannot learn a codebase, has no time or effort, and cannot read a single line of the language they were hired to work on, and that's the person we should build everything for?
Does it work like that in any other industry?
Yes, it does. Imagine you were a new underwriter at an insurance company and your predecessor used the shorthand nomenclature for every detail on the accounts you're taking over, and failed to write out any of the detailed reasoning behind why they chose to approve or deny claims. How much longer would it take for you to be able to fill their shoes than if they had been just a little bit more descriptive in their documents?
> "Programs must be written for people to read, and only incidentally for machines to execute." - Harold Abelson
Yet the software industry seems to have developed an illness of perception, that encourages the continual lowest-common-denominator dumbing-down of code and an aversion to learning. From what I've seen "collaboration" as a justification for this, and from the types of people who are proponents of it, it appears to be related to the effort of managers to attempt to turn developers into disposable and easily replaceable drones; something which everyone actually writing code should be against. In the trades, there's no lack of teamwork either, but the prevailing notion is "the least skilled/experienced learns from the most" --- only in software does there seem to be a strong counter-trend to that. Perhaps we should not encourage the degradation of our craft.
Maybe that quote should be modified slightly too: "Programs must be written for suitably qualified people to read"
Yes, but the jargon and language used is standardized; pilots don't get to put their own twist on e.g. formal communication with the tower when they come in to landing. You don't get to switch to idk, km/h when communicating your airspeed.
...so is the ternary operator.
I don't know what shorthand nomenclature of insurance underwriting is, but if it's a standard part of insurance underwriting then I would expect a new hire to be familiar with it, or need time to become familiar with it.
If it's something separate like actual Gregg or Pitman shorthand English which the one underwriter used personally, unrelated to underwriting skills, then I wouldn't compare that to a programming language's built-in ternary operators.
Which equals more overhead than is necessary. Elegance over accessibility is always more expensive, no matter the product. In my experience, folks who are obsessed with "perfect" code over readable code introduce the most long-lasting, destructive bugs, because, at best, nobody wants to and, at worst, nobody can decipher their stuff.
if (statement = ref) { print(statement) } else { print('have you done everything right?') }
was I wrong to be confused as to what he might have intended with statement = ref? Because he was in fact using the language functionalities to express what he wanted. I just thought it looked confusing and misleading.
test.c:3:17: warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
if (statement = ref) {
~~~~~~~~~~^~~~~
test.c:3:17: note: place parentheses around the assignment to silence this warning
if (statement = ref) {
^
( )
test.c:3:17: note: use '==' to turn this assignment into an equality comparison
if (statement = ref) {
^
==Seasoned 10x developers, titans of the industry, and all agree on simplicity and even they frequently make mistakes on easily confusable constructs such as nested ternaries (or even BS like forgetting a break in a switch etc).
It's not some bad "state of the industry" that calls for no such constructs.
If anything, it's amateurs and newbs, more self-confident than they should be (and less humble) that call for using any old crap in a language, and think that such constructs are only problematic if developers are "not paying attention" or are "ignorant".
As an extreme example, people familiar with mathematical notation have a hard time when they start using a programming language that uses '=' for assignment, because they intuitively understand it as asserting equality, which it isn't (x = x + 1 would be paradoxical). And yet we all here presumably think of assignment as something rather simple.
There are plenty of code bases where the ternary operator is used frequently. It's a more succinct (but less versatile) variant of the if/then/else expression that a number of languages have - do you find that confusing when nested?
The ternary operator is just as simple as if/then/else expressions. You may not be familiar with it, which can make it seem non-simple, but then we're just back to the observation that simplicity isn't entirely objective but subject to your background.
From my experience, and from my reading on tons of books, code, clean code suggestions, bug reports, etc, patterns of bugs are more commonly found on such code bits, that sacrifice clarity for succinctness or "clever points" (in fact that's the very premise of TFA).
Most respected elders in development also warn against those types of code.
Lacking objective research, that's the best we can get, and I'd say better than "it works for me".
It's much easier to go through a codebase if everyone uses the same code style and you get the least amount of surprises. This does however require you to give up your own ego, that is, you have to conform to the standard.
For better or worse, I operate mainly in the PHP world and the recently added spaceship operator is a solution to this. While I think it's a crazy operator on the one hand, on the other it's much simpler to use. https://www.php.net/manual/en/migration70.new-features.php#m...
return a <=> b;
EDIT: fixing typosThe 10(!) lines of code you referenced are actually only 8(!) lines compared to your 1(!) line because you included the function declaration and the final brace in your count. The line you proposed is 34(!!) characters wide. I wonder if your statement will continue to grow past 80(!!!) characters as your operand expressions grow in width? Will you transform it back to multi-line when that happens?
However, I'm not sure why we're exclaiming line counts at all and now, apparently, character counts.
Agreed on the semi-colons though.
return
(a < b) ? -1 : // a smaller
(a > b) ? 1 : // a greater
0; // equal>and it's dramatically more compact
Why do I want that?
You need to practice it to overcome your skepticism resulting from your ingrained habits that prejudice you against it, not because it's inherently unreadable. It literally takes 5 seconds to understand the idiom: conditions/guards on the left, value on the right. It's essentially a truth table.
> Why do I want that?
To add to the other poster: the more context you can fit on your screen, the less scrolling, jumping you need to do to understand a program's behaviour. Compactness that doesn't sacrifice readability speaks for itself. The code sample we're discussing is compact and very readable.
I understand how ternary operators work. It's still hard to read if you inline multiple ternary operators the way OP suggested is easy. It adds cognitive complexity, and hides bugs because your brain will fill the details on what it assumes it does, versus the subtleties of what it actually does.
In fact, it is obviously so confusing that to make it work someone suggested the introduction of white-space and multiple lines, as follows:
return
(a < b) ? -1 : // a smaller
(a > b) ? 1 : // a greater
0; // equal
And they still got it subtly wrong, because the semantics of their 'fix' makes it seem like it is the equivalent of: if(a < b)
return -1;
else if(a > b)
return 1;
else
return 0;
which isn't quite true. It's actually: if(a < b)
return -1;
else
if(a > b)
return 1;
else
return 0;
Will this make a difference in this case? No - but there is a subtle semantic difference that you have to stop to consider when you're scanning this code.I disagree. The ternary version is much easier to read than your if-else. If-else statements can feature compound statements and side-effects where the ternary version is simpler because it only returns a value. There are fewer corner cases to consider.
> It adds cognitive complexity, and hides bugs because your brain will fill the details on what it assumes it does, versus the subtleties of what it actually does.
It adds no complexity. It's significantly easier to understand than if-else.
> which isn't quite true. It's actually:
Right, there's literally no semantic difference between those two.
> No - but there is a subtle semantic difference that you have to stop to consider when you're scanning this code.
You really don't. There's nothing special to consider, no corner cases. It's literally condition-on-left-value-on-right.
There's a natural trade-off in the sense that in order to make something more compact, you have to rely on the context to provide whatever information you remove. For example, the ternary pattern of chaining ?:'s. You have to be used to it. However, once you know it, the more compact pattern works fairly well.
It should be. The C ternary is just an expression-oriented version of if/elseif/else.
Languages with a functional bend simply make if/elseif/else an expression in the first place e.g. in Rust it'd be
return
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
Though obviously that specific version is an overly complex way of writing: return a.cmp(b);We can regard the condition as the head of a clause, and the ? and : as heads of sub-clauses:
A ? B
: C
which leads to: return a < b ? -1
: a > b ? 1
: 0; if (a < b) {
return -1;
} else {
if (a > b) {
return 1;
} else {
return 0;
}
} if (a < b) {
return -1;
} else
if (a > b) {
return 1;
} else {
return 0;
}
Unlike your perfectly formatted code there, this has a bit of a visual problem: a one-liner consequent is fully braced, whereas a multi-line alternative isn't.Ternary operators are different. They have confusing nesting and do not support the equivalent of the "if/else ladder" pattern very well.
The nesting is irrelevant to understanding the semantics, so this objection doesn't fly. The idiom is basically just a truth table with conditions on the left and the matching value on the right. You read it left-to-right, top-to-bottom, just like all other code. The first condition on the left that matches returns the value on right-hand side.
The nesting is absolutely relevant to producing the semantics.
The following uses deceptive whitespace to suggest a nesting that is contrary to the actual nesting, interfering with understanding:
if (foo)
if (bar)
xyzzy();
else
flop();
The ternary operator A ? B : C is the goofy invention of demented mind. In nested situations, it is mind-bendingly unreadable. It behooves us to style it in a way that reveals the abstract syntax tree structure.In C, I would in fact recommend:
#define if3(a, b, c) ((a) ? (c) : (c))
return if3(a < b, -1, if3(a > b, 1, 0));
Now we can have it in one line, yet it's clear.Fortunately, the ternary pattern doesn't exhibit this problem. So again, this objection doesn't fly.
This depends on the right-to-left associativity of the ternary operator, so this definitely counts as 'clever'.
If you don't put the same type on each return sides of your ternary, you might be in for a nasty surprise.
Definitely true for JavaScript and PHP. I am always surprised what expressions can evaluate to true or false.
Now consider this:
return a > b ? 1 : a < b ? -1 : 0;
Or: return a == b ? 0 : a < b ? -1 : 1;
Or: return b < a ? 1 : a == b ? 0 : -1;
Are all these equivalent?Figuring that out is much more difficult than their 8-line counterpart, at least for me. My eyes have to keep jumping from left to right in order to do the checks, like they would do in a piece of uglyfied javascript.
That said, context is everything: if your one-liner was the single line in a function called `comp` with `a` and `b` as parameters, I would not comment on it in a code review.
However, if this line was part of a bigger function, and if instead of `a` and `b` we had `someLongVariableName` and `someOtherVariableName` I would not approve the pull request until this "oneliner" was changed into something else (possibly a call to the `comp` function).
But is that really what you're looking for? I see that idiom and I think, "Ok, this is a comparator" and I move on. If I suspect a _bug_ in the comparator, then I'll look at it more carefully, but the context is about writing code whose intent is clear, not writing code that can be scanned for bugs easily - which would be a tall to impossible order anyway and isn't made any easier by sprawling your code across 10 lines where one will do.
If I see it "on the wild" (as part of a bigger function), however, my train of thought goes "Wait, why is this piece of code here instead of a function call called `comp`? Is this a comparator or is this something else? Ok geez time to do the javascript eye dancing thing. You know what? I'm just not going to approve this and demand that this piece of code is at least given a proper name and put somewhere. Possibly with unit tests"
That's nowhere near clear. I frequently use a single level ternary, but this is an abomination and very easy to make a subtle error in.
YES!! And what do you think the statement
return a < b ? -1 : a > b ? 1 : 0;
looks like to many people? One big 'run-on sentence'. It took me a few moments to parse that line. I had a good idea what it was doing, but I had to do a double take to make sure there wasn't some subtle bug in that line.And that's the point here. In some sense, you are writing to the lowest common denominator. You're a clever guy who can optimize their code to minimize line count and terse statements are obvious to you, but your code will be touched and looked at by developers of all levels. Developers that will have to fix your bugs, and extend your code, or refactor it - years after you write it.
The standard for collaborative programming is to write code to be understood, not to show how clever you are, or to minimize character count or to minimize line count.
If I write clever code, I document it extensively on top of the clever code, with references if possible.
It's about the life cycle, maintainability and longevity of the code in the first place.
if (a == b) {
return 0;
} else {
return a < b ? -1 : 1;
}The key to clarity and maintainability for me, though, is in the structure. If the code is structured in manageable blocks with clear description on what each block does and how blocks fit together I care little whether within each function the code is concise or verbose.
As a data point, in larger projects with good structure, I generally saw more use concise code than verbose, which may account for my original "concise is better" bias. Just my 2c.
return a < b ? -1
: a > b ? 1
: 0;
Reads like a switch statement, yet not overly verbose. I can parse it even more easily than the one liner.Your point about "macro-readability" is spot on though. Nobody should care about the readability of an isolated piece of code. We should care about the readability of pieces of functionality and that generally requires understanding of a whole function, or even a whole class/module.
if (a < b) return -1;
if (a == b) return 0;
return 1;
Is simple boring code, has the advantage of looking odd when something odd happens.Although I've used a ternary operator or 'if' statement from time to time, in general I would like to model the boundrary conditions themselves and separate condition and action (reduces coupling and enhances the possibility of reporting what is going on).
In your case, the a < b statement is most likely part of the natural ordering of the type of whatever variable a and b are. So we should be much more interested in describing the natural ordering of this type, so we can say things like:
intList.sort(IntOrdering.reverse())
to sort a list of ints in reverse order, or intStringList.sort(IntOrdering.compose(StringOrdering))
to sort a list of tuples of ints and strings in natural ordering.So, creating this natural ordering needs to be done only once, over all the possible programs that can be expressed with ints. Maybe that one, single, time you'd be writing the actual expression you wrote above. But this lower-level logic will be well hidden by the abstractions used to reason at a higher level about correctness when placed orthogonally with other low-level code.
In other words, every time I write ternary expressions or long if-then-else expressions, I am either writing something low-level, or I need to abstract away.
return a < b ? -1 : a > b ? 1 : 0;
That's a nice approach. However the document is about Go, and Go doesn't have a ternary operator. So that probably leaves the switch solution as the clearest one. return -1 if a < b
return 1 if a > b
return 0 a<b ? -1 : a>b ? +1 : 0
# or maybe
a < b ?? -1 !! a > b ?? +1 !! 0 # perl6 style
# or
a < b then -1 else a > b then +1 else 0
not for verbosity vs succinctness but for the mnemonic that visually-smaller operators bind more tightly. if a == b { return 0 }
return a > b ? 1 : -1
Also it would be ~7 lines, not 10? if a > b {
return 1
} else if a < b {
return -1
} else {
return 0
}When the code is mostly calls to well-documented APIs, clarity is achievable (though not guaranteed.) if it is more complicated than that, I do not know that there is any way to achieve clarity other than to structure it, internally, in that style.
return a < b
? -1
: a > b
? 1
: 0;
If that is too much: return a < b
? -1
: a > b ? 1 : 0;
Other possibilities: return a < b ? -1
: a > b ? 1
: 0;
return a < b ? -1
: a > b ? 1 : 0;Anyone who has the experience of reading lots of highly trivial functions like those, and gotten the feeling "I've read a lot of code, and I understand what each little bit does, but what is the whole thing trying to do?" That's a symptom of excessive verbosity.
Smalltalkers sometimes solved this through executable comments. There would be some code, with a message to "debug this," and we could just highlight the code and quickly and easily walk through it in the debugger. This demonstrates one of the advantages and disadvantages of Smalltalk: Everything could be embodied in the interaction of relatively small objects, so everything tended to be modular, more easily. The disadvantage comes from the fact that the interaction has to be understood to understand the system as a whole.
My excuse for taking this approach in all languages is accessibility, with accessibility applying to people who can't really code. Whatever the language you can hack a few if statements even if you don't know the language. There are people who are learning and you want to make it 'accessible' for them.
Let the compiler do the work and stay off the bleeding edge features. That includes spaceship operators in PHP. I also think it is worth keeping code a few versions old, so in the case of the spaceship operator, I can do if/then/else things for the compiler to do the work and have my code work on someone else's box that is still on PHP 5.4 because the main software they use doesn't work on PHP 7.3.
Or in Javascript, it is obviously best practice to use the fancy new things but if your web page dies in the console on some Apple iPad running an old version of Safari then you can end up refactoring those best practice iterators into for loops. Invariably these refactoring exercises are hastily done, so you can come a cropper.
Another thing is that if I had to explain my code to someone then the nearer to natural language the better. The example ternary would require me to stop and think for a moment, if it was in boring if statements I could explain it to a not-so-technical person in terms they could understand, even if they could not write it themselves.
On the contrary, even a "not-so-technical person" is likely to be able to guess at the meaning of a question mark; and as a bonus, it's not specific to English either.
match a.cmp(&b) {
Ordering::Equal => 0,
Ordering::Greater => 1,
Ordering::Less => -1
}
It's easy to see what the possible outputs are and what is produced in each case. The compiler even checks it to make sure you didn't forget any situations.The clarity comes at the cost of some verbosity, but there's only 4 lines of functional code (not counting the closing brace) here compared to the article's 10. You'd get around the same number if you added line breaks to make the ternary example more readable.
When I've come across - or written - "clever" code in my career, it's always been about making something that normally wouldn't fit into memory - or perform slowly - viable. The examples of this are numerous and still happen plenty today on modern hardware (e.g. games, VMs, NaN tagging). There is nothing wrong with "clever" code so long as it's _clearly_ documented and abstracted away with various types, macros, functions, etc.
Then there's all other code. And what I feel the author wanted to say was that anyone coming along later shouldn't have to think twice about what a given line of code is doing.
If there's a bug, sometimes I'll be lucky enough to have a debugger, sometimes not. But, I shouldn't have to second guess a line of code to determine whether or not the bug is there. In your nested ternary expression, I would be second guessing all over the place; what's the precedence? Hell, whenever I use ternary operators I still force myself to parenthesize the conditional, because while operator precedence in C/C++ is well defined, it's not well known.
Clarity is another reason I prefer type safe languages over dynamic ones for critical code. It allows me to be a little more "expressive" with my code where the reader coming along after-the-fact can be assured that everything works out type-wise.
Having worked on code that people's lives depended on, there are times when getting it wrong isn't an option. I can only feel for the software engineers of the 737 MAX going back over gobs of code and trying to dissect what went wrong and come up with a fix. They'd be heavily scrutinizing every line of code, and I can promise that a nested ternary would be very scrutinized. But, so would a deep chain of if, elsif, elsif, ... and recursive functions (which are often a _big_ no-no in mission-critical code). Although, I don't think anyone here effectively argue that recursion is something to be considered "clever".
It's all about a code reader being able to _prove_ to themselves that the code does _exactly_ what it looks like it does and nothing more.
I'm not a Rust fanboy, but - just like static type checking - the borrow checker adds one more level of surety to the reader that another thread isn't coming along and stomping over the data. And this is without needing the whole context of the code base. This is a _big_ deal.
return a > b ? 1 : a < b ? -1 : 0;
return a < b ? -1 : a == b ? 0 : 1;
return a == b ? 0 : a < b ? 1 : -1;
return a > b ? 1 : a == b ? 0 : -1;
return a != b ? a > b ? -1 : 1 : 0;
return a != b ? a < b ? 1 : -1 : 0;
Are all equally valid ways of writing that code. You can get used to a convention that makes it seem more clear, but reading it they are all equally easy to decode.The less lines of code, the faster you can read a code base, apply fixes and have less bugs. Obviously you can take that too far and make code so dense no one can read it. It is finding a balance between the two. Comments can go a long way towards making complex code understandable within reason.
return a < b ? -1 : a > b;
This does the right thing, but removes the nested tertiary which to me is a big win.Or, if that's too many parens for you:
(mcond (< a b) -1 (> a b) 1 0)
The definition of MCOND is four lines of code and can be found here:
https://github.com/rongarret/ergolib/blob/master/core/ergoba...
In the end, what's simpler, spending 15 seconds looking at a one liner using well known language syntax, or digging through half a screen of if states, for loops (use iterators instead), for two examples.
```
(cond ((< a b) 1)
((> a b) -1)
(t 0))
```edit 1: formatting
If anything, it should remind you that clarity depends on the context.
It’s concise and simple to understand. Instead of returning 1 or -1 it returns a positive or negative value with I think is equally effective.
That will overflow if a & b are longs.
cmp() is deprecated because there is seldom reason to use it anymore.
E.G:
Sorting words according to their number of letter is just:
sorted(words, key=len)
You don't return 1, -1 or 0.If you want to rank participants of a game in a dictionary of scores using names as keys and points as values, you would to:
sorted(scores.items(), key=lambda score: score[1])
Again you don't return 1, -1 or 0, just the value that is significant. It makes it very, very simple to reason about.> If you really need the cmp() functionality, you could use the expression (a > b) - (a < b) as the equivalent for cmp(a, b)
I think the ternary operator is the sweet spot between too verbose and too confusing.
I mean, I get that your example is directly related to the point you are making in verbosity, I don’t even necessarily disagree with you, but your example isn’t very clear to me.
Yours is in the style of James Joyce where it's very dense...
the easiest to read is probably somewhere in between
Is this a joke? This comes off like an order of operations test.
Anyone who has the experience of reading lots of highly trivial functions like those, and gotten the feeling "I've read a lot of code, and I understand what each little bit does, but what is the whole thing trying to do?" That's a symptom of excessive verbosity.
To offer another (subjective) point, I think the lack of semicolons or other statement delimiters does make code harder to read, as it makes it look similar to a run-on sentence. Natural languages have punctuation for a similar reason --- you can quickly scan over sentences by finding their delimiters.
Also, the counterargument: https://www.linusakesson.net/programming/kernighans-lever/in...