It's interesting that the comparator function is one of the examples, because that's something which shows how people often confuse verbose with clear; I think it's best written in a single line:
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.
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.
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)
That makes code formatting inconsistent because many prefer the other way. The VB.Net style is more compact and readable in my opinion:
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.
IMHO, better but it’s still confusing when you have multiple places where you name the variable. Functional programming style forces you to be more explicit and pushes you to separate out the assignment expression. In Elixir I’d do:
Maybe, but that misaligns the values. I'd rather see them lined up. And often more complex logic may be added to the sub-blocks such that the simple pattern goes away. Code should be able to "degenerate well", meaning it shouldn't require lots of rework when the initial pattern fades or changes in the future. It's one of the reasons I often use If/else instead of switch/case statements.
As I mention above, it "degenerates" better in that if more code is needed in each sub-block, it's easy to add. I'm okay with syntax that could factor such out if it doesn't require overhauling the block when the starting simple pattern goes away over time. As they say, the wrong abstraction is often worse than no abstraction. Change can kick the simple pattern away.
Function that supplies value, can also encapsulate alot of data generation code. After that you can write a strategy to select which data generation algorithm (supplier) shpuld be used.
Just maybe he genuinely doesn't know what you mean. I mainly use Python and have no idea what you are talking about.
(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.)
> Combinations of if/else can seriously damage readability and maintainability.
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...
> every programmer worth his money just knows that ?: is right associative
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:
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 '?:'.
This came up recently on the Swift Evolution forum (which is the official forum for discussing changes to the Swift programming language). Dave Abrahams said, “[…] I keep meeting experienced programmers (really smart people!) that have no trouble reading [a chain of if/else statements] and yet are confused by the analogous ternary construction: [a chain of ?: expressions]”.
My favourite solution to this problem is how rust does it. In rust every block can evaluate to an expression, so if/else is the ternary operator.
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.
Yep! Coffeescript does it too. It was always weird seeing random values pop out of functions while debugging. Also coffeescript's loops evaluate to a list, which meant that functions which ended in a loop in coffeescript would end up constructing and returning arrays that would never be used.
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.
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.
There's are two pretty simple explanations for that.
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.
"readability" is largely a matter of familiarity, so it's very hard to make such sweeping statements accurately. The difference between "common idiom" and "unreadable mess" is "common", not the code itself.
Strongly agree with this. My company has its own coding standards and openly acknowledges that they are not objectively "the right way to do it", because such a thing does not exist. Instead, they define rules to make our code (relatively) safe and consistent.
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".
It's becoming more popular. Kotlin, Swift and now C# 8.0 have pattern matching (well, close enough at least). I sure hope other languages take note, because you are right about it essentially being a solved problem.
Yep! Scala's pattern matching is so far ahead of Kotlin's, vis a vis destructuring, that it strains the conscience to say Kotlin has pattern matching at all. FWIW I find both to be enjoyable languages in which to work.
Short circuiting is poor for readability in my view because not only do you have to parse the syntax but you need to mentally walk through each case to understand what’s going on.
Traditional if/else conditionals express clarity by embedding the decision for myVar and visually show how it would be assigned in various cases.
I actually prefer the long version. But I think the real trick is to be consistent in style. Parsing ternary operators is a learned skill and so is parsing long if statements. You can get used to both.
Programmers have to learn to read different styles, to read code from other projects. So use the construction that is clearest in its local context.
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.
I don’t think consistency is a hobgoblin (I hope I understand the meaning correctly). When you are new to a project you should follow the style of what’s already there and not do something completely different. Obviously there may be good reason but that should be discussed and you also should ask yourself if you are just not willing to adapt or if you are actually making things better.
I have also seen switch statements used (abused?) to do a similar thing. Since a switch executes the first statement that is equivalent to the control expression you can pass true as the control expression, e.g.:
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.
I strongly disagree; creative formatting like this means that as a reader, you have to squint and readjust to this new style to see what is going on.
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.
Personally when I find myself with these constructs of complicated and nested conditions I try to simplify the branches and condition checking to functions.
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.
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.
About the only change I would make to your layout is to put a line break after the “?”, with the value sub-indented on the next line, to avoid “guessing” /maintaining how far to tab over the value column.
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.
As long as verbosity is meant to make your code more explicit I'm fine with it.
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.
"Personally, I find that ternary example you posted to be terrible code."
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.
by replacing tokens recognizable as natural language words with arbitrary punctuation
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!
Or don't learn German but continue speaking in English which is inherently same as German in functionality but at least you and people around you can understand each other.
> It's not about the line count, it's about expressing the main idea clearly and succinctly, which this code does.
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.
Python has its version of the ternary operator as well. It may look unusual due to the different order of the operands, but it works quite nicely, in my opinion:
x = (-1 if a < b else
1 if a > b else
0)
This emphasizes the possible values that x may be assigned to.
I agree, the ternary style is better for this sort of variable assignment situation. Usually if I am using the above pattern I am calling different functions as the result of the conditionals.
> We are not using parchment anymore, there is nothing to be gained by saving a couple of lines.
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.
It really is a balance, and it's very hard to get it "just right". I mean I can appreciate a succinct oneliner (which is why I really like functional programming, doing a .map() instead of a for loop for example; it's a lot more compact without losing expressiveness, however, you do need to learn the lingo at first).
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:
> It really is a balance, and it's very hard to get it "just right".
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.
I disagree, what is "saved" is your time when reading/understanding... why spend time to read 10 lines and figure out that it actually does what a simple ternary operation would accomplish??
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.
> why spend time to read 10 lines and figure out that it actually does what a simple ternary operation would accomplish??
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.
> 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.
Which is why important details get missed when reviewing verbose code.
These are both extremes and both bad. The 10 line switch statement to switch a variable is a high cognitive load for no good reason but so is a nested ternary under pretty much any circumstances, but especially with single character variables and magic numbers.
>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?
The bigger constructs are made out of these smaller pieces. Focusing on just the high level abstractions will end up with your application being difficult to fix when something lower level breaks and focusing on just the lower level code will cause your application to end up unscaleable or refactorable because of poor design.
It's a question of trade offs but you cant entirely ignore one or the other
Because if the structure is different from what you're 'used' to, you spend ten times as much time reading (or as the original presentation coins, "decoding") that one line to understand what it means.
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.
I wouldn't call that example nested; I'd call it sequentially chained, and I think that pattern is quite clear even with many conditions in the sequence.
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 agree with the sibling to this comment, though, that a ternary is more readable with newlines and indentation:
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 think your version is reasonably clear, but it won't survive automatic indentation in any tool.
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.
This is a natural point of disagreement. The question is what you're trying to do.
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.
> 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.
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) };
}
These are great examples, and I totally agree with both of you. However, the code most people write is somewhere in the middle, so it is a more subjective decision.
This I think is obviously personal preference, but to me nested ternarys are pretty readable as long as they're wrapped and indented.
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.
It's also a fallacy that a ternary operator is some impossible to understand concept that will make the code totally unreadable for everyone but you. C programmers should be fine with the use of ternary operators for something like the example shown here.
totally agree, but life is full of compromise. I like to write code so that someone new to field could without too much effort follow what's going on but also so that someone better than I isn't so bored that they miss obvious things in the code (which does happen).
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.
I know ternary operators very well but I still have to think twice when I see them nested
“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.
> 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.
I've been taking the approach of writing code at the highest level of trickery/abstraction that is well-supported by my automated refactoring, debugging, and static analysis tools. This usually means keeping code quite simple. I can then play around with the code much easier if I need to make any changes, and I don't have to focus on the details much because they are quite explicit.
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.
>If someone is hired to work on your code base, presumably they know the language. They know ternarys work...
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).
how many branches/how much depth in a ternary statement would you consider to be too many/too much?
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?
To be clear, a lisper would not solve the problem by switching to a more verbose syntax, but that doesn't mean no depth is too deep. Breaking a big, deeply-nested function up into smaller ones is a perfectly lispy thing to do.
Yet it makes people's live better day by day... I feel that as insiders we don't really realize that software works quite well compared to many every day things.
Exactly; to use the phrasing used in the presentation, it's about how difficult it is to decode the statements. I'm not a C guy; a regular ternary is fine, as long as it's simple enough. A nested ternary? That gives me all kinds of red flags because I have to lean in and frown and go over it a few times to decode it.
I would make the exception that if you judiciously indent then across multiple lines, to reflect the nested structure the way the if/else equivalent would, then they're fine. But yeah, nested ternaries on one line are impossible to follow.
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.
If I understand correctly, what you're really advocating isn't ternary vs if/else, but expressive conditionals vs imperative conditionals. A statement that is something vs a statement that does something. Using expressions instead of imperative code that mutates state is a staple of functional programming, and is basically always preferred where possible. In some languages you can do this:
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.
Yes exactly. I mentioned in another comment that rust allows this, and having used it I think the best of all worlds.
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.
>then it exactly mirrors the if/else-if/else structure.
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.
They are confusing and misleading to anyone new to your code base.
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?
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
No, it doesn't. In most other industries, you are simply considered incompetent if you don't possess the skills or aren't willing to learn the ways of the trade. Look at aviation for example: pilots and ATC talk in jargon, acronyms, and abbreviations like it's a completely different dialect of English, anyone who complains that they can't understand would be laughed at and told to keep learning, and that's the way it should be.
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"
> Look at aviation for example: pilots and ATC talk in jargon, acronyms, and abbreviations like it's a completely different dialect of English, anyone who complains that they can't understand would be laughed at and told to keep learning, and that's the way it should be.
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.
Did you see that it was me quoting that exact quote as something to argue against?
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.
> I would expect a new hire to be familiar with it, or need time to become familiar with it
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.
I once had to maintain a chat app in which the first developer had written something like the following at some point:
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.
FWIW modern C compilers recommend double-bracing exactly for that reason e.g. in clang, by default, it triggers:
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) {
^
==
>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?
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".
Yes, they all agree on simplicity. Part of the problem is that simplicity is not entirely objective.
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".
The mistakes that people make are not subjective. They can detect when they make a mistake, and must do so to correct it. When people make mistakes by using a syntactic construct, that's something can be measured, and people do measure it, which is one of the ways they develop syntax preferences. The problem is that we don't ever measure it in an unbiased and representative manner.
Yes. Go is intended for BIG codebases - millions of lines of code. You can't afford to have to squint and decypher one particular author's code style when you have to go through that amount of code.
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.
I personally find this one-liner mentally difficult to parse in a glance. Yes, I can fully understand what it's actually doing, but when I glance at the code I have to focus on it too long before I am sure of what it is doing.
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...
The multi-line comparators are definitely clearer and more readable to me.
The 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.
You see the problem though, I need to try it and practice it to see how simple and clear it is. I can write code that I can easily understand. I can write terse, clever code that I can easily understand. But I want to write code so that the next person can also easily understand it. More importantly, code that's easier to understand has less chance of getting buggy when it inevitably gets rfactored or extended.
> You see the problem though, I need to try it and practice it to see how simple and clear it is.
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.
>It literally takes 5 seconds to understand the idiom
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.
> It's still hard to read if you inline multiple ternary operators the way OP suggested is easy.
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.
Compactness and readability are related, in the sense that your working memory also works somewhat in terms of lines of code.
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.
Most compilers will probably reduce if-else and ternary to the same instructions. If it's a conditional value binding it might not be a branch but a conditional move instruction.
i find it really odd that of all the knocking back and forth on this thread, you are the only person to suggest that using symbolic values for 'greater than', etc, improves readability.
It's not really my suggestion though, it's just what Rust does (inherited from Haskell, interestingly enough OCaml does not do that) and I figured I'd post "proper" rust code rather than a bastardisation.
Nope, since I haven't added any non-whitespace characters, rather it is like insisting that people do:
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 irrelevant to understanding the semantics
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.
I tend to avoid the ternary operator because in my experience even people who think they know how it works are often unaware of its edge cases when it comes to silent numeric type conversion (looking at you Java).
If you don't put the same type on each return sides of your ternary, you might be in for a nasty surprise.
It's not 10 lines, it's 8. Your implementation would need the function declaration and the } at the end too.
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.
Well that is what I think if it's on the first case I mentioned. I see it contained inside a comp function and I move on.
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"
>It's interesting that the comparator function is one of the examples, because that's something which shows how people often confuse verbose with clear; I think it's best written in a single line:
return a < b ? -1 : a > b ? 1 : 0;
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.
>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
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.
FWIW, I write a decent amount of "clever" code (think C right out of K&R) but this took me a couple extra seconds to parse. Maybe it's an acquired skill?
Clear code is supported because it can be maintained easier by other people, and the original developer won't need to remember the cleverness he/she did in the first place, when he/she returns to the code six months later.
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.
In my experience, verbosity is a slight detriment to clarity -- concise code tends to be easier to understand, although the relationship is not very strong and there are many counterexamples.
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.
Nitpick: I often break nested conditionals in several lines, like this:
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.
I find it interesting to see so much emphasis on small syntax nit-picking while totally forgetting the large scale question.
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:
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.
I disagree. Reading the verbose example takes less time to understand, not just because the concept is clearer, but it is also visually easier to separate lines, even if that is 10 times as long as yours. If you see that somewhere, it will take a blink of an eye to understand, but your one line solution has to be read from left to right carefully considering colons and separating parts in your head.
But there are other benefits. You need to make a compromise and there is no win-win situation. It is true that the 10 lines version is totally obvious for everyone, but I do not agree it will make easier to understand a complex piece of code. If I fill my screen with a function that makes a simple comparison, I will need an IDE and at least a couple of monitors for a function that actually makes something, I will not be able to understand the code because I will be too busy navigating it.
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.
The fact that half the people in this subthread think that your version is less readable and half think it's more is really strange to me. If you have to traverse more lines and scroll down and switch between lines to track variables and conditions, that's unpleasant to parse. On the otherhand, excessively dense code where you have to figure out which of multiple function calls or syntax constructions to start at, then that's unreadable, too. But if you can do one thing and do that one thing in one line with predictable syntax, that's the ideal solution. I have a hard time empathizing with those who prefer verbosity over succinctness.
I think part of the problem is that it'sn't visually obvious that ? and especially : are lower precedence than < and >. I suspect that most of the people with genuine objections would feel better about:
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.
While code of this size and scope can be unclear, it is not where the real problem lies. In general, what's not clear is some version of "what does this do" and "why is this being done" where "this" is often a function call with an arbitrary amount of complexity behind it.
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.
In Smalltalk, we had return values from ifTrue:ifFalse: statements. This served the function of the ternary ( ? : ) while enabling if-then style brackets. Also, deeply nested logic and methods longer than 10 lines were regarded as "code smells" in the community, so a lot of that logic was implemented through polymorphism.
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.
Compilers are very good at doing the ternaries for you. With good ol' Javascript you get things like the Google Closure Compiler that will return yet more Javascript rather than some bytecode, this informs me that my use of ternaries is a waste of time. I might as well go for the if/then/else type of approach and refactor it when it gets ugly into some functions.
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.
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.
Depending on what syntax constructs are available, it's possible to achieve clarity without the verbosity or "line noise factor." For example, in Rust you could write that same code as:
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.
IMHO this is pretty hard to read, unless I guess you work on code written this way every day (and most of us don't). It's not that I can't understand it, but it takes unnecessary extra effort and time to process. I want to be able to scan quickly throw a code, concentrating on understanding the logic behind, not on syntax itself, and that's very hard when it's written very dense like this. For me ternary operators are great when used on simple expressions, without nesting, and always with brackets around expressions to help eyes to navigate. In your example I'd always prefer if/elseif/else structure in place of this (unless there's a spaceship operator of course).
I always feel like the idea of "clear" vs. "clever" is missing the point.
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.
In recent times, this verbosity has been accompanied by the (to me) insane decision in some JS prettifier plugins (opinionated even) of effectively putting one word per line. Now every if condition spans 4 lines on average. Delightful!
That’s a nested if statement which stops being clear.
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.
I completely agree. At my first professional programming job, I had to follow a style guide that forced me to code the verbose way. It drove me nuts and I found another job about 6 months later.
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.
Parentheses would help make the one liner simpler but your basic point seems quite accurate.
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.
I strongly disagree that your example is better than Dave's. I would much prefer to see Dave's switch statement in a new-to-me codebase than your ternary.
Is your code really clear though? What are a and b? What types are they? What if they, or one of them aren’t that type? Why would you call the function? When would you call it?
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.
One could make analogues to prose writing. The author's is in the style of xkcd's "Thing Explainer" where they deliberately only use 1000 words: https://xkcd.com/1133/
Yours is in the style of James Joyce where it's very dense...
the easiest to read is probably somewhere in between
> To be clear, I don’t mean to dismiss the work of a lone programmer toiling on programs without anyone to pair with or learn from. I’ve been that person many times in my career as a programmer, it’s not fun.
Definitely fun, but also slightly nerve-racking. A new project, script, or proof of concept left entirely up to you. The canvas is completely blank. Your initial code will probably be the seed for whatever this is for months or years to come. Remember all that trash you talked about the poor decisions everyone who came before you made? It's your turn. Don't screw it up.
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.
It completely depends on the quality of your collaborators. Are they good developers, do they have somewhat compatible views, and are they reasonably friendly? I prefer working in a highly capable group.
Working on personal projects, not having to conform to other people’s requirements, trying a bunch of cool hacks and getting to see how things really work…that’s the most fun part of programming!
The difference is the 'personal project' bit. If you have to do something, because it's for work and there's a deadline, then the fun hacks produce a general feeling of unease, uncertainty, and of not doing ones job properly.
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.
Side projects are probably the most fun that can be had with programming, not just for the freedom of requirements but also because you can build something you truly enjoy building.
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
Bs, nobody will read your code. 1 per 10 000 maybe. And even that 1 will be polite enough. Don't spread this bs - people shouldn't be afraid of shaming when they have some code to publish.
That's why I put the `/s` there. I wasn't being serious, and hope that most people got that. If not, my apologies. I constantly commit my toy projects and the few that got some viewers / comments were all polite indeed.
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.
Oddly enough I find the collaboration more interesting now. Not sure if that’s a getting older thing but I like managing the PR’s, ensuring code is of a high quality and serving the needs of the users.
I do both, and they have their own unique rewards. Writing personal projects is an exercise in self-improvement–it really doesn't matter all that much how your project turns out–while working on projects with others means you need to care about how other people and your project is improving. But in return you get to interact with people, show off your skills, receive and provide help…all useful skills in their own right.
I've never been happier working on a project than when I'm working with somebody that is a better programmer than me and willing to give advice and criticism.
I agree with the author maybe because for first four years of my software development career I was a lone developer. Maybe Dave was in similar situation.
I'm pretty sure we're not supposed to admit that, but I agree. Of course, if somebody asks me in person, I agree that team work makes the dream work and there is no I in team and we're all better together and the whole is greater than the sum of its parts. If there's a steady paycheck in it, I'll believe anything you tell me.
This reminds of a quote by Nikola Tesla: “The scientists of today think deeply instead of clearly. One must be sane to think clearly, but one can think deeply and be quite insane.” I always preferred clarity over "cleverness" not just in programming, but also when say reading a book on a technical subject. Clarity evokes feeling of beauty I would dare say.
“Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better.” - Dijkstra
Nice intro, which then jumps to an absolutely arbitrary (for someone who hasn't coded in Go) set of recommendations, which are (imo) poorly argued from the perspective of clarity. Especially the case of "var person int", how is it cleaner than "var person int = 0" - and why are we even talking about zero values when the variable was not supposed to be intialized. Many decent imperative languages let you not intiialize a variable, and then not allow you to reference it until it's been initialized in all branches.
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.
'Especially the case of "var person int", how is it cleaner than "var person int = 0"'
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.)
> > Especially the case of "var person int", how is it cleaner than "var person int = 0"'
> 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.
When you are talking about making code "clear," I think it's important to ask "clear about what?"
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.
I agree with the sentiment, but not with Go's interpretation. To me, higher-order functions like map and reduce are much easier to read than the equivalent for loops and are less likely to have subtle bugs (off-by-one errors, etc.) because they more directly capture the author's intent.
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.
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.
I'm fairly sure that we as a community can come up with an objective measure for the phrase "clever code". Something like: Exploits implicit and/or hidden details to correctly function.
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.
> 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.
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).
> 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.
>To be clear, I don’t mean to dismiss the work of a lone programmer toiling on programs without anyone to pair with or learn from. I’ve been that person many times in my
career as a programmer, it’s not fun.
I love Go and respect the philosophy of the language, but I do agree with userbinator that the question of how to achieve that becomes much more interesting and less bike-sheddy when talking about macro readability.
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.
Reminds me of an early code review that preferred applying De Morgan’s law and being initially surprised. It was a small chat that started talks of the clever vs readability on a small team.
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.
Yeah. I always see the De Morgan's Law being used to make the code look prettier or look less complicated.
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.
Sometimes. But only sometimes. A clever one liner that is easy to misunderstand when reading it while trying to find a production issue with eyes on you is NOT fun at all. And it isn't worth the two lines it saved.
I'm not disagreeing with you, and maybe this is just my personal experience, but there seems to be a movement that promotes succinctness over all other qualifications. It's as if someone's comp-sci class had a competition for who could accomplish some task with the fewest bytes of code and a whole generation of programmers never got out of that mode.
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.
On the contrary, I think the zeitgeist for the last 5-10ish years has been the opposite. "Verbose and explicit > Succinct and implicit".
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.
That's interesting. I wonder if that's influenced by the languages we use, as they might have different cultures. I primarily work in Java, which is famously verbose.
Reading this discussion, I think one inescapable conclusion is that personal experience has a great influence on what people consider a desirable coding style. Among other things, people often infer something that isn’t actually there, because such an inference would hold in the programming language(s) or the more general programming model(s) they are familiar with.
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.
I think the semantics of the language is also important.
A ternary operator in a C like language probably is frustrating because it's some magic that was just shoehorned into the language with no rhyme or reason behind it.
However, in a language like Perl 6, each magical operator has an intelligible semantic meaning behind it almost like a natural language. So when you mix and combine different operators, it starts to make sense to you. See this post for an example https://perl6advent.wordpress.com/2017/12/11/all-the-stars-o...
I think the semantics of the language is also important.
Yes. I suppose I’m arguing here that the importance of semantics is much greater than the importance of syntax. It’s not that syntax doesn’t matter, but these days I find myself much more concerned with what features a programming language offers than exactly what they look like.
A ternary operator in a C like language probably is frustrating because it's some magic that was just shoehorned into the language with no rhyme or reason behind it.
I’m not sure I can agree with that. The big difference between the ternary-if operator and if-else statements in the C family languages is that the former is used in expressions while the latter is used with statements. Sometimes that distinction is useful.
You are right that (2) needs to be dereferenced first, however, in go a pointer is autmatically dereferenced when accessing its properties or "methods". What I mean by this is that if you want to access a property on thing, it would look the same.
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.
json.Unmarshall takes a pointer as its second parameter. In (1), a pointer to thing is created using &. In (2), thing is already a pointer and can be passed as-is.
They're semantically the same. The type of thing isn't, but that's syntax.
Well, the second arg of Unmarshall is expected to be some kind of pointer.
> 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.
Using the keyword switch for successive condition testing is a travesty. Switching means taking one of several code paths based on a value. The problem with using a switch syntactic sugar for conditional testing is that the cases are not exclusive:
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
}
Either my minimal understanding of Go is showing, or this just proves your point, but don't you have your conditions backwards? `a == 4` would fail the `a > 5` condition and so execute the `a > 3` case, right?
Yes, I should have a > 3 first, since I was trying to say that the values of a that would trigger a > 5 are caught by a > 3. If a clause is obviously unreachable, that's likely a bug. But it doesn't mean that reversing the clauses is the right fix, either. They continue to overlap; and if that is allowed, this is not a "switch" in any sense.
I have noticed that ES6+ is a lot more functional, so there’s a lot more map/reduce stuff in modern Javascript. Although it’s definitely more succinct than the equivalent for loop implementations, I often find myself having to copy and paste it somewhere else and run it in stages to see what it’s doing whereas with the for loop variation, I could just step through the actual code as is with a debugger to make sense of it.
Clear is indeed better than clever, but clear and clever are not mutually exclusive poles. You can be clear and stupid, clever and stupid clever and clear, all is possible.
kinda funny...
the talk is precise, touche, and cliche.
Like software engineering 101 from top-notched software companies.
Especially from those top-notched software companies which writing C++ code.
That is, if C++ were not coded in this way mentioned in this talk, the project is just doomed to fail.
But for Go Lang, even you don't coded in these mind sets, the project still works, and maintainable, just that, it's shit code.
Do you have some examples of this style in go? This was once a gripe of mine with go, but I have since just gotten use to the lack of clarity around types I'm working with.
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.
Programs must be written for people to read, and only incidentally for machines to execute. –Hal Abelson and Gerald Sussman
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.
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.
Nothing kills a users experience like buggy code. [..]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.
You want a large amount of code, because you're afraid of maybe having to rewrite a small amount of code? How does that make sense, and why won't the large amount go through the same iterative adjustment and subtle bugs until it needs rewriting?
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.
It's not about the length of the code. It's about how understandable it is. I think we are talking past each other so I'll bow out now. We can agree to disagree.
> Fast but buggy code will lose to slower but correct code everytime.
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".
Something being correct enough is another way of saying "Most if not all of our users will never experience this." That's essentially saying that the code is not buggy.
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.
To be frank I have always found such dogmatism deeply questionable. I can understand caveats like "for long term maintainability" but taken literally it suggests assinine ideas like using bubble sort instead of merge sort for a large set not because of any memory footprint constraints but because it is easier to read. Insisting upon not referring to reality is not a good pattern.
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.
>... but taken literally it suggests assinine ideas like using bubble sort instead of merge sort for a large set not because of any memory footprint constraints but because it is easier to read.
No, it doesn't suggest this at all. It means use the merge sort but implement it in a clean, understandable way.
It is on a spectrum but it is already here to some extent. Look at previous performance practices which have become deprecated like manually unrolling fixed for loops. Those are the sorts of things which have been fixed up and assembly coding is pretty rare still.
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...