Please do not attempt to simplify this code
github.com
github.com
A naive look at this and my head is screaming that this file is way too big, has way too many branches and nested if statements, has a lot of "pointless comments" that just describe what the line or few lines around it is doing, and has a lot of "logic" in the comments which could quickly become outdated or wrong compared to the actual code.
Yet at the same time, it's probably a hell of a lot easier to maintain and manage than splitting the logic up among tens or hundreds of files, it contains a lot of the inherently complex work it's doing to this file, and it is so well and heavily commented that it should be pretty easy to ensure that any changes also keep the comments up to date (after all, any change without changing the resulting comments should show up like a sore thumb, and will most likely prompt the reviewer to look into it at the very least).
I'm only halfway through John Ousterhout's book Philosophy of Software Design but I think it agrees with you on this -- that smallness-of-file or smallness-of-function is not a target to shoot for because it prevents the things you build from being deep. That you should strive to build modules which have deep functionality and small interfaces and should contain their complexity within them so the users don't have to know that complexity.
Sure, it solves the problem when viewed from the outside. "users" of the software (users being other devs in this case) get a nice small interface and docs that explain how to use it, but internally it's much harder to work with. Having everything in one file like this without breaking it into "sub modules" for various parts of the module means that you need to almost have a complete understanding of the module before working on it.
In this case, I have a feeling that is a pro not a con. Because this file is so core to the system, and has so much complexity, that breaking it up into smaller parts could cause a dev to feel like they understand it only to find out they don't after it's released. And it means that any devs that truly do understand it top-to-bottom would just waste a lot of time switching around files if it were split up.
Putting it all in the same file here nudges you to really make sure you understand it top to bottom before making any big changes. It's intimidating and scary for a reason, because at its core it is a complex piece of code, and dressing it up in "simple code's clothing" won't help.
There's a balance to be struck here; you want to minimize the size of the code a developer has to understand to work on (or with) a given abstraction, but you don't want to split beyond that point, as it only makes the developer jump around files. In my experience (with Java in particular), current development trends involve splitting the code too much.
A relatively bad example here: https://wiki.haskell.org/Worker_wrapper#Hiding_the_worker
This runs the risk of strongly coupling them to the main binding and making them less reusable. IMHO strong coupling where two bindings are aware of each others' internals should be avoided.
That’s what I always do if I want to move an auxillary function to the top-level for reusing elsewhere.
I think you're on the money here. This was done intentionally, to ensure that devs understand the whole thing before making changes, because the functionality is so critical and so easy to mess up.
Also, breaking single logical things up into smaller files (rather than into a composition of smaller logical things) just leads to unneccessary file hopping and wrecks locality of reference for the dev.
For example, as you say, a small piece of stand alone code implies that someone can do meaningful work on it without understanding the entire context around it. It also implies that it is suitable for reuse. But if it’s both reused liberally and encourages you to keep working on it, it will almost certainly mean that your changes will have unanticipated consequences. So you still can’t get away with being unaware of the context.
When you break things apart you also fossilize that way of approaching the problem, which often makes it more difficult to see orthogonal approaches and refactor towards them later. Instead you keep working within the structure that’s already there, which often leads to concerns being spread out across different modules. Too often people decide on the structure before they even understand the problem.
I think it has similar problems as religiously following the DRY principle. There are so many situations where your code will be much, much worse if you insist on always sticking to DRY.
> Too often people decide on the structure before they even understand the problem.
This is particularly true on teams where a significant proportion of the developers is reticent to refactor code as they go. It seems that given a developer with a sub-80th (or so)-percentile propensity to refactor, the more broken up the solution is (into smaller functions, methods, classes, modules, etc.), the less likely that developer will be to refactor the solution when an obviously better approach exists.
As for comments, this file is essentially Ousterhout taken to the extreme. Still, I think he would have like it, given how critical this file is. In the book, he encourages writing more comments than the current trends would suggest, pointing out that you can't fully express abstractions in code, so all the things the code doesn't contain - the high-level concepts, the rationale, the caveats - should be documented in comments in appropriate places.
Overall, I'm extremely impressed by the book, and its focus on reducing and mitigating complexity.
Another way of describing this, that I ran across recently, is that this increases the cognitive load for developers working on the code, and cognitive load is one metric by which code can be measured as "good" or "bad". Things like excessive scrolling and switching between files increases cognitive load.
So heavily-commented code can decrease cognitive load if the programmer can read the comments and the related code block at the same time and if the comments help to explain why the code exists the way it does. Or, heavily-commented code can increase cognitive load if the comments don't accurately describe the code, or if they're so verbose that the programmer has to scroll up and down to digest both the comments and the code together.
Actually that’s one the reasons why he advocates not splitting code (arbitrarily) in the book.
Unlike a single use function, it does not have a name to remember and is in-place - and definitely is not shared so can be assumed to be safe to modify.
Comments would be much nicer if we could still use column-based commenting which unfortunately is not usable in any modern IDE. Reading code and comments side by side tends to work much better than interleaving.
Putting my PR reviewer hat on I would say the code in question would pass muster if it were relatively stable so you would not be constantly redoing the comments. I love nice comments but Golang does not give you much help keeping them in sync.
(Weirdly enough I'm working on a PR for persistent volume documentation at VMware today so the code is very apropos.)
What do you have in mind, here? Something like python's doctest?
Having tried that style, I notice that I don't particularly favor it, and for the very reason you site: the code is no longer all in one place.
I've switched to moderately sized methods/functions with comments every few lines. Some say that comments like this are a smell, and that you should refactor the commented section of code into it's own function, but honestly comments are easier to read than method names (and again, there's the benefit of locality).
I'll have to take a look at the book.
Something like:
#{ Parse input parameters
.... <several lines of code here>
#}Also, when using applicative or Monadic style programming there is little reason not to split things of in small separate functions and chain them together in the right order.
There are definitely places where it works out really well though.
I say this because I'm always astonished by the number of "modern" programmers who refuse to use IDEs.
In sane code the name of the function should describe what they do well enough that you rarely have to click in to learn how they do it.
Or something like that...
I.e. I wouldn't be inside a particular function of a particular module if I didn't have to know something about its implementation. There's a good chance I need to understand all of it at the level of abstraction of the module (often because I'm supposed to change something about it). Making that less painful leads to better and less bug-inducing experience.
Elsewhere[0], 'usrusr brings attention to nested functions, lack of which I see as a huge factor contributing to overeager splitting of code. With nested functions, you can have "best of both worlds" - a function whose implementation is divvied up into well-named pieces, while keeping those same pieces in the correct conceptual place, close to where they're used, and restricted from polluting unrelated code.
--
Pascal nested procedures are pretty easy to parse and if they're defined before the `var` block then you don't need to worry about non-local state modification (apart from the parameters, but modifying parameters is unusual).
First-class nested functions with variable capture are harder to understand. Nested functions in e.g. JS are more like object construction than function definition, and it's generally expected that such nested functions will be capturing state from the enclosing context.
Standard Pascal permitted outer scope variable access combined with downward funargs - the nested functions could be passed to other functions, but could not be stored in variables or returned as arguments. This reduces the non-local effects, and handily doesn't require a GC to ensure memory safety, since the captured state can stay on the stack, because the nested function won't outlive the frame.
For nested decomposition of a problem, I'm more of a fan of Pascal-style nested procedures than nested function expressions like in JS. Idiomatically, one expects function expressions to capture state, while nested procedures are just more procedural decomposition.
That's probably not what you mean to say, but I don't find much to really discuss here.
I do like local functions in Python, but I don't see a huge difference from having the called function right below the calling one.
The example was maybe the average function - and the reasoning was, if I recall correctly:
1. The defintion is shorter then the name average ;)
2. Every praticioning programmer will recognise the definition as a common idiom
3. From the definition it is immediately clear how corner cases are handled (e.g. zero length array)
I just leave this here as an example to show, that programming communities/cultures exists with completely different/alien? ideas about what clean code is ;)
Common repeated well defined and mostly immutable code is best left as functions. This is why for example in C strcmp exists instead of everyone writing a two-liner - and the specialized variant gives big performance gains.
Just for the sake of nitpicking: both your actual examples have been solved by compilers automatically.
SIMD/AVX: https://code.jsoftware.com/wiki/Guides/AVX
JVM generates vectorized string instructions from your simple for cycles - if availabe: http://jcdav.is/2016/09/01/How-the-JVM-compares-your-strings...
edit: my second example link is maybe wrong, but anyway, auto-vectorization is a real thing...
That's... not the point. Jumping around is. Imagine reading this comment thread on a bizarro-HN, where you only get to see a short camelCased summary like: debunk(this.previousComment), and have to click to open each comment in a new tab. This is how jumping around small functions feel.
> I say this because I'm always astonished by the number of "modern" programmers who refuse to use IDEs.
There are reasons for it. Many languages don't have an IDE. Many are not suitable for one (especially ones closer to Lisp on expressiveness spectrum). IDEs are heavy and often mouse-oriented, and not efficient for reading and editing text. Sometimes (read: Java) they are a crutch to work around the expressive deficiencies of the language.
Mind you, I have nothing but good things to say about IntelliJ. I've spent a lot of time in it even recently, and I pick it up any time I have to do anything in Java. But for everything else, I launch Emacs, because it can handle all other languages well, and has superior editing capabilities.
There is a tradeoff: Small functions make high-level logic clearly visible and easy to find, at the price of forcing you to jump around when you want to dive into implementation details. Putting everything into one big function lets you follow all the implementation details without jumping, at the price of making you read everything to actually understand what the code is doing.
The latter is the biggest price you can possibly make me pay. Jumping around is a minor inconvenience.
There's a middle ground - sectioned code, esp. with code folding or similar commented sections. Good IDEs support such a feature so you can hide the code you think you understand to not look at it every time.
You will have to read this code at least once or trust the documentation and name. I found that in "mature" code the latter is a recipe for disaster and days of debugging.
This is a minor point of your comment, but I'd like to refute it: Lispers actually often cite IDE integration as one of the great features of Lisp. It may have lost pace a bit with some of the very best modern ones, but Lisps have had "modern" IDEs since about the 70s or 80s, with stuff like jumping to function definitions, finding usages of a function, looking up the docs etc. Even better, this is usually a part of the language runtime (predating most other language servers by quite a bit), so it can even apply to dynamic uses.
I'm saying that, even with that convenience, I don't wrapping a line or two in it's own function is more readable than just adding a comment above those two lines within the context of a (somewhat) larger function.
Obviously this isn't always achievable.
It comes down to: "What do you want to focus on?" Each drill down should be to a lower level of abstraction. It is seperating the what from the how.
It results in functions/classes that either detail a flow (set of decisions) or that implement actions. For example I generally don't need to know how to read a file from disk where I am trying to decide if I should read a file.
When looking at code you drill down in a very vertical fashion when you want to know "How does it do operation X?" Meaning the operation largely can be understood in isolation. There shouldn't be a lot of context that is required for it. A side-effect is that all the unit tests are very self contained.
This utlimately steers you towards a much more functional and compositional programming style. Certainly it is moving complexity around, but the end result is heavy compartmentalization and limiting the context required to understand any section of code.
I disagree. Unless you are methodical and know what you are doing (like NASA or the authors of Kubernetes), it is hard to create large functions and files by keeping levels of detail consistent and not repeating code.
How do you test a function that has 100 unique outcomes? How do you safely maintain it to ensure it won't break? How do you even know it's working?
Constraint or property based tests as opposed to value tests? (That depends on how unique.) Usually the only thing that does unique things is a direct mapping, everything else is either an equation or has other preconditions, postconditions and internal or external properties. It also makes the logic more directly visible and refactoring easier presuming tests are reasonably written.
Ensure coverage with a tool to be sure.
Similar to UX design principles. Just like google or other successful sites present a very simple interface (in case of google the home page is quite clean) but behind it lies very complex code.
IMO, encapsulating complexity and handling all edge cases also lends to total functional programming [1] even without using pure functional languages. [1] https://softwareengineering.stackexchange.com/a/334876/38285
this. interfaces are not the most important thing in a software but definitely one of the most. you can live with a crappy implementation but your interfaces must cater to the usecase and should only hcange when core premises change.
As for the "space shuttle" term, it's more of a euphemism for "do not even attempt to refactor this mess, it's so complex that you'll surely fuck up if you do, and you'll make it harder for the one guy who can actually understand this"
I am all for code that is concise and whose syntax/naming is expressive, but sometimes comments are necessary to clearly spell out the logic or business use case. Expressive code can only go so far. Well-crafted comments significantly reduce the amount of time required for other developers to dive in and become productive with an unfamiliar code base.
The key is keeping the comments up to date. There's nothing worse than an inaccurate comment. One's code review process must include a review of the comments accompanying the modified lines.
It's probably because reading comments only is worse than reading code without comments, that some devs developed an aversion towards outdated comments and thus comments in general.
Comments are additional information and no source of truth, always take them as that and read code and comments.
So you wind up with files stating an author who no longer works there with an email address that the company used 3 acquisitions ago, a ticket number you aren't even sure what system it was for but you just know it isn't being used anymore and source control history that goes back 5 years out of a total 25 years of development.
The entropy is real.
I actually understood it immediately when I read it. There's something to be said when you have a decent roadmap for debugging critical code segments. It's not just software maintenance here, it's for the "holy crap something went wrong and we can't understand how we got here!" moments.
That stuff is lovely and terse when it works. And when it doesn’t you are at a total loss.
The beauty of imperative code is there’s a beginning and an end, and at every step you can easily see what follows and without too much work what came before.
the positive renforcement cheerleading to start off so that people dont get demoralized in the hateful comments, while effective also seems fake.
I warned him at the beginning that he needed to spend a couple of hours wrapping his head around the code before trying to modify it.
I warned him that the code arrived at its current state after long and painful experience.
I warned him that other developers had worked on the code, had exactly his initial reaction, and eventually admitted they couldn't find a way to improve it.
I warned him, after he confidently declared that it just needed to be "more object-oriented," that I had written a lot of object-oriented code, was open to writing it in an object-oriented style if it would improve things, and could not think of any way to do so that would not make things worse.
But he could not even bring himself to read the existing code. He never did. He worked on it for three weeks, wrote thousands of lines of code, and even tried to deploy his own version without ever spending a single contiguous, focused, hour-long block of time reading the existing code. He was "refactoring" the whole time.
The code he produced was exactly how you imagine it. The code went from several hundred lines in a single file with no classes (just a containing class acting basically as a namespace) to thousands of lines scattered over half a dozen files with at least that many classes. The guy kept adding classes and kept adding test after test after test. He couldn't get his code to pass the test cases I had written, so several times he declared that my test cases were wrong and changed or removed assertions. He also claimed that the existing code couldn't "pass" his tests because he had hundreds of lines of tests for components that only existed in his code. According to him, this proved that there was a ton of "hidden untested functionality" in the existing code, which was therefore "dangerous."
I was pretty busy with other things, and this was his project now, so if he had been a little bit more clever he probably could have made his abomination stable enough to replace the existing version over my objections. Thankfully it was never good enough to survive in production.
On the other hand, this code was in the middle tier of a three-tier Grails web app, so....
* didn't understand a difficult piece of code
* knew that the code was written and maintained in its current form by multiple programmers known and respected by him who had more exposure to the problem than he did
* knew that said programmers were well-versed in OO programming and had considered and rejected OO style as a way of improving the code
... this guy, because he had learned software development in OOP's heyday, simply knew as a self-evident truth that the code ought to be OO, because OO was better, and if the code was hard to understand then the proper way to approach it and try to understand it was to incrementally refactor it into classes. And he never stopped believing these things even after his multiple failures.
This is not a knock against OOP. This is a knock against the people in the 1990s and 2000s who thought software engineering was a solved problem, thought class-oriented OOP (exemplified by design-patterns-driven corporate Java style) was the solution, and thought the only hard problem left in the field was to teach all programmers to understand and accept these truths.
With code that far down in the inner loops of mission critical code, the dominant time cost isn't really writing or even understanding the code, it's ramping up on the nuances of the scenarios you serve, checking the direct effects of your changes, and then checking the second order and third order effects of your changes.
When your code is so far down that every change you make has third order effects, it's a lot easier to maintain code that is meticulously commented and written for maximum thoroughness explicitly baked into the file in front of you, because you're much more likely to miss the nuances if you just finished piecing together ten different files to figure out how the happy path works.
I kind of see it as the opposite: “space shuttle style” is code that adheres to heavyweight rules that most software development has abandoned in favor of a more improvisational style.
But in either case it illustrates that code style rules are desirable, or not, based on the context; you need to understand what purpose rules serve and what trade-offs they involve to understand which rules to use; there's no one-size-fits-all solution.
The complexity goes somewhere.
It's either into lots tests, or it's into something like shuttle style with lots of comments, or it's into a huge QA department, or it's into the type system / DB schema. It could even be going into the org structure!
But something, somewhere is handling the complexity and it is doing so as a partial function to the economic importance of the software to the stakeholders of the software.
Making the right component tackle the right amount of the right complexity is just a hard problem, and I think when it looks easy it's only because your options were constrained.
They solve the complexities the average programmer couldn't control on their own.
At a point you run the risk of just overloading memory with way to many names and entities by merciless chunking, not to mention create a file or function maze.
Inlining works surprisingly well because you do not have to memorize things and can just read them. Theoretically it is a tooling problem, but nobody wrote a good enough "inline" tool so instead everyone relies on incomplete and buggy textual descriptions.
> tests
pv_controller.go has 1715 lines. To be generous, we might say half of it is comments. pv_controller_test.go has 359. Hopefully this code is exercised elsewhere in integration tests?
> a huge QA department
That's what you're signing up for when you choose a language that expresses cases mainly using if/then/else, and when you don't feel like testing every case.
> into the type system
Sum types aren't complex, although they're not familiar to everyone in the way i/t/e is. Even a result type (a straightforward example of a sum type, available e.g. in Rust) would simplify a lot of the "if err != nil" boilerplate, and greatly de-indent this code. It would probably also make invalid cases unrepresentable in a few parts of this file, eliminating a few more branches. In fact, a sum type typically requires an exhaustive pattern match, making practices such as their rule "every 'if' statement has a matching 'else'" the default.
My point is, complexity (and economic utility) are not conserved as you choose between these "somewhere"s. A few basic type system features can drastically reduce complexity for the QA team or eliminate comments and cases from the space shuttle.
Still - good on them for this degree of discipline & for so many well-written comments, with clear contracts about what is modified or not. ATBGE.
Well, unless your computer can read and parse high order proofs over code. Not even Haskell can do that. (It can barely parse F types much less optimize them.) The closest I have come to that feature is Isabelle's simplify and even that is limited.
GCC and Clang can do basic bounds proofs at best and otherwise are tough buggy heuristic beasts. Just look at the trackers.
GHC has a slightly easier job but it's paid for in programming complexity. There are hoops in type system you have to jump to get performant code... and even then the code generated its at best meh quality. There is always some joker telling that it could be compiled for SIMD or multithreaded but that never materialized in usable form.
Of course they can help. Maybe you meant:
> they can't ensure that they are correct
This example - despise the perplexing celebrations - is a product of the limits of Go
All coding style is a product of the limitations of the language (well, except when it is coding style dictated by the limitations of a different language applied in a cargo cult fashion out of it's appropriate context); and celebrating the choice of how to approach this problem given that it was being done in this language isn't the same as celebrating the language choice. Go would be pretty close to my last choice for anything, but that doesn't stop me from recognizing this as a thoughtful way to apply Go to this problem.
The main problem here with this "solution" is that any monad looks like every other monad. You can easily lose context and have to rely on naming conventions, which is quite terrible. You cannot spot what the code is doing at a glance.
And if you mess up, the type system and compiler will spout some completely unhelpful message because it parses everything the same.
Code that does very different things should look different, not identical. (Unlike what Lisp and FP people think.) Just think on why we do not use textual buttons everywhere in UI. It's a major reason fewer people accept Lisp than could...
Exceptions are an ad-hoc solution to a sixth problem. You still have the other five. (Ok, it's possible to use exceptions to replace null. But that still leaves the other four).
C does not have exceptions, or any solution to error-checking hell at all. C++ is a notoriously non-simple language. Neither is at all convincing as a counterargument.
> Code that does very different things should look different, not identical.
In a language with monads, code that does things that are specific to error-handling still looks very different from code that does things that are specific to async I/O. But code that does a thing that is fundamentally the same (parametric), such as composition, looks the same. This is the very essence of programming (and indeed of mathematics): "2 + 2" does something that is, on the surface, very different from "3 + 5", yet there is an important underlying commonality. Code that sorts a list of integers is doing something that is, on the surface, very different from sorting a list of strings, but there's an important underlying commonality. Monads just take the same thing one level higher.
I think the major point you're missing here is that the whole point of pushing monads as a core abstraction is the way they shift what "different things" means.
Me: "complexity is not conserved as we choose beween implementations"
You: "So basically complexity is conserved"
Maybe we're getting confused by this word "complexity", so let's break it down into two things: complexity of task handled (COTH) and complexity for programmer (CFP). For a given task of a given complexity, COTH is tautologically the same no matter how you choose to implement. The level of CFP, on the other hand, depends on how you decide to handle complexity. Implementing it in assembly language? High CFP. Implementing it with i/t/e, and commenting loudly your intention to handle all cases? Medium CFP. Enforcing these good, exhaustive standards with the type system? Low CFP. Why does CFP differ? Because the tradeoff matters. In particular, my point was that sum types add a tiny bit of complexity to the language, and remove a TON of complexity from the code. It's not a wash, or even a close contest.
(Notice that I don't make any super-radical suggestions, e.g. that their discipline around mutation/purity should be enforced with types or monads instead of exclamatory comments -- in which case, the CFP added to the language might outweigh the CFP alleviated from the typical file. But my point holds: CFP is not conserved)
The PV subsystem interfaces with external storage systems, so integration/end-to-end tests are much more useful than unit tests (yes, they do exist).
I think you are more or less spot on. Personally I'll mention explicitly (one could argue that you already say this above) that well designed libraries and languages helps you by containing some of the inherent complexity while avoiding to add incidental complexity.
This isn't a dichotomy. My point is that there are clear examples of situations where you aren't just pushing complexity around, but actually achieving great simplifications.
No it wouldn't. The complexity of a pattern can usually be conserved while reducing its length, but for each pattern there is a limit. This is the entire concept behind the Kolmogorov complexity of a system and any patterns that cannot be reduced any further without removing complexity are at their limit already.
This is also related to the idea that you cannot have a universal compression algorithm.
I take this more to mean that the logic you're trying to implement has a fixed, non-zero level of complexity (sometimes called "essential" or "inherent" complexity), which forms the complexity floor of your application. On top of that, your implementation adds additional complexity (sometimes called "accidental" or "incidental" complexity), which is not-zero but not fixed.
So, my reading is that in saying "every application has an inherent amount of complexity that cannot be removed or hidden", the law is referring to the essential complexity. Meaning, the law says "some of the complexity is unavoidable in every application" vs. "the amount of complexity is fixed in every application". I do think the name of the law is a little weird, as it implies the latter meaning.
Unless there is one system to rule them all, configured exactly the way it is needed out the box.
Most software other than that written for space shuttles and other seriously critical applications.
A colleague of mine works for a telco, and changes to software that runs on satellites takes months to approve and goes through verification stages that include running on simulators and duplicate hardware that is on the ground.
Once two functions are too far apart to be on screen at the same time, jumping back and forth between two functions in the same file doesn't seem any easier than switching between different files. If anything, switching between two different files is easier since they each get an editor tab.
On the other hand, being wary about extracting functions (which moves things logically together further apart) makes more sense.
I started out writing low-ish level code. Motion control, image processing, digital imaging, and the application level code that coordinated it all. I've steadily moved up the abstraction tree over the last 13 years and there's one thing I hate about it; too many fundtions and classes which do far too little. Abstraction for abstraction's sake.
It's a bit subjective of course and not everyone has their own style, but there is a Starbucks difference in philosophy between the two types of groups.
With jump-to-definition editor integration, who cares?
I did, in an IDE, and I can tell you, jump-to-definition removes the problem of "what file do I have to open now?", but still leaves you with the questions like "where am I?", "how did I get here?" and "what was I trying to understand, again?", which you start asking yourself after ~sixth jump.
In emacs go-mode, pop-tag-mark will take you back to where you jumped from.
It'll take me one level up, which is not sufficient to answer my question, and by the time I find my bearings again, I have to jump back down a couple levels.
I've done a lot of this, both for Common Lisp codebases in Emacs, and for Java codebases in IntelliJ. Having to jump around and remember stuff eats into "7 ± 2" short-term memory limit that you need for the code you're working on.
(With Emacs, it's at least a tad easier to split the window to have 4+ different in view at the same time.)
I'd add this kind of bullshit started directly from Sun Microsystems when they gave us Java EE blueprints to develop end-to-end enterprise solutions in all seriousness.
This is just brilliant, not only because as educational exercises explains perfectly what's doing but the business/thinking process/context.
Never read 1k lines so quickly before, never enjoyed so much.
If Anthony Braxton wrote software...
// Design:
//
// [... 4 paragraphs of English prose
// explaining goals and intent... ]
That's exactly the type of comment that should be at the beginning of most files!It still baffles me. Every programmer has to start from scratch dealing with a new codebase, and it makes improving any non-trivial program impossible unless one is willing to spend hours of archaeological examination. To open-source developers: if you want to get people contributing to a project (and make everyone's effort much more enjoyable!), these sorts of comments are essential. Not to mention they'll save everyone boatloads of time; it's a shame that every programmer has to piece together knowledge from scratch, rather than being 'tutored' by their peers' comments.
I think this type of summary should not be per source file but per package/folder/module/project. A high-level developer overview with sufficient depth will also help stop repetition of philosophy in subsequent files.
In this case, I do agree with its existence though because of both the length and complexity of the file. For many files, neither are the case.
Design patterns can tap you easily in bad designs. Usually the lost thing is performance, big way, or actual simplicity.
I've met more than one codebase which tried to use a big hammer pattern to open doors. Heavy indirection to do simple things because of design choices, usually caused by a forced framework - instead of easily factorizable simplicity. Patterns should be emergent not forced. Or more specifically, the choice of appropriate pattern should be based on the problem being solved, rarely one pattern can cleanly solve all problems.
Similarity is very seductive but end result is often complex and hard to reason about when followed religiously.
A tautology is a tautology.
How long is a piece of string?
When I read something saying "writing thoughtful comments is too much work" I really read this as "thinking about the code you write is too much work, it's better to write whatever crap that first comes to mind".
I agree with your point, and I will be benefit from this style if it were the standard, too. But don't you think a good community culture can make people maintain a good git history for this purpose?
My daily job is a Linux kernel developer. I found that source codes are only the "What" part, git comments can and should state the "How/Why" part, and if all those still make no sense to me, I look for the original mailing list for the deeper "Why". Most of the time the information is sufficient.
Also, the whole point of putting something in a comment is that you have to read it when working through code around that comment. Putting a note in a commit log instead ensures that crucial information important to the code will not be visible, and most likely not read at all, when working on that code.
It seems to be easy to find plenty of projects with bad Git commits though.
IMO the big architectural guidelines and structuring and other highest level things and API contracts etc. should be in an external file (not code). The high-level details around a certain implementation in the commit logs for that file/files. Relevant implementation details and important things in the code. The lower you go, the closer to a comment line in the code you should get. This reflects also the rigidity of the code: the high-level architecture should not change often; if it does, then the architecture is not really ready.
Things should be certainly documented, but not in great detail inside the code files. The lack of documentation is in my opinion OK, the code is always the last word for how things actually work anyway. Misleading or wrong documentation are the absolute worst.
Sure, the back-and-forth on a mailing list or review system such as Gerrit can provide more of those direct links between code and explanation, but then it's often buried in a bunch of other discussion and dead ends. It's the "oh noes, comments can become out of date" times ten, so hardly a good alternative. And even when that's not the case, it's a horribly context-switchy way to get that information. Separating the explanation and the concrete expression just isn't a great idea. Funny how many people who would rather die than write a design spec also suggest that an even greater separation is a good thing.
If your "why" doesn't contains this, I have an hard time understanding why you want it in the first place.
The "why" is important to not make the same mistake. Theses others discussions and dead ends are all related to the issue, they are all questions that was asked/answered during it, they are all mistakes that may have been made.
Personally every time I only needed a tiny bits more context, the commit message was enough and if I needed more, the full related ticket was essential (and not a simple why, but the full though process that became that decision).
> Funny how many people who would rather die than write a design spec
I'd rather die than lose time over something I consider won't save time in the long term. Writing any documentation is long and if that time is longer than the few time we'll need it, it's a loss of time.
It's pretty rare that we need to go back to that information, when we do, the ticket information is enough and if it's not, doing the though process a second time isn't so bad the rare time it happens (which will provide more context by the new ticket discussion too).
For sure if your ticket just say: - Break when we enter text - FIXED
You are going to have a pretty bad time and you need to change that.
The "why" doesn't need to include every minor style nit (all the way down to variable naming) that came up during the review. It doesn't need to include every "I would have done it this way instead" comment which was in neither the original nor final version. That's noise, not signal.
> Personally every time I only needed a tiny bits more context
Lucky you. For much of the code I have read or written, that was not the case. In general it tends to be less and less the case as code climbs up the complexity/innovation scale.
> I'd rather die than lose time over something I consider won't save time in the long term.
Is that an unavoidable issue with the medium, or more of a reflection of how some people are bad at writing? I've whipped out a spec for a medium-complexity feature or component in under an hour, and been thanked for it five years later. Many times. If you've never had that experience, then I can only say I hope you'll be able to some day.
> when we do, the ticket information is enough and if it's not
Again, lucky you. Others have a different experience.
> Minor fix
> Showing 6 files with 171 additions and 203 deletions
The assumption is that the original author knew what he was doing, and if not, there was a code review and anyone can fix it if they see a problem.
Finally, it's code that probably won't be around anymore five years down the line, so detailed commit messages etc feel like a waste.
(Mind you, I don't agree with the above. At the same time, I don't want to do this type of meaningless throwaway work anymore)
Histories of active projects grow with age. A well maintained artifact should plateau in complexity.
This is not to say that there aren't reasons to write large comment blocks, or architecture documents, but that they are often better written not while the system is being first built, but later, in a maintenance cycle, when someone already had wished for the comments, and has regained the knowledge the hard way. By then, it's clear which part of the system are dangerous, suspicious and unclear. Where there's more need for high quality error handling, and where thousands of lines of error handling never get hit, because the failing case that was originally considered doesn't really happen in this dimension anymore.
Writing code so that someone, even a future you, can pick it back up and improve it when it's needed, while still delivering the code at a good pace is a kind of skill that many just don't learn, either because they are always in greenfield teams that never pay for their mistakes, or have an approach to maintenance that involves not becoming intimate familiar with a system, and instead either hack or rewrite.
But nobody looks great in a resume by saying that they are a specialist in software maintenance.
I don't think the "Lean Manufacturing" approach works here. By the time someone "pulls" you for a comment, you've already lost the most important knowledge that should go into the comments - why the code is the way it is. Maybe you'll recall it when asked, hopefully not missing anything crucial. Meanwhile, comments are extremely cheap to write as you're writing the code, and even before you're writing the code (you did spend the time thinking about what you'll write, and aren't just "coding from the hip", right?).
And writing documentation only seems like a waste of time to the developer who just finished writing the code. It wastes their time. But spending time now to write good documentation saves the organization as a whole much more of other developer's time in the long run. It's a smart investment to make, even when the collateral damage is time wasted documenting code that really is thrown out.
(But, if you actually document the code well you'll find out that you throw things out much less often.)
I disagree with what you wrote. It does not waste developers time, it wastes money of business owners. Developers are usually paid for their time even if they are reading HN instead of working.
Now question is to people who pay money if they want to pay for "something in the future maybe will be useful". They will say hell no! They want time to market to be as short as possible and as much of end user value delivered.
Now you take example of SQLite or Armin Ronacher those are exceptions. There is a lot more software that is not anything done by Armin and is not SQLite.
That said, I mostly agree... I tend to favor a discoverable code base, where the directory structure is organized over features... for example a given feature may have a few controls, some actions, state logic, api client for foreign system, etc... any of the above. I organize by the feature, not the type of files/components of those features.
I tend to favor using abstractions only when they are simple enough, or make the related code much more simplified and understandable. The exception to this is needed complexity, such as an enterprise product that needs to support being deployed on oracle, sql server, etc.
Simple, replaceable code is often better. That said, when you have a broadly used produce and a piece of functionality that is well used, moderately complex and unlikely to dramatically change, this level of detail isn't a bad thing.
Code without specification is a maintenance nightmare, a pure liability, a ticking time bomb.
And sure, unless you're creating the control system of a nuclear reactor/warhead you don't have to go full Coq and CMMI5 and "high assurance" and whatnot, but spilling a few sentences as a minimal kind of pseudocode before writing what you want (a function, a class, a method, change the build system, refactor a big ball of if-s) is almost ideal. It helps you double check yourself, and it helps reviewers too. (Throwing there links to some issue tracker is also nice, but just the links are not very helpful.)
If you have to write docs later, it's hacking (reverse engineering), not engineering.
> spilling a few sentences as a minimal kind of pseudocode before writing what you want
Isn't it simply repeating the code you'll write? Which in the end, will be just as bad as the code alone or will lose time of the one that will read it afterward.
Sure it's hard to write good code, sure it's not always obvious what's bad code, but that's a big reason why the review process is there. It's not only to catch mistakes, it's also to see if someone else can understands what the code meant, the context, the decisions, etc... Sadly many just go quickly over it and just check for obvious mistakes.
Sometime also a code is meant to be temporary but is later deems to useful not to be used but no resource is allocated to is maintenance (in this case we could say refactoring). It's the case of many internal tools.
I remember when I did my first internship, I was in the configuration management team and I was responsible for doing the smoke test. I made some tools for myself to speedup the process and once my manager saw how useful they were, he was interested of giving them to the QA department. I refused because it broke every few builds and I knew after my internship I wouldn't be there to maintains them and it would just create more problems. There's so many instance where it wouldn't break so often or that person wasn't considering he would no longer be there to maintains it or won't have the time.
If you need to read comments to understand code, you don’t know how to read code. Comments can lie, only code is the source of truth, once you gain experience you won’t bother with comments.
The file in the OP is Go. In Go, you are encouraged to write documentation and code in one place[0]. The end result is very nice, auto-generated, human readable documentation[1] which is tightly coupled to the code it documents.
Currently the only person in-office over Christmas on my first software dev job. Debugging a 50k LOC COBOL beast that digs into three other beast programs and ends up in a final 20k LOC uncommented piece where things are supposed to happen and be returned back.
Nothing is commented, the programs are huge and one can only debug one program at a time, requiring me to submit untested changes in one of them to the shared dev environment since I'm making changes in two, before debugging the other program.
If it just said somewhere what half of the stuff is I would save an insane amount of time getting to know the system.
my first software dev job. Debugging a 50k LOC COBOL beast
We found the real MVP. tapland, what were you thinking when taking this job!?I have no real previous development experience, only some Java and Python, since I chose an industrial engineering program at uni, and I got the offer the semester before my CS-classes (my chosen specialization) started.
Changed my university classes to equivalent distance-classes and took six months of school to get into training and a job.
Solution: write clear, simple, modular code that is self-documenting and does not need extensive commenting.
But here are a couple of Martin Fowler quotes (from his Refactoring book) I tend to follow:
“A heuristic we follow is that whenever we feel the need to comment something, we write a method instead.”
“Whenever I have to think to understand what the code is doing, I ask myself if I can refactor the code to make that understanding more immediately apparent.”
Key trick is writing comments as close as possible to the code they affect. Then it's hard to miss affected comments if one's not doing a shoddy job.
That also helps it show up in a PR so it may be caught in code review. Even so, IME comments get missed enough that over time I can't be as confident as we'd like that they match the code.
I think it's also worth calling out that this key trick is not being applied in the recommendation up-thread, which asks for a block comment at the top of the module.
https://github.com/torvalds/linux/blob/master/drivers/net/et...
Magic numbers and timing without explanation why. Rarely used technical descriptions such as "Lance mode". Tons of unchecked assertions on lock when kernel provides a lockdep check for this. Custom logging macros. Flies in the face of kernel "goto error" error handling convention in a few places. XXX with obvious bugs mentioned, unfixed.
And more...
Splitting it into more files wouldn't help at all.
If the comments are completely redundant with the code, they're bad. But you can't express as code why the some code is the way it is, and why it does what it does. After you tried to express in code every important information that's expressible as code, whatever important information remains must go into comments.
Sometimes writing the proof of why it should be like this is tricky, especially when complexity or performance is involved. (But then the tests are hard too.)
Linters are essentially constraint systems for code, but not for the program.
It's an odd feeling, having the best of both worlds.
Stepping back and whilst we can all overlook it, good code comments can make an enormous difference in productivity - both for an individual, a team and indeed a business. It aids repository knowledge (something that is easily lost between current and prior teams/individuals), which shouldn't be mistaken for the intelligence of someone looking at code...
I've spent far too much time personally and otherwise attempting to reverse-engineer code written by someone with no comments or explanation. At times, super experienced programmers/developers will take performance-enhancing shortcuts that the less-experienced don't understand; They'll compress routines and functions that are a result of their explicit knowledge of a language and/or domain but without explanation...
On a basic level, comments should: inform, educate, outline and help others understand the sometimes complex routines and functions that we all create and often under an enormous amount of pressure.
There are those that believe good code shouldn't need explanation and to some degree that's true, but you can't apply that brush to every codebase. Code can become complex, awkward, spaghetti-like and almost unfathomable at times.
I've always strived to teach less experienced developers to comment well, efficiently and with a little humor/humour (where possible); Something that allows us to understand code quickly, appreciate the efforts of those before us and smile/grin at the complexity of a challenge.
Personally, I don't really care about the code/comment ratio - It's a complete red herring. At times, code comments can be worth more than the code itself. At other times, they just help you get your job done; quickly, efficiently, no fuss, just great code.
I have yet to find a codebase that couldn't be made clear as soon as a programmer actually put some effort into doing so. Far too often adding a comment is used as an excuse to give up on making the code readable before you've even started.
1. Write some piece of code
2. Now write a comment about it
3. Is the comment adding more information, making the code more clear?
If Yes: Put that information into the code. Rename variables. Pull out code into subroutines.
If No: Delete the comment.
You’ll be amazed at how often this practice works. Doing it all the time will make your code more readable.
We have a second, enforced practice at work, thanks to code review. The question in your head is simply: “Am I gonna get a comment about this at review time?” If yes, you gotta make the code clearer/simpler/better. Because you’ll have to answer and address the comment, and that’s just gonna slow you down more than if you just fix the problem now.
Sure, in theory, you can do everything in code, but I find that usually this trade off is taken, as there's no time/budget to go that 80% extra time. (Or however the Pareto curve looks for the actual problem. And usually it's not less than 80, but more.)
Usually codebases are a mess, so were people commenting more on what's the goal then refactoring it would be easier.
Software is very susceptible to the "perfect is the enemy of done" mantra. Software works as soon as it works for the first time. And then it gets a shipped. It's 1.0, even if it looks like a mess internally. Because IT development is expensive, there is rarely budget to go that extra mile (which is again usually would cost a lot more than the first few miles).
One of my pet peeves is JS projects. I am not sure why but the front-end developers refuse to add any comments at all. This trend, oddly enough, started with ES6. Pre-ES6, JSDoc style comments at least, were quite common.
Just because some popular Javascript project doesn't have comments doesn't mean yours shouldn't. Straight-forward code may not need comments but most of these projects definitely need to explain why or how are things supposed to work or why things are done a certain way.
Code comments usually are not the best place to elaborate on "why things are done a certain way". One can also write documentation about the architecture, design etc.
> On a basic level, comments should: inform, educate, outline and help others understand the sometimes complex routines and functions that we all create and often under an enormous amount of pressure.
Take the time to simplify the complex routines, clean them up and make them readable and don't waste your precious time in writing "good comments".
> Code can become complex, awkward, spaghetti-like and almost unfathomable at times.
The solution for this is not "comment more". It's "clean up the mess".
> At times, code comments can be worth more than the code itself.
That doesn't make any sense. If the code has no value, you can remove it.
> I've always strived to teach less experienced developers to comment well, efficiently and with a little humor/humour (where possible); Something that allows us to understand code quickly, appreciate the efforts of those before us and smile/grin at the complexity of a challenge.
Please don't do that. Of course it's your code base, but usually it's best to find better venues to express your humor than code comments. Also: teach the junior developers to write clean code first.
Comments too often are used as a bad device to fix code. A developer first writes a horrible mess of code and stops. He may then realize that perhaps no-one will understand it. But if we now teach them not to clean up the code but rather just "write some funny comments", do you think it's any better?
I've seen too many code bases written with this attitude. The comments usually don't help at all, they distract the reader and they distract the author. It's too often useless noise. Many developers hide the comments from these code bases so that they can concentrate on what ACTUALLY is relevant: the code.
It is so frustrating to hear when someone thinks commenting more and adding humour is somehow cleaning up the code. But to then hear that they are teaching this to juniors just hurts so much.
Simpler advice to juniors would be to read Clean Code by Robert C Martin, apply some of those techniques and then politely ignore “more experienced” developers who think writing humourous essays as code comments is a good thing.
I’ve even heard phrases banded around like “the more comments the better” - I mean seriously WTF.
I like the analogy of...
When in an unfamiliar city, having no map at all is much better than a map that you have no idea whether you can trust or not. Code comments are absolutely that untrustworthy map, doesn’t matter how many code reviews or convoluted PR process takes place, you still need to read the code to know the truth so more effort making the code communicate is the key to maintainable code. Simple things like good variable names and grouping behaviour into functions at the same level of abstraction can completely negate the need for comments.
Comments have their place, but should be the exception not the norm.
What if the underlying logic changes? Do I need to update the original witty comment as well?
How do I know if the comment is still relevant? No compiler nor tests will tell me that and now I'm left with a task of parsing a language with much more degrees of freedom (English), without any support from the IDE. It becomes even harder in international teams.
This is a feature of several (mostly functional) programming languages, e.g. Haskell. Fun to see that often people figure out that these types of concepts are a smart way to write your code. Too bad it usually means many people reinvent the wheel instead of learning about computer science history and other languages.
An example would be head, which takes the first element of a list and throws an error on an empty list.
I really love Haskell but it seems as if they are going for something different here than what Haskell provides.
In all seriousness though, Rust does get a little closer - it requires exhaustive matching (or explicit opt out) and deliberate error handling (or explicit opt out). There are still ways around it, but the happy path in Rust is handling errors.... well, maybe not happy. It is a little verbose.
When I started at Oracle, on the first day my head near exploded as everyone communicated in ER diagrams instead of pseudo code or more procedural form. Learning to design databases at scale, and learning how far to go in making things metadata driven was hugely educational.
More recently, breaking up legacy application domains into microservices and defining the APIs between them feels to be like a "higher" skill than database design/object modeling.
Probably doing IoT projects would help bring some other skills to the fore.
That said, Haskell still isn't actually an example, chiefly because exceptions can be thrown by any code, and are reasonably often used in practice.
Unfortunately the odds of this ending up in a mainstream language this decade is pretty low: the extreme focus on terse code and DRY means the average dev balks at the verbosity (thus the coment in the linked piece of code being necessary). It's a shame, as it's objectively superior by many metrics.
Functional programming advocats, especially for the "pure" ones like Haskell, always strike me as odd. It seems that all the beauty of those languages make people obsess over that beauty and purity while keeping them from being productive.
Meanwhile, people with simpler languages like Go just get stuff done that is useful and makes people happy. Now if I _ever_ came across a useful Haskell product, I'd be happy to test drive it, shouldn't be a problem by now with all the container technology. But the closest I ever came to using a functionally developed product was RabbitMQ (written in Erlang). That one was _such_ a pain to use and operate — must have been the developers still dreaming in the purity of its code instead of writing some installation docs. I moved on to Kafka later and didn't regret it a minute.
Rant off.
There's at least git-annex and pandoc
Others were not so lucky: https://issues.apache.org/jira/browse/ZOOKEEPER-801
https://github.com/apache/kafka/tree/trunk/clients/src/main/...
The better burgers made by large commercial chains are more expensive though.
That said, there do exist useful products written in Haskell. For example, Facebook writes a lot of spam fighting code in Haskell. https://code.fb.com/security/fighting-spam-with-haskell/
Go and Haskell are really at different ends of a spectrum. Haskell is a very high level language, which makes building and reasoning about very complex software much easier (at the expense of a learning curve and fine control of space usage).
Go is a low-level language with no learning curve and a very limited facility to abstract (by design it seems).
Personally I would have written something like Kubenetes in Haskell or OCaml, with perhaps the odd performance critical section in C or Rust.
There is indeed a lesson here, but which one do you think it is? It's certainly not that McDonald's makes better (or "simpler") hamburgers: once you taste good hamburgers you can never go back to McDonald's (and yes, I make better hamburgers too, even though mine won't win any prizes!). To me, the lesson is that there's more to success than product quality. The established brand matters, the scale at which you can sell a (possibly inferior) product matters, how low you can get away with paying your employees matters, how well you can survive PR disasters matters, etc.
If you had to make burgers, would you rather make cheap and mediocre ones, or would you rather enjoy making premium burgers? :)
At the risk of derailing the thread, that would be the lesson I wish people would take away from that example.
Criticizing fast food like that is dumb signalling IMO; McDonald's!hamburger != homemade!hamburger. It's an entirely different product sharing the same name and some of the ingredients. It tastes different, and has a different form factor. People like this, even if many don't want to admit it to others (or themselves). When I go for a McD's hamburger instead of a foodtruck one, or when I say I prefer chain restaurant pizza over a home made one, it's because I want a different product. Treating the two as the same category is like treating tea and coffee as the same thing.
People only like the cheapness and the convenience (and perhaps the no-surprise factor).
Everything else being the same (price and time to prepare), nobody would eat McDonalds vs a quality burger (except the kind of people who eat Hot Pockets for the taste, but that's a much smaller demographic than McDonalds buyers).
No-surprise factor isn't that big of an issue if you're buying; restaurants tend to have consistent food quality & taste too.
Where McD’s really shines, though, is their breakfast offerings. And with that said, I think I will head there now for a quick breakfast (I live ~5 blocks from McDs).
It seems many or even most people can't tell the difference and the ambience and service is as important as the food.
It surprises me in the case of frozen patties like McDonald's though. A frozen patty looks nothing at all like an actual burger made from ground meat. It tastes differently as well, of course, but what matters is that it looks completely different!
It's all about the brand, unfortunately.
Functional programming, at its heart, is about using self-imposed constraints to avoid certain classes of programming mistakes.
If your application domain doesn't have big consequences for these classes of programming mistakes, then it can seem like functional purity can be a luxury or frivolous. However, if your application domain suffers greatly from those classes of programming mistakes, such as distributed systems, then it may be worth it to consider what functional programming might buy you.
So yes, just because you use a functional programming language won't help you sell your widgets or make a great product. And you can spend lots of time fucking around with it for its own sake and still not sell widgets or make a great product. However, if you understand what its constraints buys you, then you can make your job of building these things in certain domains much easier.
In this way, I rather like the tradeoff that the OP post shows: It's at least _possible_ to write Go code that's almost as defensive, safe and exhaustive as a safer language could provide, it's just a lot of manual work and discipline. For the small subset of code that needs these guarantees, it's possibly overall more consistent and efficient to take this route than splitting code into different languages with different strengths.
It comes down to trust. You're either fucking with your code in a powerful programming language that lets you do everything, or your fucking with the language restrictions to get your code to compile in the first place.
You can either go eat at McD's which sells pre-cooked burgers from minimum wage employees which kills the flavor and the taste of the meat being served but is extremely safe, or you can go to an upscale burger joint where artisans grind their meat in house and cook it to a perfect medium rare.
These days, I prefer the latter.
Now I want the powerful language and I can just slam it with generated tests to get the same level of confidence as the guardrail languages (in practice definitely, although I understand this is not true in theory, so unspit your coffee Haskell people).
Oh right and this is currently Clojure, so a functional language that definitely does help me make a great product. Less time implementing correctly means more time for product refinement.
Hopefully sometime soon I can share an example of this.
Badly chosen restrictions are not good, we can all agree. But what do good restrictions look like? Back in the day, programmers prided themselves on being able to do their own memory management, and bristled at the idea of a compiler or the runtime doing it for them. Now, that's the exception, as most of the time, we don't think about memory management in most languages we program in.
As time marches on, we'll find more of these restrictions that we all eventually agree are good practices, and the next generation of programmers will take it as a given in programming.
As a look beyond functional programming restrictions, I encourage you to check out Peter Alvaro's talk on Distributed Systems. Here, he talks about how queries over distributed systems is really hard to reason about, because time is now relative--there's no central clock to measure time. However, if we restrict ourselves to a language whose queries cannot express negation, a lot of the hard stuff about distributed systems go away.
> ...or your fucking with the language restrictions to get your code to compile in the first place
I think that's a wrong and outdated view on strong type systems.
A good type system is also an ergonomic one. What people start to experience is that the compiler is actually a friend that helps you write code and keep yourself true to your own promises. When new people start Elm, PureScript or Haskell (or another lang with ADTs and Type Inference), they might be a bit overwhelmed with the paradigm shift, if coming elsewhere, but if you are new to programming, there's nothing inherently more difficult in Haskell than in other languages—the cost of wrong code is just apparent earlier.
It doesn't come down to trust, it comes down to the realization that your mind can keep track of less information than a computer, and that you, as a programmer, are forgetful and make mistakes. The compiler is there to help you when you stumble over your own feet.
NOTE: I'm only including strongly, statically typed languages with type inference in the above. "FP langs" by itself is far too broad to be a useful categorization.
But I think the advantages that OP is lauding are also there, and "space shuttle code" might just be where it shines. Reading that comment in this post made me immediately think of Haskell. The Clojure components at my company fits the "space shuttle" description of importance, and its dependability is striking compared to the rest of our code. Part of it may be that being written in a different language allows its concerns to be separate from the rest of the application too, but I do think the FP paradigm is simply good in this domain.
I don't know. It seems like it would be easy enough to build an AST to make sure there were no unknown conditions that didn't lead to a return statement.
I wasn't really talking about type systems specifically, I was thinking along the lines of the Rust borrow checker here, and lower level programming like assembly and C.
> A good type system is also an ergonomic one. What people start to experience is that the compiler is actually a friend that helps you write code and keep yourself true to your own promises.
I see a lot of people make the mistake that type safe code is bug free code. "It compiles, therefore ship it."
> It doesn't come down to trust, it comes down to the realization that your mind can keep track of less information than a computer, and that you, as a programmer, are forgetful and make mistakes. The compiler is there to help you when you stumble over your own feet.
You trust the compiler to find bugs. Awesome. Luck be to you. I trust unit tests more than the compiler. Mostly because I wrote them. The compiler, I didn't.
By your logic, it's not enough to have faith in unit tests because you wrote them; you must also write the test framework and runner.
Swift, rust, c#, reason
Haskell is a highly opinionated reasearch language. This makes it a great languages to talk about. People could make many of the same points with, say Scala or Ocaml, but because those languages arent as opinionated, it is harder to use them as a basis for discussion than a language which is heavily opinionated about the subject in question.
Err, RabbitMQ is one of the easiest to setup and stabler queues/messaging systems. And I'm no user/fan of Erlang...
Much of theoretical physics is beautiful. Only when one takes it off the blackboard and into the real world does it turn ugly.
Kubernetes' reputation is just the opposite: that far from being a simple and useful thing, it's an overengineered, overcomplicated solution to a self-inflicted problem (deploying a distributed monolith).
> But the closest I ever came to using a functionally developed product was RabbitMQ (written in Erlang). That one was _such_ a pain to use and operate — must have been the developers still dreaming in the purity of its code instead of writing some installation docs. I moved on to Kafka later and didn't regret it a minute.
Erm, Kakfa was developed in Scala, whose advocates far more of a reputation for purist pontification than Erlang developers do. Maybe all that beauty and purity is actually good for something?
I would call the erlang system the "one white lie" category of purity on the FP purity scale. It is, by the way, a huge tradeoff that makes operating in EVM languages far easier than, say Haskell.
This is not what Kubernetes is for.
Take a step back and understand what you're criticising. You just look uninformed.
Thats why companies pay the buck to get 24/7 support. Because its that good!
Furthermore, why are you using the user interface as a yard-stick to judge language paradigms with?
Good examples of this translating to huge gains for the overall community are React + Redux.
I'm quick to admit that functional language ecosystems are not as mature, which might lead to lesser organizational productivity but no need to rat on functional programming in general.
Because I'm not in the business of making hamburgers nor am I interested in doing so.
Struggling to see this business coach's point. Is he saying that McDonalds makes better burgers than me because I'm not selling a million burgers a day? Because that's a load of crap.
E.g. Low cost, made quickly, scalable (easy to train workers from any culture, uses ingredients that can be sourced at scale across the globe, etc), consistency (burger is always up to standard regardless of time or place).
Serving customers is not a contest of who can make the best burger.
This:
#include<stdio.h>
main()
{
int sum; int x; sum=0;
for(x=1;x<=50;++x)
{
sum = sum + x;
}
printf(" 1+2+...+50=%d\n",sum);
}
Is more cognitive load and error prone than this: (println (reduce + (range 1 50)))
This is mostly what experienced FP advocates tell you. Btw. you can transpile the latter to the former.This disparity in numbers is in turn is primarily because golang/Python/C is inherently much more approachable than Haskell because the average programmer has cut his/her teeth writing imperative code for a significant portion of their early programming career. The jump to functional way of thinking requires a certain leap of the mind, that most people don't want to take the trouble doing because they seem to be happy "getting things done" in golang or Python.
However, that is not to say that we shouldn't be striving for correctness in code that Haskell fosters, or the inherent simplicity that it forces on you because you are forced to separate your IO and effect-full code from the pure bits. These are things worth striving for. These are broad principles worth emulating even when you are coding in an imperative language.
To turn your McDonalds analogy around: sure, a McD will let you just get your food requirements out of the way quickly and cheaply (i.e. just "get stuff done"). But in the long run, it's bad for you.
Haskell is a healthy salad to an [insert favourite imperative language] burger.
I really, really want to use Haskell because it’s type system seems neat in general, but the type system doesn’t justify all of the tedium. Go definitely loses on type system, but it wins in many of these other areas and so the trade offs are just better.
That said, lots of things are functional: chunks of Facebook, Twitter, and Microsoft (and I assume Google) are written in OCaml, Haskell, Reason, and F#. Jet is built in F#. Jane Street famously uses OCaml, and Scala is becoming the standard language for hedge funds. Spark apps are usually written in Scala. Etc.
- the Flow and Hack compilers
- Facebook's Messenger app
- Jane Street's entire trading infrastructure (and everything else they do)
- XenServer
- some parts of Docker, including the TCP/IP networking stack on OS X and Windows
This is just one of a common practice of programming, not a feature of functional languages. These practices are not even "reinventing the wheel".
I mean, this is obvious in many areas: from implementing complex logics like Kubernetes to making hardware drivers in C. Programming languages themselves can't automagically ensure every single conditions because these often happen outside of the program (e.g. targeting hardware state). We need to cover and test all cases by hand anyway.
On one project I work with guy with significant railway signalling experience and this is one of the issues that I'm somewhat unable to explain to him. Probably because our product's design target is not to fail-fast-and-safe but fail-secure.
I didn't see that.
In fact this speaks to me as mission critical software like this needs to be as tediously documented as possible to eliminate surprises. Those branches and conditions are collected through a huge pool of trial-and-errors, implying Haskell can provide those valuable use cases out-of-box is misleading, no it can't.
Model checker like TLA+ or Pluscad might do the trick.
But the true complexity is coming from states, and combination of states, and many of those are unknown to the developer until certain incidents kicked in. Good programming practice can't relieve programmers from the cognitive burden of reasoning the outcome of those combinations.
In this case right here, what's your counter-example, or what would you use instead of their specific approach?
No, or at least not often enough to be worth thinking about. It is of course possible to use abstractions badly, but the problems that business software has to solve are always fairly simple because they're human problems; human business processes were never that complex. So if a program looks really complicated, the overwhelmingly likely cause is failing to use appropriate abstractions.
> In this case right here, what's your counter-example, or what would you use instead of their specific approach?
As others have said, a result type would greatly simplify this code without sacrificing any safety. No doubt after such a simplification made the code shorter and easier to comprehend, further simplifications would become apparent.
It’s also an open source project, so if anyone wanted to throw up a branch with an example of simplification without sacrificing logic branch completeness that door is open. Code might even convince the k8s team to change what you think is misguided behavior.
Sometimes the best software is no software. Kubernetes exists to solve problems that people using better languages don't generally have.
> It’s also an open source project, so if anyone wanted to throw up a branch with an example of simplification without sacrificing logic branch completeness that door is open.
The "Please do not attempt to simplify this code" comment suggests otherwise. In any case, the team have chosen a language in which good solutions are not possible.
This statement doesn't make sense, and the arrogant tone just stinks of ignorance.
Kubernetes isn't perfect, but if there's better provider-agnostic language-agnostic way to dynamically scale an application written in heterogeneous languages across a cluster or on a competitive choice of cloud platforms with dynamic provisioning, I'd like you to tell us, and explain why it's better than Kubernetes.
Language agnosticism isn't something to discard lightly. Different languages have different strengths and weaknesses.
(The JVM startup can be a bit slow for scripting one-liners, particularly if you're running them on every file in a directory or some such. There was a time when I would switch to Python or Bash for those, but I realized I was wasting more time making mistakes in those languages than I ever saved at runtime)
That’s a falsifiable claim. Human problems / laws are some of the most difficult to code for, IMO. Examples: legal software, economics software, etc. Usually complexity increases as you delve out of the abstract and deal with real-life processes with humans.
Not only does this add the stricture of a compiler enforcing correct return types for all conditionals, it is also idiomatic of any language that supports this.
Having said all that, changing languages is rarely an option. Maybe this is the best solution given the tool available. If so, it demonstrates that maybe this tool isn't right for this job.
What in this <2k LOC file suggests strict types help anything whatsoever? It's not massive. It's large, but fold this and it immediately becomes more readable. Any attempts to simplify this would simply be reasoning about it differently.
It must be understood that complexity is an economic consideration and not either a financial or technology consideration. It is always already present. Complexity is not something that is created or destroy; but retained, absorbed, or transferred.
Abstractions become necessary when they separate different functional layers to perform different respective responsibilities. In this case complexity is transferred both in and out of a system at a given layer, but you don't care so long as you aren't doing the jobs of the other layers. That is referred to as separation of concerns which results in the hardening of a system (risk reduction) which is the side effect of reducing costs due to restricting requirements available.
Many abstractions exist solely to provide a layer of convenience. In the case where an abstraction does the same job as the code it abstracts risks increase and costs increase. This is because the system continues to simultaneously absorb and transfer complexity like described above, but the requirements between the various layers isn't clearly separated. That results in fulfilling requirements, the same requirements, simultaneously at various layers. This has various names like scope creep, technical debt, and so forth. This is bad because risks and costs increase directly to the correlation of increased code and increased requirements. This is what makes the law of leaky abstractions valid.
It is easy to tell the difference between a necessary abstraction and a wasteful abstraction by the forcefulness of separation. If you can perform the same job in a lower level the abstraction isn't necessary and you are probably better off without it. Most JavaScript frameworks are unnecessary abstractions.
func (ctrl *PersistentVolumeController) syncUnboundClaim(claim *v1.PersistentVolumeClaim) error {
the variable "pvc" seems to have been renamed into "claim" and "pv" into "volume", judging from the code/comment mismatch. Comments in the lines 339, 358, 360, 370, 380, 395, 411, 422, 427 point to the old names. Furthermore in line 370 the comment reads: } else /* pvc.Spec.VolumeName != nil */ {
while the matching if is: if claim.Spec.VolumeName == "" {
So not only the variable name mismatches, but also the comment is wrong. The VolumeName seems to be a string, so is never nil, the else comment should specify that the VolumeName is non-empty.The more verbose and detailled the comments are - the more work needs to be spent in ensuring that they are correct.
FTFY: …hopefully!
Only if they’ve used a language with Algebraic Data Types support, the compiler would enforce that “every branch and condition is considered and accounted for.” The only PL with ADT that I’ve used was Haskell, but I’ve heard that Rust has them too “enums”.
People are arguing that “code is what computer executes, comments don’t ensure anything!” and so on, but besides being executed, (this Go) code does not ensure anything either. It’s human eyes that skim through all the cases, and look for a matching branch for every one of them that “ensures.”
In my humble opinion, this so-called “space shuttle style” is just one of the many workarounds to deal with Go’s by-design limitations (the most famous one being lack of generics), a language that’s designed only 9 years ago.
Two years ago I’ve moved one of my biggest projects from Python 3 to Go because my program was inherently concurrent, and at the time -and I think still- Python has 3 competing approaches for concurrency: threading, multiprocesses (for parallelism), and asyncio.
Although it’s nice to have a variety of options, I think this balkanisation affected the community in negative ways because the options are not compatible with each other (see the emerging “SansIO” libraries for asyncio).
This is actually the reason why Python gets so much praise: thanks to its standard library, there is often a single canonical way to do something (if you need sets use `set`, if you need matrices use NumPy, …), which means that the vast majority of libraries are interoperable* with each other. Same goes for Go when it comes to concurrency, and that’s what guided my choice.
*: high cohesion, low coupling
This a thousand times. The praise in this thread is disturbing.
The absurdity of this code is the logical conclusion of ignoring decades of PL advances in favor of Go's "simplicity."
When you insist on "space shuttle" era language design, is it any surprise when you're reduced to "space shuttle" era programming? I can't imagine anything more fitting.
There are no perfect languages, that is certain.
Worse, they've made an exception for simple error checking, and the result is that the majority of the if statements at the top of the file have no else condition. Quickly scanning by eye doesn't help me determine if someone screwed up and missed a scenario.
- "It's the "jazz music" of software development."
- "...breaks all the "rules" but does so purposefully..."
- "this is irreducibly complex, and cannot be split into multiple files"
- "that smallness-of-file or smallness-of-function is not a target to shoot for"
I am wondering: can't all the above statements be said in defence of any poorly engineered, gargantuan single page code?
No.
Yes.
Yes.
Also, poorly engineered code is seldom irreducibily complex.
edit: seems to be a custom language designed for the purpose. .agc file extension corresponds to apollo guidance computer.
https://www.youtube.com/watch?v=2KSahAoOLdU&index=1&list=PL-...
However, it does give me some comfort. When it’s not gamed, do other HNers also feel that a high comment:code ratio probably indicates quality?
There are reasons why this may be the case. (More thought, more time and a large team etc)
I don’t advocate using this measure to reward anyone because it would be gamed immediately.
Comments when done correctly are vital.
Correctly done comments are a one in a million thing. In my experience, they are utterly surpassed by "i = i + 1; // increment i" style comments. (Seriously, I'm working on code written by someone who teaches programming and he writes this type of comment.)
The code to do this is simple, but not concise, leaving a comment so I can scan the function and skip 5-10 lines doing something trivial is nice.
And then there's something I recall running into, a decade ago:
using namespace std; // using namespace standardIt wasn't even "using standard namespace"...
And if your code is clean, you shouldn’t have a bunch of redundant comments explaining the obvious.
I absolutely do not comment enough, but knowing this, I try to stick to the principle that if I have had to stop and think through an expression before I write it, then I am likely to eventually thank myself for leaving a short explanatory note.
And moreover, it may not be me scratching my head over that nest of ternaries in a year's time - it may be some other poor soul. And while that poor soul won't thank me for leaving a comment, he or she will certainly curse my name - quite possibly vocally and publicly - for not leaving one.
There’s nothing worse than going back to a codebase from a year ago and seeing a couple magic numbers and having no clue how they came to be, haha.
If you're doing something obvious, you should generally be able to program it concisely, in which case you have a high comment-to-code ratio because the amount of code is low. Sometimes this will be because you're importing an external library to do something, or because you're calling out to an internal library. Sometimes this will be because you found a straightforward implementation. If you're finding yourself writing hundreds of lines of code to do a single obvious task then chances are high you're implementing it poorly (and, specifically, in a way where your defect rate is likely proportional to the number of lines of code).
And if you're doing several obvious things, then the point of the code is not to explain what the code is doing, but why it's doing that. What is the business purpose of the code? Which customer cares about this edge case that you're handling, and under what circumstances can you stop handling it? Why did you decide that the common library wouldn't actually work here? If you're converting data from an awful legacy format, why are your ingesters / parsers for the legacy format designed in this way? If you're micro-optimizing for performance, why are the optimizations sound (i.e., why do they accomplish the same thing as the unoptimized version), how do they work, and why did you decide these spots need to be optimized? Each individual thing you do might be obvious on its own, but the arrangement of the whole thing needs comments for each step, which again gives you a high comment-to-code ratio.
I just meant simple to understand variable, function, and class names. That combined with small classes and functions, makes following the logic of your program extremely easy.
Following concepts like DRY (don't repeat yourself) and the single responsibility principle ensure that you're making more easily testable code, and I'm sure less overall LOC.
// The binding is two-step process. PV.Spec.ClaimRef is modified first and
// PVC.Spec.VolumeName second. At any point of this transaction, the PV or PVC
// can be modified by user or other controller or completely deleted. Also,
// two (or more) controllers may try to bind different volumes to different
// claims at the same time. The controller must recover from any conflicts
// that may arise from these conditions.
How would you rewrite the code so that this information was explicit in the code alone, and as obvious as when it is stated in these comments? Note that simply being able to handle any conflicts that may arise from these conditions is not necessarily the same as saying that they can occur and must be handled, as any particular implementation is invariably an over-specification.As for redundant comments explaining the obvious, that seems to be something of a straw man, at least in my experience - personally, I have very rarely seen such code. The person who is not motivated to write useful comments apparently prefers to write no comments rather than useless ones.
Additionally, in no way am I advocating for no comments, that's obviously not possible (like your example). Comments are useful, even necessary, for code that might have an otherwise confusing logic to them.
I've seen plenty of code with documentation for a method with nothing more than:
/**
* Bills the user
*
* @param user The user to bill
*/
public void billUser(User user) {
//
}
In my opinion, that comment is completely redundant, and I think it's driven by the idea that we should comment EVERYTHING.With regard to the sample, then if that is your experience, I cannot deny it, but, irritating as it may be, it seems fairly harmless. It appears to date from a time when it was thought that extremely prescriptive coding style standards was the way to fix programming - an even less realistic belief than the idea that code can be entirely self-documenting.
I see this as someone trying to fool a linter that demands they have documentation. I think it's better to say "comment everything" because it puts documentation as a first-class consideration rather than an afterthought.
https://github.com/miohtama/aliens-vs-predator/blob/master/s...
Comments are very barebone. There is structure, but needing to mess with this kind of code would be scary. Granted, most games are write once and never look back.
// It might look like you should do X here but esoteric reason Y dictates that you should do this instead.
Core, critical plumbing/logic at the kernel of business critical, long-lived applications, will be the source of my stress-dreams long into the twilight years of my life; in the form of a lack of documentation and a presence of organic growth.
To criticize myself quite bluntly: If the core code I worked on at work looked like this, I'd feel a great deal more comfortable in some of the changes/digging that inevitably arises.
I would never use it as an absolute metric; but I'd use the level of comfort e.g. a new dev feels when looking at something that might otherwise be a spiderweb and saying "Oh this makes sense" (As I do when looking at OP) as a north star for the most sensitive bits of logic.
It is in no way a guarantee that we got them all, but after spending so much time reason in through why those 'else' clauses were correctly empty, we thought it would be rude not to write it down.
In truth it was as much for future-me as anyone. My memory is know. To be spotty. :)
I think this provides me higher quality, less bug ridden results. So if others use comments in this style I would tend to believe it does increase code quality.
If a line of code doesn’t match the comment, something is clearly wrong. ;)
It is absolutely useful to do, and really not too difficult.
Languages that allow for more formal assumption-checking (especially before runtime) are even better, but comments have an additional benefit of being understood by a human directly.
I wish languages with static analysis could somehow allow authors to encode human-friendly/sematic errors that you often see as runtime exceptions into the static analysis itself...
I think around 10% of your code as comments is a good measure, but also remember that you may not revisit a module for years, and you will come to appreciate each and every breadcrumb you left which leads back to your original state of mind when you wrote it. If you measure code quality as maintainability, then comments can indeed increase code quality, just non-linearly.
On the other hand, as soon as someone not safety minded gets their hands on it, trouble happens. Comments aren't updated (and there's no way to make sure they are checked, other than GREAT code reviews by the original authors, usually with at least two or three people doing critical reviews). Then the comments can become misleading and a liability as people will take them for truth, as they should.
If you have code that has a lot of subtle dependencies or edge cases, really great comments can help enormously.
To me, comments are noise, and code is signal; the code is what actually executes.
It's one thing to have a summary of intent at the start of a listing, that should not count towards the code:comments ratio.
Once the code begins however, there should be a minimum of comments necessary - especially in a high-level language not constrained to assembly-level instructions.
In assembly listings it was common to have two columns, the code on the left and comments which often resembled high-level pseudo code on the right. Here's some representative apollo guidance computer source:
MAKEPRIO CAF ZERO
TS COPINDEX
TC LINUSCHR
TCF HIPRIO # LINUS RETURN
CA FLAGWRD4
MASK OCT20100 # IS PRIO IN ENDIDLE OR BUSY
CCS A
TCF PRIOBORT # YES, ABORT
When you're already working in a high-level language like C or Golang, you should be able to clearly communicate what is going on without the need for littering it all with comments.https://github.com/chrislgarry/Apollo-11/blob/master/Comanch...
You did, to be fair, say that comments at the top shouldn't count. But I think that depends a lot on personal (and language) style towards multiple files and multiple units within a file - for instance, none of this code is object-oriented, and comments above each class make sense in an object-oriented language. I think as the language gets more concise - Go is a lot more concise than the Apollo assembly language - you're going to need to have the same amount of prose to explain what you're doing but a lot fewer lines of code to get anything done, and it makes sense to have comments above each function or each block, because that's really the comparable unit.
Comments are noise to the compiler, but code is both a communication between humans and from humans to machines. To imply that only what executes is signal and all else noise is to ignore half the purpose of code, which is documentation.
And despite what a lot of people want to believe, code itself is often not sufficiently self-documenting.
If you don't want to spend your career rewriting the same thing over and over again for slightly different business use cases and platforms, comments are incredibly valuable. (On the other hand, I guess there's a lot of job security in being hired to write the same thing many times....)
I consider it a big risk of errors.
When some code is changed, will all related comments be rewritten too? I doubt it.
And then you end up with a codebase which indicate A but comments which clearly spell out B, and you as a maintainer have no idea what to believe.
DRY. Don’t repeat yourself. The comments should not double up for the code. That’s just future maintenance nightmare.
If every function is just a few lines long, the comments are easier to keep synchronized, and if a function drops out of service, it should eventually be garbage collected with its now-irrelevant comments.
Kind of the whole problem is when there are weird corner cases going on that straddle function boundaries.
I'm not saying that's a good thing; mind you - but nor is it always trivially avoidable, especially if code needs to be concurrency and/or exception safe -- or in general whenever the statements your function consists of have surprising and opaque behavior based on system state, particularly if said state is hard to grasp due to being implicit or dynamic, or simply large and complex.
It gets a little tiresome threading the relevant state to every function that needs it, but it’s worthwhile in the end.
The fundamental issue remains that sometimes your knowledge about that state (whether the classical kind or a proper parameter) can be complex and dependent on what happened elsewhere, especially if the codebase your in was grown into that situation, and not designed like that per-se. A comprehensible set of preconditions and postconditions isn't always a luxury you have, certainly not at first.
If the problem has "hub and spokes" topology, i.e. it's relevant to multiple places in code that all reference a single location, put a comment describing the issue in that single location, and everywhere else put a comment with a reference. //Warning. See comment in [that location].
If there's no single best place for the detailed comment, put it in some design notes file, and put a comment with a reference to that file in all the affected places.
DRY can, and should be, applied to comments as well.
I'm a bigger fan of WET(Write Everything Twice). Usually the first iteration of a component you don't understand enough of the domain space to get the abstractions right. So use that first attempt to explore the issues/problems/corner cases. Once well understood, rewrite it into something concise and well abstracted.
I've also find that if you try to re-write a third time you'll end up being to clever in trying to predict where a system will evolve and get you right back into the same situation as the first iteration.
Everything in moderation, including moderation itself.
Can you name a few examples where you encountered this? In my career (30 years programming) I've never seen it. I believe it's a common, poor excuse for not writing enough comments.
The benefits of comments are well-understood. For me personally they often helped compensate sloppy code, (non-obvious) assumptions and prevented bad solutions because I reconsidered while writing (embarrassing) comments.
when you have to maintain a large codebase modified thousands of times in 15+ years, every single comment is invaluable.
//here we go baby!
//do not touch!
//1 ... //2 ... //3 .. //8
The last one apparently was to indicate the order of some overengineered stuff that could have been written properly.//This happens only for CompletedOrders (while in reality it was the opposite)
//This calculates the notional in the same currency (while it was converting it in GBP)
//This is extremely important, never delete (and after that there was a bunch of commented code)
Honestly I don’t remember all of them, I tend to use my memory for more important things, but I bet that if you had seen only 1/10 of the bad comments that I have encountered you would have a different opinion. Btw in your post you are explaining exactly why I hate comments:
“For me personally they often helped compensate sloppy code” - the solution is obviously to fix the sloppy code, not to write a comment because you are too lazy to fix it
“(non-obvious) assumptions” - this is probably the only legitimate reason to write a comment.
“and prevented bad solutions because I reconsidered while writing (embarrassing) comments.” - this has nothing to do with the comments given that at the end you didn’t write it. Thinking more about the code instead of trying to comment it gives you the same result, if not even better.
> “(non-obvious) assumptions” - this is probably the only legitimate reason to write a comment.
If that's what you think, I rest my case...
As someone who is clearly a fan of comments, are you saying you’ve never modified a comment to make it more accurate to what the code is doing?
And a bit more on the same from clean code: http://www.kyleblaney.com/software-blog/2012/6/29/comments-a...
obviously
More lines doesn't mean more complex, it's the same logic just the logic is named now and more reusable. It's possibly not the best example, as the logic is minimal, but when the logic becomes more complex, wrapping it and naming it becomes very powerful. We're creatures of abstraction.
> You've made the code more generic for what you currently think future changes are going to look like, which may or may not be accurate.
I'd argue that I've reduced the number of reasons the code has to change, which should be a goal while programming. If we change how we calculate a leap day, we don't touch how we modify a leap day, which means we're less likely to cause adverse side effects.
> And in the process you've split dateIsOnLeapDay and convertLeapDayToPreviousDay into separate functions, so if someone is tracking down a bug in line 10, they need to jump to lines 20 and 21 to figure out that the associated code is in line 15
They should be separate functions, they are separate things.
> In a large program these would get even further separated over time
Is it really a problem if they are separated? What links them? There could be plenty of reasons for wanting to call one without the other.
Maybe this is just different instincts/experience and I'm not saying you're wrong, but my feeling here is that you do actually want to change them at the same time. Suppose we decide instead of adding a day to February 29, we keep the months the same and add a festival day at the end of every fourth year, numbered 13/1. Then modifying the festival day to 13/0 is wrong - the day before 13/1 is now 12/31.
If you have one function for "fix leap days for reporting purposes" then you're fine, and you've set the abstraction in a good place (or at least good for my example case, I will totally concede there are other examples!). When you edit the is-it-the-leap-day line of code, you'll see the subtract-one line of code directly below, and if you forget, your reviewers are likely to notice. And you haven't really made things noticeably worse for the case where the customer says "Actually we need February 29 rounded to March 1, instead", it's not distracting to have that line of code above where you are (and if anything it's useful to have that comment, so that if this is a different customer asking you realize that you need to not break expectations for your first customer).
I am something of a skeptic of reusing very short pieces of code - for instance, my team's own codebase has a poorly-designed function for calling a subprocess and swallowing certain types of errors from a very specific command, and in a code review I had to tell someone to just use subprocess.check_output(), which does the same thing but without the modified behavior which they probably didn't want. Abstraction makes sense when there is a meaningful concept to abstract. (Similarly, I am also very much not a fan of getters and setters; I think most people are better off with a structure with public fields, because I have very rarely seen it be useful to convert a trivial getter/setter to a non-trivial one without bothering to look at how callers use it, and it is useful to rename the field and see which code fails to compile / no longer passes tests now.)
transactionsLeapDayAdjusted = transactions.map(t => t.date == '29-Feb' ? {date: '28-Feb', ...t} : t)
One thing I always push back on is references to ticket numbers or other external systems (except perhaps e.g. ISO standards). Repos should be self contained and perpetual. One might not have access to the ticket system now, or ten years from now.I've been on Rails/React teams where comments were seen seen as a possible smell. Not talking about useless literal comments, just that their need was seen as pointing to possible bad design and that a well factored codebase was self-documenting -- ie. if you had to comment something, perhaps methods/vars were poorly named, SOLID principles were not adhered to, methods needed to be broken out, or it was just a sloppy approach. Even explaining design decisions was considered more in the domain of git messages and having nicely packaged atomic commits.
While I see that aspect of it, there's no getting around the constraints of the real world and that some problems are just difficult and much easier to grok with a user guide in plain english, so to speak. And mission critical stuff needs as many safeguards as possible.
That said, inaccurate comments can be dangerous and when your code is highly commented there is real danger things can get out of sync. If you're working on a 5000 line file that 100 developers have touched over a 20 year period... and no one has taken it upon themselves to do a recent comment audit, there be dragons.
I find that comments can be a last ditch effort to make hacky convoluted code look better than it is. It can be an indication of lack of thought and planning and later obsessive documentation to make up for it.
Tests can help understand the interface, but they don't help to understand the rationale behind it, the underlying abstraction, or implementation caveats.
One time, I inherited a big steaming pile of spaghetti my co-worker wrote to meet a tight deadline, before being shifted to another project. That code implemented one of the key functions of the application, and half a year later, the customer demanded extensive changes. Believe me, I would have paid half my monthly salary the just to have a third of the comments that we see in this Kubernetes file.
--
[0] - http://antitrust.slated.org/www.iowaconsumercase.org/011607/...
Comments become useful when behavior is implicitly tricky. Ideally you'd make the "trickiness" tangible and expressible in-whatever-language you're in, but that's not always easy to do.
A high comment to code ratio, where the comments are of the form "Do this thing in this way," indicates a lack of quality - generally a sign that the programmer is not confident enough in the language that they're writing in, and is trying to solve language-level problems instead of business-level problems.
Uncommented code better come with some reference for why the code exists in the form it does. Sometimes commit logs and the VCS "annotate"/"blame" feature works. Sometimes commit logs link to bug trackers or feature requests. Sometimes there's a README. If you don't have any of those, I tend to find that it's generally low-quality code.
Our purpose is to deliver business value. (Or non-business value, as the case may be; if you're writing a free video game for fun, you want people to successfully have fun.) Our purpose is not to generate lines of code. All code is, to some extent, legacy code; comments can help it be manageable legacy code, or make it even more unmanageable.
Elsewhere in this thread Ousterhout's book is mentioned; I like his advice about always placing comments in the most obvious places and as close to the code they affect as possible. This way, you can't miss them, and and it's hard to forget to update them.
Woah, that sounds like a pretty dumb feature. Auto-collapsing whole functions is useful, but auto-collapsing docstrings sounds like a recipe for disaster. People write docstrings and inline comments for a reason.
Comments don't need to be near the code they affect. They don't even need to be in the same file - consider if you've organized your code by features, but the comment relates to a layer than spans multiple features.
Eventually I tracked down this line:
// writes to the log file at c:\...\xyz.log
AppendToLog(message);
Yeah, that was the correct path and everything, and yet the line wasn't executing!Eventually I looked inside the AppendToLog method. It was writing to another file in a completely different path :)
That was when I stopped bothering to read comments. They always lie, and I couldn't even blame the programmer who changed the AppendToLog method -- the comment wasn't inside the method, it was on a call. I can't honestly expect someone who changes a method to look for all the places where that method is called and make sure any existing comments match the change.
This file isn't that heavily commented. Do you look at many OSS projects for comparison? Though when things get complicated with many branches and function reentries it makes me wonder whether the problem would have been better solved with declarative logic that handles the procedural mess for you. (It might also be much higher quality since you may unlock access to various formal methods and go beyond unit tests. Though perhaps for example there's a vetted TLA+ spec not shown that this controller is based on.)
I don't think doc'ing every function is unusual, usefully doing so is less common though. Comments in the function body also aren't that rare, though it might indicate a place for better factoring e.g. just more function calls on descriptive/suggestive names. (Having more functions will help in not having to stub out (and deeply stub) so much in a behavioral test, too, since you can get away with just mocking the function call instead of the potentially hairy state logic the function does underneath.)
I see an example at a random spot for a couple improvements in naming (in my ignorant opinion, I don't know about kubernetes) -- though the fact I feel able to express even a weak opinion on an improvement suggests the comments were reasonable. I've seen code less hairy but with no comments or useful tests and without a need to really understand it I just want to move along pretending I saw nothing.
Look at the set of ifs at L591. The first if is a null check with part of the explanation on L592, better to remove that part and have a function call, something like "claimWasDeleted(claim)". The matching else if on 615 checks for an empty string name, I'm not sure but I think its explanation is at L634 and the empty string check could be "isClaimPending(claim)", and maybe move the mode mismatch check to its own else if before the isClaimPending block and give it a better name. I appreciate the comment on L635 telling me why the next line of code on 641 is done (it may likely not be clear from the commit history, which can be another place for whys) though with the isClaimPending change the comment and code might be replaced with a fn call with the details in the fn doc. I'm also reminded of an idea in more expressive languages to annotate purely optimization metadata of any kind (inlining being the simplest) and being able to toggle it on/off for extra QA in a test suite. Anyway the next elif on 643 and its comment, could be something like "isVolumeBoundToClaimProperly(claim, volume)". You get the idea.
I think people should spend more time commenting/documenting across the board. I'd much rather have verbose commenting that is unhelpful that I can skip, versus minimal commenting and code that is overly optimized and hard to parse. The less thinking I have to do to pick up where the last person left off, the better.
I would say that yes, in general high comment:code ratio tends to be higher quality.
There are three kinds of comments: why, what, and how. How comments are almost always a sign that the design is poor or the complexity is too clever. Why comments are necessary to understand the code and are almost always a good thing. What comments can be useful guideposts for skimming code, but they are also extremely prone to code rot. I suspect what comments generally end up being neutral in a net value proposition.
You want a high ratio of why comments to code, but I suspect most high comment-to-code ratios arise from what comments, which severely attenuates the utility of a pure comment-to-code ratio.
Just the opposite. It typically indicates a history of rigorously documenting terrible code. Sometimes, comments come from complex business requirements or other external constraint. Documenting the former is largely an anti-pattern while documenting the later is hugely useful.
Nope, imho code with lots of comments is generally crappy code. It's littered with comments to explain the sloppy code they couldn't make clear because they're bad programmers. Good programmers use few comments, write simple clear code that doesn't require explaining, and leave comments about why something was done rather than simply trying to explain what the code does.
Code never lies, comments often do; don't trust comments that explain the code.
That's always been my instinct.
For example, line 463 shows:
if !found {
// handle missing
} else {
// handle found
}
I would simplify this to: if found {
// handle found
} else {
// handle not found
}
Or even: if missing {
// handle missing
} else {
// handle not missing
}
The test-negative style is repeated throughout the file, but inconsistently. Sometimes the negative is tested in the if branch, sometimes it's tested in the else branch. Why?This is almost the worst of both worlds: the trouble of wading through a pile of words, together with the lack of clarity.
15 months later, they moved to a less stressful team, the app is not functioning because the servers got upgraded, and you wonder how the hell this thing works. And if it doesn't work, you have 60 hours of redevelopment to do.
This is the cliche that needs to be put to rest.
Yes, often there are tradeoffs. But just as often one thing is better than another thing, and there is no tradeoff.
A worldview in which everything has pros and cons and is ultimately subjective is fertile ground for entrenched habits, because it means never having to admit you're plain wrong, that there is a better way, or that other approaches are simply that much better than yours.
That sound's awfully like postmodernism which is terrible everywhere it's applied, not only programming.
In particular Go (like C) has no built-in exception "throwing" / unwinding support, so for any function call where you want to pass an error onto the caller, you need to do something like
result, err := try_to_get_a_result()
if err != nil {
return nil, err
}
See also https://blog.golang.org/error-handling-and-go . As far as I can tell, all of the if-statements without else-clauses are doing just this.(It would be nice in theory if there were better language support for making this lexically obvious but they're in a language where that's not doable.)
For instance, this would not be a simple error check:
server, err := find_current_server()
if server != nil {
...
}
because if find_current_server() believes that it's a non-exceptional case that there might be no server at all (i.e., it might return nil, nil instead of nil and an error), then you absolutely want to handle that case.Besides, this syntactic rule is not implemented by code but by the author and reviewer, who should know what the function returns. (And shouldn't name it "err", then.) They're doing the best they can in a language without syntactic support for what they want, I think.
There's probably a lot of wisdom in this file embedded in a lot of noisy Go code. If someone tries to implement this code in a different programming language, a lot of this wisdom would have to be painfully extracted from the Go code. Some sort of extracted decision table format (examples [1]) would be objectively easier to read, could be rendered in different formats, hyperlinked, etc. Maybe a small subsection of this code could be generated from some structured data specification.
Mbeddr [2] is an example of a language that allows embedding decision tables directly in C99. It can also embed state machines and other goodies directly into the code. This is just one way to go... I'm sure some ppl would complain about lock-in in a specific IDE.
It is not clear to me exactly which domain driven techniques could be applied to this particular piece of Go code, but it could be worth it to find that out.
From what I see out there model driven design seems to be applied to areas were really convoluted, and some times nonsensical logic needs to be mapped to code, like in insurance policy management, and stuff like that, or for other complicated code like firmware, drivers, protocols, etc. Why can't we apply these techniques to general programming problems?
1: https://medium.com/@markusvoelter/the-evolution-of-decision-...
More seriously I’ve done this style unintentionally before in JavaScript for a similar situation with plenty of branching logic. Stuff that easily could have been have the lines but not as easy to verify by hand. I’m happy to see it being formalized for certain scenarios.
if claim.Spec.VolumeName == "" {
// ...
} else /* pvc.Spec.VolumeName != nil */ {
maybe some cleanup actually would be a good thing.Also HN: c++ and java are awful! They're so verbose and I have to type so much!
// 1. Every 'if' statement has a matching 'else' (exception: simple error
// checks for a client API call)
Comment rot? The code is filled currently with else-less if statements, not only for error checking.I have not cared to read the file much further so I can't comment on whether comment rot is really prevalent.
Actual "space shuttle code" is AFAIK built to satisfy a specification, not to serve as a specification in itself. When you find that the specification is lacking or somehow faulty, you get the people responsible for it to revise it, wait for the new revision and adjust the code to match. The key is that they are two separate processes. Someone responsible for the specification doesn't need to be concerned with the exact details of the implementation, and those responsible for the implementation only need to be concerned with satisfying the specification.
When the specification and implementation is all mixed up as prose and code in the same file you don't get to enjoy the benefits of separating those concerns. Changing the spec is the same process as changing the code, and the order of change is indistinguishable. You can change some code, but then you have to find all the references to the code you changed in the prose and adjust that too. You can change the prose, but then you have to adjust the code accordingly and any prose that assumed that the code worked as before. You'll be relying on tests and reviews (reviewers suffering the same drawbacks as the submitter) to enforce this, which IMO isn't not necessarily a bad thing, but not exactly rigorous enough to give a "space shuttle" stamp.
I thought this was what you were supposed to do. Not that I do it every time, because I'm lazy, but Zed Shaw used to say every `if` conditional should have a corresponding `else` (which can also take the form of guard clauses). In Ruby, this is really easy to do because every method has to return something, so you build "returning nil or some String" as a concept into your program. There's a whole discussion around whether _that_ is a good idea, but I digress...going through the motions of enumerating every possible condition for a given piece of logic is not a bad exercise, and can result in very robust code at the expense of the code looking a bit more confusing than necessary.
We have a very similarly-written piece of code to this in our eCommerce platform. This code, written in Ruby, is used to calculate prices of discounts with regards to discount compatibility. There's a big warning atop the method stating something like "Please don't decorate/override this unless you ABSOLUTELY need to, the consequences could be very difficult to debug!". Not the easiest piece of code to look at, but it gets the job done in an efficient way without having to rely on C extensions for performance. We were also focused on correctness here, because calculating the wrong discount group could result in zero or even negative order totals, which our clients would _not_ be happy about. So the code is very verbosely written, isn't optimized for legibility, and strictly specifies both if and else sides of each conditional.
I personally dislike that style since that level of detail seems extraneous for normal tasks, but when you need to write code with absolute assurances like this then preventing the stack from being unwound without an explicit return (or panic, because some stuff is just terrible) is quite helpful to inspect and insure you have full coverage.
As an example of what I mean, look at some places where the if chunk is as big as the else chunk. By the time you are at the else, you might have to scroll back a ton to get to what it is you are in the else of.
Now, yes, to an extent, you could split this between functions. Such that you could have:
if foo {
doSomething
} else {
doSomethingElse
}
Literate style, though, would be more akin to if foo {
<<since we have a foo, we can ...>>
} else {
<<Without a foo, we have to ...>>
}
This is somewhat subtle, but not having to define everything in terms of functions with inputs and single outputs can free certain parts of your code to just dealing with the core logic that you care to explain.Not a panacea, but can be quite powerful.
The only unusual things I see are a) 8-space indentation, b) lots of “if err { return nil }”, and c) lots of comments.
Aren’t a) and b) standard for Go? How else is it possible to write Go code?
With less verbose error handling and 2- or 4- space indentation it wouldn’t look especially “branchy”.
It seems that the "do not attempt to simplify this code" is necessary rather because the cost of having so many people relearn what they already know is too large, and not because the code itself cannot be written in a simplified manner.
It's a beautiful example of the difference between real-life constraints of a codebase developed by multiple collaborators, and the elegance that we strive for when coding alone.
There is no "one size fits all" coding style in software engineering.
"Programs must be written for people to read, and only incidentally for machines to execute."
If the code is complicated, sometimes it's better to make it extra explicit than just relying on the reader's comprehension (including empty else blocks, for example)
Because in a nested A/B/C condition it is very easy to not notice what happens in the case where A v !B v C and/or why is it different from A v B v !C especially when some of those can't happen together (but then the code does "something" and "can't happen together" means "it will happen in that very weird case")
Maybe there are too many comments, but for code that is critical and might be harder to understand at a glance that is better.
As it is it’s obvious that many different hands were in it (and if you look at git blame, one of them doesn’t know how to revert changes properly and needs to stay after school for extra lessons. If this file is so important it shouldn’t have the most critical lines with a “revert changes” comment as the commit message. You done goofed, son.)
Calling this "Space Shuttle Style" is an appropriation of credibility and an insult to the engineers of STS.
1) One would have to make sure all the branches are accounted for during unit/integration tests.
2) The problem I am struggling with is the interviewers. So much of what I code I keep thinking about interviewers questioning it. "Why so many branches? Couldn't you have done this is in a more DRY manner? Couldn't you have done a better job at naming functions and variables so that so much commenting is not needed?".
This thing we do is truly an art form that can more easily be picked apart than properly understood. Ultimately I think we need to be like MMA fighters... able to properly pick the correct response for the situation with no ideological preference... only results matter.
The complexity has to be there, somehow. It's also highly subjective how one sees and grasps complexity.
Breaking code up for the purpose of reducing complexity is more often a symptom of clearing your mental space.
But IMO most often, complex code is much more well off being put in the same place, and with extensive comments too.
I also believe it's a true flaw to believe that a lot of comments are unnecessary when writing "important" code.
Comments are written in a human language, we interpret them as such. Computer language is different and it takes, more often than not, more mental power to understand and more important to change.
Some examples (none of which are perfect) would be COBOL, Ada and Eiffel. But they all have their own issues, and then of course there's a question of long term support and maintenance.
But when I do have to write complex parts of software that cannot be simplified, I will take a lot longer because I'll need to prove, first to myself, that it really cannot be simplified. Then I'll still add a big warning, either to others or to my future self (which is effectively a different person).
Likewise simplicity is not a valid requirement. Code coverage by tests, proofs and comments is.
A valid requirement could be considered complexity, number of names and entities and a lot of other linter constraints.
Heck, aggressive deduplication is not even always good.
Self similarity is not necessarily good either (the reuse of patterns) as you could have problems trying to spot the differences and special cases as they may no longer stand out.
A more compact style allows more code to be viewed at once making it easier to understand. Of course, taken to the other extreme, overly compact code becomes hard to read.
It's called, "the art of computer programming", because blindly applying programming principals rarely yields good results.
I've always considered this a best practice. There's rarely a good reason not to have a matching else, even if it's no more than a comment reading
// intentional NOP
or
logger.trace( "THING didn't happen, ignoring." );
Either one tells anybody reading the code that the else case was thought about and handled the way it was for a reason.
//checkVolumeSatisfyClaim checks if the volume requested by the claim satisfies the requirements of the claim
func checkVolumeSatisfyClaim(volume *v1.PersistentVolume, claim *v1.PersistentVolumeClaim) error {
// check if PV's DeletionTimeStamp is set, if so, return error.
if utilfeature.DefaultFeatureGate.Enabled(features.StorageObjectInUseProtection) {
if volume.ObjectMeta.DeletionTimestamp != nil {
return fmt.Errorf("the volume is marked for deletion")
}
}
volumeSize := volume.Spec.Capacity[v1.ResourceStorage].Value()
requestedSize := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)].Value()
if volumeSize < requestedSize {
return fmt.Errorf("requested PV is too small")
}elseif v1helper.GetPersistentVolumeClass(volume) != v1helper.GetPersistentVolumeClaimClass(claim) {
return fmt.Errorf("storageClassName does not match")
}
// function here, obviously
err = volumeAccessModeChecks(volume, claim)
return err
}
// Here's the function
func volumeAccessModeChecks(volume *v1.PersistentVolume, claim *v1.PersistentVolumeClaim) error {
// even the naming sucks. DISTINCT case error name
isMismatch, volumeModeErr := checkVolumeModeMismatches(&claim.Spec, &volume.Spec)
if err != nil {
return fmt.Errorf("error checking volumeMode: %v", volumeModeErr)
}elseif isMismatch {
return fmt.Errorf("incompatible volumeMode")
}elseif !checkAccessModes(claim, volume) {
return fmt.Errorf("incompatible accessMode")
}
return nil
}
This whole function is already a separated function from the middle of the file, no reason to stop there. No need for separate files when the function calls are going to be distinct and inline beneath their usage.But doesn't this style add a considerable cognitive load such that thinking several layers up becomes difficult? Perhaps there is simplified documentation of lower or mid level components so that thinking on the higher levels is not so difficult.
Also good: Line comments that explain cryptic details (eg, the semantics behind bit manipulations) or link to things like protocol definitions.
Bad: Line comments that simply repeat what the code does.
1. My comment assumes the code itself is well-written and clear. If it is, the plain English explanation will be superfluous, and you'll be able to understand the code just as fast. If it's not, then the comment was the wrong solution to a real problem. The correct solution is to rewrite the code.
2. What counts as "well-written code you can understand instantly" is of course experience-dependent. And not just "years of experience" dependent, but primarily "familiarity with the paradigm and style of coding experience" dependent.
Steve Yegge talks about over-commenting by junior developers here:
http://steve-yegge.blogspot.com/2008/02/portrait-of-n00b.htm...
I don't include that to be insulting, but because it tracks with my own experience as well.
Comprehending this code relies on reading a long series of verbosely referenced assumptions around an implicit state machine with numerous error conditions and edge cases. The author's argument is that extreme verbosity in control structures assists with ensuring no such conditions escape an explicit code block - ie. they admit that they are (ab)using code to deal with their personal challenge of achieving a rigorous and precise conception of the problem. I would posit that approach is the wrong tool for the job, and further that they have simply failed to clearly articulate the problem, and are writing illegible code and documentation as a result. A clear symptom of the resulting fragility is the title / opening plea to others not to change their carefully constructed house of cards. Fragility does not good code make.
If I were to rewrite this, the implicit state machine and its valid transitions would be documented first and foremost, eg. with a railroad diagram or ABNF.
The benefit of using [a formal specification language] is that it teaches you to think rigorously, to think precisely, and the important point is the precise thinking. So what you need to avoid at all costs is any language that's all syntax and no semantics. - Leslie Lamport ... via http://github.com/globalcitizen/taoup
A key part of the software lifecycle is being able to easily onboard newcomers to be productive, or coming back to a part of a code base months, or even years later.
//checkVolumeSatisfyClaim checks if the volume requested by the claim satisfies the requirements of the claim
Rust has a macro try!(x) that takes a Result<T, E> - a two-variant type, either a successful result of type T or an error of type E - and translates it to, if x is the first variant, evaluate the output of try! to the value of type T inside, otherwise return an error with the value of type E (on the assumption the calling function returns a Result<something, E>, too). So you can do things like `let file = try!(open(...));` and operate on the file. People liked it so much that a couple years back Rust added the question-mark operator, so you can just do `let file = open(...)?`, and chain it to `let data = open(...)?.read(...)?`. It's still visible that this is how you're handling errors, and you can always leave off the question mark and write out `match open(...) { Ok(file) => ..., Err(error) => ... }` instead, if you'd like.
Haskell has its monads, which for the present purpose can be interpreted as just a wrapper type. Given a wrapped object of type T, you can give it a function that takes a T and returns a similarly-wrapped object of type U, and have it apply the function to the data. If the wrapped object is in a failure state, it can choose to "apply" the function by just not calling it at all and instead returning the same failure state. Haskell has special notation with the "do" keyword for calling several monadic functions repeatedly, where it looks like you're writing regular, imperative code and assuming errors don't happen. But again it's obvious when you're using it and you can always handle the exceptional case specially as soon as you want.
Or pretend I meant io::Result :)
I'm sure he can have opinions - he is very experienced at having convincing-sounding opinions on software development. But where is the test of whether he's right?
> Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.
Also, just use a code formatter that rejects any incorrectly styled code.
I'm genuinely curious to understand the reason for your comment.
Note that while this isn't good practice I still would adopt Kubernetes. I was exaggerating for effect.
See rusts, match operator. The operator won't even allow your code to compile if all branches are not accounted for.
MyBlog: The Pure Function Pipeline Data Flow https://github.com/linpengcheng/PurefunctionPipelineDataflow
edit: After I posted this I saw the title was lowercased and dumbed down. Unfortunate that the term 'guideline' has been misapplied again, making things worse, but at least there's some irony to get a chuckle at given the article's subject matter.
I'd rather write my code like its written in the Linux kernel. There is no evidence that that style is better either, but at least Linus has provided loads and loads of arguments in favor of it on a mailing list.
As you said, nothing having to do with the control system.
That might be true if you live in a cave but when you are working on a team of 6-8 people with a team lead then basically none of that is true and you are forced to follow others' decision and everything is decided for you (instead of you to make decisions) no matter if you like it or not.
Of course some might say but then you have to make compelling arguments to raise your concerns and being heard but that's beyond the point and that's politics that most people (including me) despise. As a software engineer your only choice to make those decisions you mentioned is that you are a manager of some sort or you work completely alone.
And for some reason most developers accept this hierarchical relationship in their daily professional life but still pretend that they are allowed to make decisions by themselves instead of pointing out the implicit forces which rule their daily lives.
And I like to call out this line of thinking because it is pushing this liberal agenda that you are the sole reason for your luck or failure which is not true at all. Not even close to reality.
Actually many thought leaders in our industry for example Uncle Bob anticipate that our industry MUST and/or WILL be regulated (if by self-imposed regulation or external one is again beyond the point) otherwise the whole industry would lose credit. And I fully agree with that.
Not only because of credibility reasons but also because it makes the implicit subordinate relationship that most developers are blissfully unaware of, more explicit.
Yes, I agree if you have an established team and there is a strict hierarchical development cycle/roles then rules will be enforced. But how about other team dynamics or lone developers that push their code into the world. How many js libraries import explicitly or implicitly an addition library (too many to count on top of my head but not enough to bother writing a script for you to show). There is a lot of bad code out there that were created by bad untrained tech managers and lone coding wolves because some standards have not yet been refined yet or agreed upon(standards like ssl, aes encryption).
> liberal agenda?
You lost me there? And maybe I lost myself as well
> Uncle Bob When I said Bryan, i misspoke and was refering to Uncle Bob. But do you agree with self-imposed or external? They lead down very different paths