Clear is better than clever [pdf]
dave.cheney.net
dave.cheney.net
return a < b ? -1 : a > b ? 1 : 0;
That's one line, compared with the 10(!) of his proposed method using a switch statement. Having worked with some "modern" codebases, I think verbosity is a bigger problem that's become especially prevalent recently, since the idea of making a simple thing simple is so enticing and easy, but emphasising "micro-readability" to that extent (that's 10 lines of code for two simple decisions) will bloat the code and make "macro-readability" harder, and it's the latter that really matters for the understanding of complex software.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...
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}
(Does not make me any less right)
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.
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.
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 :-)
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.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.
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?
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;
}
}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.
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.
I'd say it's a lot of fun.
Tangentially related for some of the younger devs: The predecessors on your project probably weren't idiots. It's possible, but they were likely working with a different set of requirements under a different scope. Even the code that it morphed into over time was probably a good enough decision for the constraints at the time.
At work myself i work mostly in isolation on codebases that have been in maintenance mode for years, where the original authors have typically either left the company or moved to another dept, and where there is almost no documentation. Everything is hacks and exploratory work, but it sure ain't fun. Adding relatively uninteresting small changes can take days or weeks.
Working on a broad and deep project as half of a pair is my preferred working mode so far in my career.
I'm lucky to be writing fun code at work - most of the time -, but the side projects keep me sane for many reasons. Not having to deal with 'bad' code that I didn't write, is also a nice benefit. All the bad code in my personal projects is mine, so no one else to blame :D
EDIT: typos and delete possibly negative statement
EDIT: I have deleted the original statement. After your comment it made me think that it might involuntarily scare people from sharing things in the open source world, which is not my intention.
But the author said "in my career," which I took to mean during the course of ones paid employment.
Mant things are left on the table. First of all, the audience. If you're explaining something to someone, you must consider the audience to be successful. So are we writing code to be read by Go experts? Go noobs? Software Engineers with general background in many lanaguages, but perhaps not go? Noob programmers? You want to assume the lowest level of understanding that is imaginable, and that's probably the last but one I listed in corporate environment.
Now that you have your audience, you gotta wrestle with some high level issues that then trickle down to concrete code. Explicit over implicit. Purity over mutable state. Small code units over monolithic code units. DRY. Verbose naming (that doesn't violate DRY). You gotta agree on the right level of code documentation - especially important for anything that resembles library code (or is an "abstraction").
The hard parts (like good naming) are not Lintable (of course Lint anything you can, use auto-formatter etc.), they are cultured through countless interactions between people in your organization. Getting third opinions is a valuable method here.
Because they're equivalent. There no uninitialized values in Go; everything not given an explicit initializer is zeroed out.
"So are we writing code to be read by Go experts? Go noobs? Software Engineers with general background in many lanaguages, but perhaps not go? Noob programmers?"
One of the nice things about Go not particularly appreciated by a lot of the HN community (as evidenced by a lot of the other comment threads) is precisely that code targeted to those various targets will not differ that much.
I also program in Perl 5 a lot, unfortunately, and to make it professionally palatable, I do indeed have to program in a dialect of Perl that has been chosen to be something that can be read even if you are not a Perl expert. I tend to avoid dragging in huge rewrites of the object system, for instance, in favor of the built-in support, which I can reasonably expect everyone to know. I avoid using autovivification, etc. I often see code written by a Perl expert in the code base, and have at times even torn it apart and put it back in the LCD dialect we locally use, just so we don't have this sudden, confusing chunk of code. The code is messy for many of the other reasons I've come to dislike large dynamic-language codebases, but you won't encounter $% += $(-/\#/[[]]/l; in our code base.
In Go, there's not much need to cut back to a subset of the language. There's only a handful of constructs you need to avoid, and it's the same code for almost every level. The "programmer who only knows half of Go" isn't that big a deal, because anyone who knows half of Go can finish the rest of it in a couple of hours.
I value this professionally. I understand why people who seek other things from their programming do not, because I also have times I seek those things. My personal codebase is much more mixed. But when I'm being paid to produce code, I'm not being paid to feel like I'm really clever for stringing one line together that downloads a web page, parses it through an HTML parser, and strips out all the text, returning the text nodes as a stream of strings or a concatenated string or whatever. Yes, I can do that, in several languages, but except in certain specialized circumstances, that is low quality professional code, even if it "works", because when I expect someone with two years of experience to make a change to that code base, I'd like them to be able to do it in a reasonable period of time, and with a reasonable understanding of the consequences and tradeoffs, not as a modification of an incantation.
(Every time someone tells some story about the one guy who has been around a long time and knows all the things and is the only one who can make any changes to any production system because it's all his code and only he can understand it because it's all crazy nonsense to anyone else, and all the schedules are busted because even he no longer can keep up with the mess, we all have a good laugh and condemn him. But when we get told that in order to avoid that, we may have to not write the cleverest possible code that we can, and that we need to prefer simple, effective code that works, and is easy to understand, a lot of us get all offended at the infringement of our rights to cleverness. Well... it's the same thing, just two different angles.)
> Because they're equivalent. There no uninitialized values in Go; everything not given an explicit initializer is zeroed out.
Using equivalence is often regarded as less clean.
Because it relies on the reader to be familiar with language-specific quirks. Even if they are, it adds a touch of cognitive overhead until it's become habitual.
In a language like Go, I think the performance will be identical in both cases because it has a basic data flow optimiser.
I program a lot in Perl5 too, and like you, I tend to favour the built-in support for things. I also really recommend the "experimental" function signatures, which make it read a lot more like other languages, and removes boilerplate. However, for better or worse, performance matters in some things I write, and Perl5's optimiser doesn't bother with much dataflow optimisation (improving the interpreter seems to have stalled for decades). So when performance matters, I do write things like 'my $x;' instead of 'my $x = undef;', relying on familiar equivalences. I would prefer the interpreter made them identical so I could state the intent more clearly without penalty, especially because performance-sensitive code tends to be difficult algorithms where clarity is more important.
Is it:
* Clear about what the machine is doing?
* Clear about what the possible error cases are?
* Clear about the purpose of the code?
Go often prioritizes making it clear what the machine is doing over making the purpose of the code clear. In some languages you might write `newList = map(myFunc, myList)`. This is clear about what I intend, but not how it is accomplished[0]. The equivalent for-range loop in Go is clear about what it does at the expense of diluting the intent.
[0] for example, that map function could apply to the first element first, or the last element first, or it could apply to all the elements in parallel.
In my experience, loops and recursion are lower level constructs that should ideally be used only in libraries (map, filter, limit, sum, distinct, etc [1]) and in hot code paths (for optimization).
Kevlin Henney makes a good observation on one of his talks about how "you have written all the loops you will never need in your life" [2], and those are constructs such as map/filterand friends, present in LINQ, Java Streams, Ruby Enumerables, etc.
His argument is that code made with those primitive is clearer than code made than procedural loops, because when you read it you don't have to "execute the code in your head".
I'd argue that using procedural loops is what's actually "too clever" here, despite being more verbose!
-
[1] I have mixed feelings about "Reduce". It is hard to use and, most of the time, hard to read as well. It's too low-level.
Fair enough. I'd certainly always prefer a special case like `.sum()` over something like `.reduce((a, b) => a + b, 0)` where applicable.
I don't think reduce is overly complicated, but it's not much of a step up over a for loop and can be abused to write code that's more confusing to reason about than an equivalent loop.
However, I've never seen a convincing and objective description of clear code.
Sometimes people mean that clear code is verbose code.
Sometimes people mean that clear code is uniformly formatted code. [For best results we should manually format the code the same. This builds character. Automatically formatting will not be considered.]
Similarly, sometimes people mean code smells are unfamiliar code. Or that code smells are patterns that they misused once and it bit them.
Objective methods of describing the ability for people to comprehend code when all else is equal do not exist as far as I have been able to see. The best we have is cyclomatic complexity, but there's some reason to believe that line count may be a better indicator (which can't be good). And cyclomatic complexity completely misses the effect of mutable or immutable state, the presence of bad APIs, poor variable naming, etc.
> However, I've never seen a convincing and objective description of clear code.
I take "clever code" to mean "code that looks pretty on surface the but requires the reader to dig in in order to understand the intent".
The problem with clever code is that it is misleading. It lacks empathy. You assume the next programmer will understand it at first glance just because it looks cool or pretty, but they'll struggle to parse it.
---
> Sometimes people mean that clear code is verbose code.
I also consider some verbose code to be "clever code" too. Most of the time it is just people using hammers where screwdrivers would be more appropriate:
- Unnecessary structures - eg: classes that could be functions taking 2x or 3x more space
- Unnecessary usage of polymorphism - eg: inheritance chain that could be a simple if
- Excessive indirection - eg: layered code where most of the time the layers don't do anything
- Plain wrong abstractions - eg: using query builders to build queries that would be smaller and more readable in SQL
- Fear of making classes too big - eg: instead of adding a method to a class, making a second class that knows too much about the innards of the first one
- Procedural code disguised as OOP - eg: breaking up a method into multiple private ones, but ending up having a lot of instance variables that were local variables before. When everything could be a single function.
---
> The best we have is cyclomatic complexity, but there's some reason to believe that line count may be a better indicator (which can't be good). And cyclomatic complexity completely misses the effect of mutable or immutable state, the presence of bad APIs, poor variable naming, etc.
Great observations. I agree 100%.
Since you mentioned it, I find cyclomatic complexity a bit too easy to game. I always wanted to have a metric that prevented people doing that, and took multiple methods into account.
When you break a method in three or four without adding REAL abstractions (a.k.a. "things you don't have to follow with the debugger to understand"), you're not making it easier to read, in fact you're making it harder because the reader has to jump around your code.
Of course, it's harder for machines to know the difference between good and bad abstractions. But I think we should take that into account in code reviews and such.
Yeah, there's competing forces involved. If you push too many things together that do not belong together, then you end up with code that is hard to comprehend. Additionally, if you spread too many things apart that should otherwise be together, then you end up with code that is hard to comprehend (your point). Finally, what things belong together and what things ought to be separated will depend on the domain and even the specific constraints within the domain.
In order to objectively determine when things have gone poorly you have to factor in a lot of external details.
For example, manual memory management is a detail that should almost always be someplace else because it isn't relevant to solving the problem at hand. So we did this with garbage collectors. However, sometimes we need this detail present (high performance computing and/or constrained hardware ie video games etc).
That's a great example.
IMO looping is another case: I think it is clearer to use constructs like map/filter/group/sum/reduce instead of for/while/break/continue/etc. You only really need the procedural ones in hot-paths and such.
Concurrency too: in Javascript you had to nest callbacks and promises, and they were also "too noisy". Async/await helps you keep your code more readable. I think Fibers in Ruby (and other languages) were a good idea too.
-
> If you push too many things together that do not belong together, then you end up with code that is hard to comprehend.
That sums it up nicely.
Cross-cutting concerns should be abstracted (but not hidden), as they don't belong together.
Wait, what? That's the most fun!
I’d also like to point out the irony of how an article about readability is borderline unreadable on a mobile device. The font size is microscopic on page load, and after zooming in to normal font size on a standard iPhone screen, I can only fit 1/3 of the width of the text without scrolling horizontally. If your content is in paragraph form, please just use html.
Too clear is not clear at all.
Too clever is not clever anymore.
Similar things would happen with say map(f) |> map(g) vs map(f |> g). My clev-dar just reminds me to ping the reviewer or leave a note.
I'd say it's objectively prettier most of the time, but unfortunately that's exactly what introduces double negatives or other ambiguous stuff and makes the code more confusing.
It's a nice refactoring step, though: apply it, extract the condition into a well-named variable and test that condition.
Less code does not mean less bugs.
I've heard comedians talk about a set not having any fat in it... every phrase, every short pause, etc. is exactly what is needed to make the bit funny. To stretch this analogy a bit, you can trim so much fat that you start taking the meat and the audience can no longer follow the joke. We need that kind of philosophy in coding.
I think python and golang are largely responsible for this kind of movement in recent years. Because of that, I find languages like Perl 6 to be a breath of fresh air.
For example, there’s a big thread about the three-way comparison function in this discussion. Lots of people don’t like nesting ?: syntax to implement the required behaviour as a one-liner, but the objections essentially seem to be about the syntax of the ternary-if operator in C family languages. In another language, with a more intuitive syntax for this behaviour that doesn’t look like operator soup, the underlying idea of testing a series of conditions in order and yielding a single output value based on the first condition that matches might be perfectly intuitive to those same people.
Someone else in that thread commented that using a switch statement with conditions rather than specific values was unwelcome because then the more general conditions might overlap. Again, that view seems to be based on what the keyword “switch” means in a lot of established programming languages. There are other languages with features that test a series of potentially overlapping conditions in order and act based on the first match, and perhaps that same person might not have had the same objection to the same behaviour if it didn’t come with the baggage of the term “switch”.
At this point, I personally wish I could concentrate more on the intent when I’m programming. For example, do we need an expression or a statement here, or are our conditions a set of mutually exclusive options or potentially overlapping? My ideal programming language today would provide ways to express differing intents with some — any — reasonable syntax, but with very clearly defined semantics. Whether a language uses semicolons or syntactic whitespace or Lispy parentheses has long since stopped mattering to me. But that’s the me who’s been doing this for a long time and got tired of debates about minor details because in the end they mostly don’t matter. A younger me, one in the early years of his programming adventures who hadn’t yet seen these discussions a hundred times before and didn’t yet think in the same ways, would probably have delighted in the language lawyering and syntactic quibbles in that thread, and would no doubt have had a strong opinion on each suggestion.
And that brings us neatly back to where we came in, because it means the properties of code that is clear and desirable to me today are very different to the properties that would have made code clear and desirable to me, say, twenty years ago. So if I were writing code today and trying to be as clear as possible, my target audience would be a huge factor in the choices I’d make.
(1) var thing Thing json.Unmarshall(reader, &thing)
(2) var thing *Thing = new(Thing) json.Unmarshall(reader, thing)
in (1) thing is a variable for a value of type Thing, while in (2) thing is a variable for a Thing pointer
shouldn't thing (2) need to be dereferenced first?
In C++ it would look like this:
(1) x = thing.Property;
(2) x = thing->Property; (or (*thing).Property)
but in go, both would look the same, like this: (1) x = thing.Property
(2) x = thing.Property
So there is a difference syntactically, and the compiler will handle them differently, but whether the difference is semantically relevant is debatable.They're semantically the same. The type of thing isn't, but that's syntax.
> Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v. If v is nil or not a pointer, Unmarshal returns an InvalidUnmarshalError.[1]
In (1) the '&' in '&thing' means roughly "the address of", so you are passing "the address of thing" i.e. a Thing pointer that points to the thing variable.
switch {
a > 5:
...
a > 3:
...
}
Here a > 3 is not reachable: it tests a condition that overlaps with a > 5 completely, and a > 5 is earlier.This form of switch would be useful (and actually live up to the name) if the compiler were required to diagnose against overlapping cases, and also incomplete cases.
switch {
a < 3: ...
a > 5: ... error: domain of variable "a" not covered in switch without a default: clause
}
switch {
a > 10: ...
a > 5: ... error: overlapping cases in switch
} func comp(a, b int) int {
if a < b {
return -1
} else if a > b {
return 1
} else {
return 0
}
}Thank you Dave but some of us prefer explicit 'vars' all the time and reserve := for quickie peripheral variables.
I came from JavaScript so it wasn't too hard, but I was writing mostly with C when I first encounter go. With JavaScript I heavily leaned on properly named variables to understand the underlying type within the context of the code I was working on. In go however, short and nondestructive variables are common place, with the lack of types in declarations I have to reply on properly named functions and general context of the code I'm working in.
I'd be curious to see what go looks like with explicit vars for most things.
Often quoted, and I wonder if there's much evidence supporting it? Let's replace "machines execute it" with "people use it without reading the source code", and we can say that code is run many many more times than it is read.
Focusing on readability of the code and putting "machine execution" second, means putting the user experience second, after the developer experience. Imagine a race car engine which is designed first to be simple for an unfamiliar contract mechanic to repair, and secondly to power a race car, with the justification that the engine "will be repaired more often than it will be built". I have my suspicions that software which is fast, responsive, and a pleasure to use is designed by people who put the machine first; even if that means using difficult languages and low levels of abstraction and requires high levels of skill.
The most important skill for a programmer is the ability to effectively communicate ideas
Iverson Notation was designed as an improved way for people to communicate ideas, years before it got turned into the programming language APL. Look where that got it.
Go programmers realise that code is written to be read and so place the act of reading code above the act of writing it. Go goes so far as to enforce, via tooling and custom, that all code be formatted in a specific style.
Books are created to be read, not written. That doesn't happen by enforcing that all books have the same style and formatting.
If software cannot be maintained, then it will be rewritten; and that could be the last time your company will invest in Go.
Pushing a cult is the last word in the slides?
Focusing on readability of the code and
putting "machine execution" second,
means putting the user experience
second
Nothing kills a users experience like buggy code. Nothing contributes more to buggy code than unreadable or unclear code. This is why another proverb often quoted is "First make it correct, then make it fast". Fast but buggy code will lose to slower but correct code everytime.Remind me how Chrome took over the browser market with "Chrome Fast" as its slogan? How did nginx grow so popular against Apache? Was it promoted as "less buggy" or "faster"? How MySQL became so prominent, was that people choosing "slower but more correct"? How MongoDB became so popular - "slower but more correct" winning out?
Cite some instances of when "slower but more correct" has ever won out, assuming the faster system does work?
Nothing contributes more to buggy code than unreadable or unclear code.
That's quite likely false. Ten characters of unreadable code written by an expert will have fewer bugs than 100,000 lines of "readable, clear" C++ code written by a novice.
Nothing contributes more to buggy code than simply more code. To quote Arthur Whitney: "The only program which stands a chance of being correct is a short one."
Ten characters of unreadable code written by an expert
will have fewer bugs than 100,000 lines of "readable,
clear" C++ code written by a novice.
Everytime this topic comes up someone brings up this point. The counter to it is always the same. In the lifetime of those ten characters someone who is not the original author will have to modify it. Because it is not clear what exactly the code is doing they will probably introduce a subtle bug. Iterate on that a few times and eventually you will have code that literally no one can safely touch and the only way to fix it will be to rewrite it.If the person doing the rewrite is smart and looks at why the rewrite was necessary they will change those "ten characters" to however many it takes to make it understandable for the future maintainer. It should be exactly as short as necessary to be clearly correct and no shorter. Then you can work at making it faster with appropriate comments about why this particular speed hack is necessary and what you need to know before you modify it.
Because it is not clear what exactly the code is doing
A ten character English which name vaguely describes what a function hidden in another file might have been doing (assuming you understand the word in the same way the author did when they chose it) at the time it was written - but it might not be doing the same thing now since it was edited so many times, is not exact. By contrast, ten dense characters right there under your nose is exact - as much as a programming language can be. There's nothing hidden, no surprises, no unseen side effects, no uncertainty about whether it handles edge cases.
Code is as exact as it gets - it does what the language does. Abstractions are less exact - they do some version of what the previous chain of developers intended the words to imply that they do, in the best case. In worse cases they also do unrelated and surprising things.
And you dodged every question about when slow but correct code won out over faster, more buggy code.
I wish this were true, but unfortunately C++ proves this is not the case. Fast and buggy seems to be the holy grail there.
This is unfortunately not always true.
It depends on what the consequences for incorrectness are. Outside of enterprise and the web, fast code can be an important selling point, and it may end up being prioritized over correctness, as long as the result is "correct enough".
I wish it weren't so.
Now if your code is such that most of your users frequently experience incorrect behavior then I think you'll discover that you'll lose to a competitor.
More charitably, a better argument is that it is easier to adapt a compiler to reoptimize it than to tweak the entire code base - let alone the costs for the gains.
No, it doesn't suggest this at all. It means use the merge sort but implement it in a clean, understandable way.
https://rosettacode.org/wiki/Sorting_algorithms/Merge_sort - which ones of these do you consider objectively "clean and understandable"?
Why should any of them (Prolog, Mercury, J, Common-Lisp, etc) be considered "not understandable" instead of "not familiar to the reader"?