Whenever I read articles like this and the ensuing discussion that follows, I'm reminded of this quote from Dijkstra:
Don't blame me for the fact that competent programming, as I view it as an intellectual possibility, will be too difficult for "the average programmer" — you must not fall into the trap of rejecting a surgical technique because it is beyond the capabilities of the barber in his shop around the corner.
Dijkstra (1975) Comments at a Symposium
Whenever I read code written in the "so boring it cannot fail" camp I get exhausted. This code is inherently procedural and rolls itself into a giant ball of mud -- composition is difficult to achieve so as requirements change the code accrues more loops and conditionals until it is nearly incomprehensible.
It might start out neat and clean but rarely will it stay that way.
Good, non-leaky abstractions are key. This can even be achieved with procedural code but I think functional programming techniques like pure functions, immutable values, and a sound type system help a great deal... even at the expense of the initial "cognitive load," it takes to learn how to employ these tools.
We've been refactoring and decoupling code for decades in imperative coding styles. I think one of the problems we have as a community of programmers is the concept that it must end up in a ball of mud. Almost all of our extant systems are developed in imperative style. The system I work on today is many orders of magnitude larger than the systems I first learned on and yet it is dramatically easier to write useful code. That's because over time, we have extracted and polished functionality into useful and unambiguous pieces.
Back when I was a young programmer it was often said, "If you don't have time to get it right the first time, how on earth will you have time to do it again?". To me, this concept is why we get into the ball of mud. It will happen to you no matter what style of programming you choose, because you can't get it "right" the first time. You don't know what "right" is. Because we resist the rework/refactoring, we build balls of mud. We even blame it on the people before us "who got it wrong" (which is easy to do because average attrition is about 2 years and they've likely left the company).
I like functional style as much as the next programmer (well, probably more than most in fact), but FP isn't going to save you in this instance. The only way to maintain high levels of productivity in projects is ruthless rework in the face of changing requirements.
"If you don't have time to get it right the first time, how on earth will you have time to do it again?"
I like that quote, but to be honeste. Some times you have to get it out of the door, and time is of the essence. And you can go back and fix it. That is okay, as long as you understand that shortcutting now, will cost time tomorrow (i.e. technical debt).
Yeah, the problem appears if your superiors don't understand that. It'll be a "quick fix", when you want to clean it up after the crunch the comments will be "nah, we don't have time for that right now" (non-coders never seem to understand that messy code is a liability - thus cleaning up code seems like a waste of time).
And a couple of months later, it comes back biting you in the behinds. At which point, someone will tell you or some other unfortunate colleague to "just fix it quickly".
In such an environment "quick and dirty" only works if it is kept locally, i.e. the author is the only one that uses the code. Hence Uncle Bob's insistence that a good developer must say "NO!" from time to time[1].
> non-coders never seem to understand that messy code is a liability
If J. Random Folk were to go to the nearby mechanic to try and fix his car, he'd be very upset at him for stitching things with duct tape and calling it a day. Yet this is mostly the default expectation from "non-coders", who are all up in arms because you're being a "perfectionist" when you're just being a responsible professional. Sad.
Big difference between intentional and unintentional technical debt. If you as the programmer are taking on intentional technical debt but the people making resource decisions don't know about it, it is in fact unintentional technical debt.
Meh. I think you're overshooting the runway a bit here.
I'm a functional coder and I like the idea of reducing cognitive load. The procedural guys use it in a different fashion but if you're writing code you can't understand after walking away for a few months and coming back? You're doing something wrong.
I don't think that relates to the abstraction or composability of your solution style. I find good naming, decomposition, and re-composition allows me to take things that don't matter and put them in a utility library somewhere. Then I'm left with a small number of new symbols and configurations that's easy enough to grok coming in cold. There are plenty of FP guys that don't do this. Hell if I'd want to maintain their code.
yeah, I'm always amazed at the people who claim they looked at code they wrote 6 months ago and think it's horrible.
I've looked at code I wrote 2-3 years ago, and upon examination thought to myself "that's fairly reasonable code, I got most of it right".
I see people claim that means you're not growing as a developer, but my growth is about being able to build larger and more complex systems rather than perfecting small snippets of code. In other words, I've stopped caring about the specifics of code (within reason), and I get better at building systems.
Functional programming is inscrutable by most. Berkeley uses LISP as a flunk out class to weed out freshman.
I find it odd that given in the physical world people require different shoe sizes for different feet that in the intellectual world people believe one size fits all, or that there is ultimately a single style of code that is comprehensible. Do you really believe our brains are all the same?
The functional programming crowd will always play a minor roll in human programming and not because as you suggest, you are all superior.
But rather it has to do with how brains differ. In fact I think the very real, emotional, visceral reaction people have to functional programming is key. People who love it, love it. Everyone else is completely demotivated by it. Lost in sets of parenthesis as the say. There is little middle ground. The very emotional reaction people have speaks volumes about how the brain views programming.
Spoken language changes every day because people use words differently due to brains being different. This vexes those who are compulsive about adhering to fixed definitions and grammar rules because of some imagined ideal. The same is playing out in software. We will always be creating new languages and tweaking them and there are there will be those who believe in an imaginary ideal of what readable code is.
Ultimately I think software writing needs to mature to where written language has matured: editors. Code review has some semblance as does paired programming. But editing is far more involved. Editors can reject entire writings.
So why do we need editors in the first place? Because in order to have happy consistency a set of rules is not enough. I comes down to style: a word here, a name change there. It may not seem like much but in fact it is.
Given a language is Turing complete, meaning it can exercise the full capability of the CPU and computer, then the choice of language and style is about the human condition, a condition which is as varied as the people in it.
If one cannot write code that ones finds usable then one needs to keep searching in earnest for a better style. However, once you have a style you can appreciate your own code from years ago then it is a matter of arbitrary standards when interacting with others. You have to draw the line somewhere but lets not pretend for a second that a single writing style is ideal for everyone, or even most. Its arbitrary because without any standards you have an unmanageable chaos.
>Do you really believe our brains are all the same?
One thing that all brains have in common is that they can learn. One thing that all feet have in common is that they can not learn. That's why your analogy doesn't work here.
>Functional programming is inscrutable by most.
As is musical notation, maths, foreign languages, CAD drawings and everything else that has to be learned before use.
I am not a huge proponent of FP as I'm not entirely convinced that the benefits of immutability always justify the restrictions imposed on algorithm and data structure design, both in terms of simplicity and performance.
But one thing I am convinced of is that whatever newbies may find inscrutable is entirely irrelevant unless you plan to cut costs by having interns write all your code for free before replacing them.
> But one thing I am convinced of is that whatever newbies may find inscrutable is entirely irrelevant unless you plan to cut costs by having interns write all your code for free before replacing them.
I completely agree. My standard in choosing a design or syntax is whether a reasonably competent (but not brilliant) programmer will understand this well if they are already generally familiar with the codebase (but unfamiliar with this particular module) and are in a particularly big hurry.
As a specific example, I was horrified the first time I saw Java's convention for setting fields in a constructor:
public class SomeClass {
private final int shoeSize;
private final String favoriteColor;
public SomeClass(int shoeSize, String favoriteColor) {
this.shoeSize = shoeSize;
this.favoriteColor = favoriteColor;
}
}
The constructor has a local variable with the same name as the instance variable. The syntax works because local variables shadow instance variables but not when explicitly referenced via "this." -- a moderately obscure part of Java syntax.
Normally, I would object if a developer created a variable name that shadowed another and expected the reader to keep it straight and not get confused. But when you do this REGULARLY, it becomes just another standard idiom. Most readers today (now that the practice has been standard for over a decade) wouldn't even blink. A complete novice might be confused, but the complete novice isn't my target audience.
Functional programming has made huge inroads into mainstream languages over the last 20 years, so I suppose this is because people's brains have suddenly changed?
I always thought different paradigms suited different problem domains rather than different people.
I'm not a software engineer by trade, but just for example, I've recently been mucking around making my own videogames and the object-oriented paradigm using the components pattern seems like a very natural way to conceptualise a videogame. E.g. Objects in the game (NPCs, environment) start as a basic object and are given specific behaviours by adding components.
Alternatively, I sometimes have to write R code for data analysis and in that case the style of using pipe operators to chain lots of functions together is perfect for readability and for reasoning about the code.
> I always thought different paradigms suited different problem domains rather than different people.
This. Any imperative program can be formally encoded as an immutable functional program, and vice-versa.
Therefore, which paradigm to choose is a matter of what style fits the problem at hand better, and how comfortable you are with the paradigm. But this is true of any coding convention.
I really don't think this is the case. FP is difficult for most developers who have experience with the OO/imperative paradigm - i.e., "most of us" - at first. It only takes about a week before the benefits become apparent.
Don't get me wrong, I don't think it's the solution to every problem, but spending a couple of months writing Clojure was a major positive experience for me, and it's significantly improved my day-to-day code in Python, Ruby, and Javascript.
While I know Dijkstra's name only from a "historical perspective" (the algorithm, the goto-letter and some of his reasonings), I somehow grew very fond of a motivational-style picture of him almost frowning[1] with the title "quick and dirty".
I still would like to see a version that is of a high enough resolution so one could properly print, frame and hang it on an office wall. I like the idea that - when the next unavoidable proposal of "can't we just [...]" or "all we need is a prototype"[2] comes around - I'll be able to point to it and say something along the lines of "convince EWD first!".
This is a rather idealistic approach to something which has real-world constraints: Despite our misinformed bathtub-curve view of the abilities of the software development community, the world is absolutely teeming with "average programmers", not superstars. It is in your best interests not to shut these folks out of your project if you want to guarantee that the people you hire are able to hit the ground running and not stop by your office every 10 minutes when they get stumped by some quirk in the codebase.
What you describe is the "Knotted Shoelace" antithesis to the ball of mud. Functional? Yes. Strong? Sure...but to re-lace your shoe you have to painstakingly pick it apart.
Imperative code can be clean and stay clean too, it just requires more effort. Remember that functional programs are easier to understand only because they are less expressive - they can't express concurrency and nondeterminism.
That being said, the principle of least power applies in full force, so if (a part of) your program can be written purely functionally, it should probably be written that way.
I always like comparing coding with writing. Depending on your proficiency, you might want to write a pulp fiction, a news article, a PhD thesis, or Shakespeare. You might write in English, American English, Chinese, classical Chinese, Japanese, or Farsi.
Maintainability or the cognitive load of the reader or writer completely depends on the proficiency of the reader or writer.
Sometimes I find myself writing in reviews for less experienced developers the comment: this is clever but not clear.
I think as developers we get too enthralled in the problem solving and forget that in the long run we are more like journalists noting business rules at a snap-shot in time, which a future maintainer of our software must act as historian/archaeologist in order to understand.
What's funny is that often our future selves is the maintainer of our software. However, as we lament choices in the past, we continue to write intricate code in the name of elegance/conciseness.
These days I'm pretty pleased when I can say a piece of code utilises only syntax and statements taught in an introductory programming course.
> These days I'm pretty pleased when I can say a piece of code utilises only syntax and statements taught in an introductory programming course.
This sounds like optimizing for read-time. This is subtly wrong, IMO. You should be optimizing for comprehension-time, i.e. how much time it takes for the next person to wrap their head around the (piece of) codebase. Often, you can have significant gains in code comprehensibility if you raise the minimum level of competence for the next person.
Or in other words: programming is a profession. You're not supposed to stay at the level of introductory programming course. You're supposed to be continuously learning and getting better. That applies to the next person too, so if your "clever" but clean abstraction is too hard for them, they're supposed to suck it up and open a book. They can afford the book, it's not like programmers are underpaid.
We like to think that, but it's not really true. Professions tend to include some kind of guild, union, or association to represent the interests of practitioners and require/evaluate formal ongoing education (reading blogs doesn't count). There are typically barriers to entry. There's usually a licensing process as well as a disciplinary process that may revoke one's right to practice.
Offtopic, but whenever I read someone referring to a programmer having to be an archeologist, I can't help but think about Vernor Vinge's _A Deepness In The Sky_[1], where there was so much legacy code that there was a need for "programmer archaeologists" to dig through it all looking for something suitable for whatever current problem needed to be solved.
Everything is unclear until you become used to it, though. I mean, used to it, as in, you can read it without stepping yourself through the steps manually. And to a novice programmer, pretty much everything they come up against represents this.
Turning five lines into one line with a reduce function sounds like a normal thing to do for experienced programmers, but to a beginner, they'll think you're a genius for pointing it out to them. So it's not surprising when they try to apply their genius and come up with something clever, too.
You'll also find that some experienced programmers specifically turn 1 line into 5. It's the beginner that tries to create the 1 liner because they think it's genius.
One-liners are bad if they are obscure and experienced programmers know that. However I wouldn't go as far as say that experienced programmers don't use one-liners at all. As usual in programming, it's all about balancing clarity/conciseness.
For example, I find this much clearer as a one-liner (Python):
validated_items = filter(is_validated, items)
rather than
validated_items = []
for item in items:
if is_validated(item):
validated_items.append(item)
"filter" has to be the worst library name. I have no clue what it's doing unless I read the documentation. On the other hand "append" feels like a much more conventional and comprehensible name.
I agree with you that `filter` is less explicit than something like `copy_if` (C++).
Does it return elements matching the predicate or not matching it? I struggled with this for a while in CS.
After learning different programming languages and getting some field experience I noticed that the `filter` pattern is pretty common in the programming world. It's one of those pervasive functional patterns that are so practical that they were included at some point in imperative languages.
>"filter" has to be the worst library name. I have no clue what it's doing unless I read the documentation
It, I dunno, FILTERS a collection keeping only certain elements (those that match the filter)? What else would it possibly do? Besides a standard CS term, it's also a concept from real life...
A filter creates two sets, one with the thing, and the other without. When you filter something, you might be interested in what is filtered in, or what is filtered out.
Ponder for a second the phrases, "filtered water", "coffee filter", "camera filter", etc. In all of those cases, you are interested in the set without the filtered thing, not with.
Compare this to "select" which is a much clearer name.
>Ponder for a second the phrases, "filtered water", "coffee filter", "camera filter", etc. In all of those cases, you are interested in the set without the filtered thing, not with.
Filtered is akin to "cleansed" (it refers to the "main body") not akin to "picked out" (which would have applied to the undesirable elements.
It's actually the water (or the light, in the case of camera filter) that's filtered (hence the name "filtered water" and not "filtered dust and detritus").
The other stuff is not what's being filtered -- just what's "filtered out".
Experienced programmers will strive to write the code in a way that best communicates its intent and scope of effect. Sometimes that involves turning 5 lines into 1, sometimes the other way around.
Honestly, when I see people complaining that the code is "too clever", my default reaction is: programming is a profession, you're supposed to learn new stuff and get better, not complain that something is beyond what they taught you in Programming 101.
One of the most valuable things I got from university was my professor's saying, "When somebody tells you your code is clever or interesting, that's an insult."
This, I have very rapidly found myself writing code which only very occasionally uses something fancy and otherwise generally opt for the most plain, boring and straightforward code. Internally, I describe the code as idiot proof.
Putting it more kindly, my litmus test for my own code is "will a 5 year old understand this?". In the many instances where I have since returned to my code to maintain it, I am often grateful for every less minute I spend reunderstanding all my own code.
To use your analogy of engineers being like authors, as a teenager, I would often find every excuse to use some exciting sentence structure or long word - doing so made me feel authoritative and clever. But once reading more, you find that some of the most powerful, and clever, writing is concise and plain.
It's worth looking at all the other pages on his site both before and after reading that article. Do you think he would've be able to accomplish all that if he "utilises only syntax and statements taught in an introductory programming course"?
What's funny is that often our future selves is the maintainer of our software.
IMHO if you find it difficult to understand code you wrote years ago, you have not actually improved. In fact, if this was any other skill (natural language, maths, etc.), the inability to do what you used to be able to, would be considered none other than a regression.
> These days I'm pretty pleased when I can say a piece of code utilises only syntax and statements taught in an introductory programming course.
Why?
Shouldn't you be building up from that towards the problem you are actually trying to solve? Is it reasonable to expect someone to understand every single bit of code in a large project without reading documentation of the components below it?
I appreciate that you are one that can recognize the beauty of originalism. There is something to be said for first principles, and something that we can all learn by doing what we can to abide by the idea that every piece of knowledge that is taught is done so not only by design but with our best interests at heart.
Needless to say, I want to share a little piece of code I wrote that uses first principles in terms of data structures, and is perhaps my magnum opus. It's not sophisticated, but it does work, and it works because I refined it again, and again, and again. And I think that is the gist of this article. Refinement reduces cognitive load more than any other technique (in my perhaps not so humble opinion..haha).
If you'd like to look at my code, please go to github.com and search for justSomeGuyWhoLearnedToCode. The Trust repository has the code that I wrote as a trust fund for some very good friends of mine, but as I cannot seem to get in touch with them, I am sharing it with the world.
It requires some compiling, but man when that thing cooks, it cooks with gas, mi amicis.
I'm working on a sequel using NLP that I am going to add to the repo on Monday, so keep your eyes and ears open for that too.
And if you fret about how well you write code, just remember, the mere fact that you are writing code speaks to your tenacity as a problems solver and your participation in the process is well being commendable if and when you step back from your "self" and look at all of the progress you have made. Don't write functional code, or perfectly simplisitic code, or code that sparkles when you're done, instead write code that reads like poetry. Write your life into it. Write your heart into it. Write your soul into that logic, because that code is forever. It is your statement to the world that you are an artist, and your art is the marriage of logic and beauty, of solutions to probelsm so large we harness the power that binds the atoms together, and have machines building machines building machines to lithograph the transistors that can fit (like so many angels) a thousand on the head a pin.
Bless serendipity, and bless the dudes at Bell labs, Alan Turing, Charles Babbage, Rosy the fucking Riveter, and everyone else who made the computer possible. We are on the cusp of a new age, and quantum computers will revolutionize (a word I almost never use) the way we make medicine, and the way to outer space then become much, much easier and the world that we live in will become much happier as we share our progress with the less fortunate. Get ready, folks, because the whole shmear is waiting just around the next bend.
And for you "older" fogies, check out the Win 10 default wallpaper, and think of the line: "You are in a room, and you see a window".
I've noticed recently that especially in online discussions, the term "cognitive load" is used as a catch-all excuse to rag on code that someone doesn't like. It appears to be a thought-terminating cliché.
There's definitely room to talk about objective metrics for code simplicity, which are ultimately what many of these "cognitive load" arguments are about. But cognitive load seems to misrepresent the problem; I think it's hard to prove/justify/qualify without some scientific evidence over a large population sample.
With that said, the article presented fine tips, but they seem to be stock software engineering tips for readable code.
I think cognitive load is a perfectly acceptable term here. Taken within the context of Cognitive Load Theory, we assume that any individual can only maintain a few items into their working memory. These items are portions of the code that you need to think about at once in order to achieve a task, and good code partitions off the logic so that in order to understand individual components you only need to reserve a few slots of your working memory.
Of course this is a bit contrived, but I would argue that pretty much everything we've come to understand as "easy to read code" all reduces down to how effectively it organizes itself given the limitations of our working memory. And in that case, it's one of the first things you should be sure to understand on your path to becoming a better programmer.
Though your points seem (technically) on-point, you might be missing the one facet of the "cognitive load" concept that I think is the defining one:
as time marches on, entropy increases and in tech that means, cognitive load does. Every day there are new command-line tools and arguments and combinations and compositions, every week new languages, every quarter new syntax sugars in minor releases of major languages, every other year new major framework versions each with twice as many new libs/APIs as the previous release, etc etc .. even with Google and StackOverflow integrated into your hypercontextual IntelliSense etc IDE, it Just. Friggin. Grows. Out. Of. All. Control at least easily perceived so. Nevermind the constant stream of new NPM packages or fresh Haskeller-PhD papers. (And everything constantly sounds game-changing-as-heck too, funnily enough. Guess that happens when we all grow up around advertising ;)
Dead-simple "ELI5" code alleviates many headaches here, simply by not piling even more layers on top of all those we can't afford to ditch in the real world, much as we'd love to.
"Simple code" to "reduce cognitive load" was even an early helpful lesson for the id guys as shown a few days/weeks back: http://blog.felipe.rs/2017/02/25/id-software-programming-pri... --- and they had to deal with way fewer foreign/3rd-party baggage, writing Asm/C for DOS games that run just a tiny level above lowest.
At some point we'll all switch to Brainfuck: at least, there's only 8 primitives to keep in your mind at all times ;) seriously Assembly language is becoming ever more appealing. With fewer abstractions, you get more LoC but at least you grasp exactly what's meant to happen. Higher-level intent not so much, regrettably, unless commented of course.
> I think it's hard to prove/justify/qualify without some scientific evidence over a large population sample.
I agree with you, which is why I decided to gather some evidence about stuff like this. Here is a start - I have posted links to it before. Grab the preprint it will soon be ieee published:
Cognitive load in general is vague and hard to measure and thus difficult to find evidence for. However the effect of memory during program comprehension can be measured, as memory is well understood (tons of psych studies).
While memory surely plays a role to explain differences in program comprehension, it isn't enough to explain all differences. Experts in many experiments are often not impacted by bad code as much as novices, so experience also appears to play a role.
Memory is often said to be limited by its capacity, but most people ignore that the classic memory model explains linguistic encoding (e.g. there is not only long and short-term memory, but also a phonetic loop, an episodic buffer and a "visual/spacial clipboard"). This it is likely that code is influenced by language processing (thus, as my studie argues, one should use natural language words as identifier names), influenced by expectations (for example layout, style), etc.
But still that's not all. Some problems are difficult to decide. Imagine a short recursive algorithm for some problem, used appropriately. It might be an elegant solution, and could maybe replaced with a loop.
Does the fact that the solution is recursive, and thus builds a virtual tree, and maybe has a non-linear behavioral order, reduce cognitive load, or increase it? Should it be replaced with an equivalent loop?
What if we found out that the curly braces cause more cognitive load, when placed at the end of a function's signature, rather than on the next line?
Lines like "Don’t use tools that are still too hard to get a grip on" are code for 'your team will get discouraged if they have to do any homework at all to understand your project'. If that's true, how do you expect them to understand the business requirements?
Every large project has embedded tools and legacy tricks so the author is implicitly saying 'don't let projects scale'.
Simplicity is hard to achieve, and people who can't understand complex code can't write simple code.
If a book hits you on the head and makes a hollow sound, it may not be the fault of the book.
This is my thought. I LIKE to read clever code. Most often than not it teaches me something. Some neat way to do x. It teaches how to write terse code and I'm probably going to learn how to apply that to other areas of my code.
I would be worried if my code is being reviewed by somebody who doesn't appreciate clever code.
Now, clever is different than complex, or confusing code. Clever code is a neat way to do something. Complex and confusing code can be spotted immediately because it does one or many of the following things:
- Functions get too nested
- Tries to do too much
- Function is too long
- Function is not broken into logical parts
- Confusing parts don't have their own function
- Long conditionals
- Non descriptive variable names
- Modifies state all over the place
And I could go on. Those are the things I would watch out for and that really take a cognitive load on me.
Also, clever code is different than tricky code. To try to use a programming language quirk is crazy. You're asking for your code to be hard to read. Using a little known useful feature is good way to extend your team knowledge of a PL.
It starts with picking the right language. Some languages help you to clarify your thoughts and some seem to do their best to obscure whatever meaning there was.
Then, after you've picked your language it's up to you, the programmer. And 'good code' to me translates into 'whatever is on the screen is enough to understand the code'.
If you have to page back-and-forth all the time between different parts of a function or between different functions or even different files then your code will be hard to maintain, hard to read and probably buggy.
So work hard on reducing scope as much as you can.
That's because no one even knows how to start doing "science" in this direction.
When you read the code there are so many different things influencing your understanding that it's hard to impossible to even list them all. And if you take a look at some of the things that might influence your understanding you'll notice that most of them are very hard to impossible to measure.
From the top of my head, things which may have an impact:
* your level of skill in a given language
* your level of familiarity with the style of a particular programmer who wrote the code
* the tools you have at your disposal (go to definition, see docs functions of IDEs)
* your familiarity with a particular framework used
* your preference and expectations regarding the identifiers
* your knowledge of what the system as a whole (or its part) is supposed to do
* your familiarity with the project structure
And so on, and that's even before we start talking about concrete examples of readable code and trying to get some metrics on it!
Writing code is no more susceptible to scientific analysis than writing prose when it comes to other people reading the code (and not machines executing it). To write good code you need to first assume something about your readers (their level of skill, prior experiences, etc.) and then optimize the form of the code so that it doesn't confuse them (too short) or bore them (too long).
Seriously, writing prose and code (the latter only if meant for human consumption) is very similar: you need structure, things following one another, sentences of appropriate length and "density" and so on in both kinds of writing. Programmers could learn a lot from writers, but they most often refuse to do so. Literate Programming should be the default by now, yet is still used very rarely...
These are just the ones from the top of my head that I can google quickly, if you want I would be happy to share my zotero database or a large bibtex file.
> When you read the code there are so many different things influencing your understanding that it's hard to impossible to even list them all.
You are completely right: Psychological research on programming shows that it is a very complex cognitive task, best done by experts, and poorly understood.
You describe knowledge and experience, and they in fact matter a lot. The above studies, for example, show (often just as a sideeffect), that experts are impacted less severely by badly written code (however that was operationalized).
> Literate Programming should be the default by now
There's nothing to be sorry about, actually, I'm really happy to learn that such research is being done! I tried searching for similar studies quite a few years back and came back empty-handed, so I assumed it's either not done at all or is very niche.
As you seem to be knowledgeable about the field, how relevant/applicable you think the studies you linked to are in the general case? In the study about identifier length, for example, seems to be very specific and I'm not convinced at all the results would be the same in a different language, with different people and even with slightly different identifiers (abbreviating start to str vs. beginning to beg, for example).
EDIT: another thought on the study: does it control for presence or absence of widely known conventions? For example in Haskell, OCaml and others it's customary to write `x :: xs` - would writing `element :: list` instead improve the time needed to comprehend the code? On the other hand, in Smalltalk, you frequently write `add: aNumber to: aList` - the identifiers are longer, but they provide additional (type) information which is otherwise not present. So how long the identifiers need to be may depend heavily on the language (the study used C# I think), is it accounted for in the paper?
Still, all the papers you mentioned look interesting and I will read them once I have some time. Thanks for posting! :)
> another thought on the study: does it control for presence or absence of widely known conventions?
I am very happy to encounter other critical thinkers - your question is a really good one :) You are right, the study is not capable of explaining this effect (that is, how commonplace / conventional some abbreviations are), but it was considered in the design. I am sure that this plays an effect but I wouldn't dare to give a definitive answer based on the data from my study.
For example, config or cfg are arguably so common that there they don't hurt comprehension. Similar for single letter variables. Point.x and point.y are easily identifyable as coordinates. Or the variable name i in a for loop may not be problematic, as it becomes almost meta-syntactic (much like foo and bar). However, i,j,k,l index names may really hurt comprehension, when you have a complicated looping strucutre with many lines in between, as they are likely to strain your working memory. As for the point.x example: I would explain this as a priming effect. The name of x is fine, because point already preactivates the right direction. X in isolation might be worse, and if you encounter new MessageBrokerInstance().X() you might as well read your code in base64... Thus, based on my experiment, I can talk about variables in isolation, but usually, code is mixed and here, other effects might be relevant.
In the longer versions of my experiment, I considered the effect of common abbreviations as well. Psychology lists several word frequency effects. Common words can be immediately accessed *(from the so called mental lexicon, a mind-dictionary if you will), but uncommon words have to be synthesized on the fly though their phonetics (see, for example the dual route cascade model, coltheart 2001, http://www.cogsci.mq.edu.au/~ssaunder/files/DRC-PsychReview2...). Thus, high-frequency words (=often occuring, common words or strings) are quickly read and their meaning is understood, whereas uncommon words or strings do not have a representation in the mental lexicon and you have to synthesize their meaning first, thus slowing down comprehension.
My argument is simple: It is always possible to understand code, no matter how mangeled or obfuscated it is (after all, reverse engineers are doing amazingly hard work). The question is how easyly the code can be comprehended. Abbreviations that are common to some (e.g. experts), may not be common to others (e.g. novices in their first job). Of course, the newbies will get there eventually, but abbreviations have a higher learning curve, thus new people will be unproductive for a longer time.
Think about yourself, you surely know this effect:
1. Write code.
2. Problem solved
3. don't touch it for 4 months
4. Changes needed, need to fix bug, add feature
5. How does this work?
6. Wtf, what was I thinking?
For the sake of all newbies, your company, or even your own, I encourage the use of identifiers that can be read, because you can READ and know LANGUAGE, and not because of arbitrary conventions. There are many conventions (e.g. x:xs, for i=0;i<10;i++, point.x) that can surely be considered domain language and don't impede comprehension, but still might hinder comprehension for novices, or yourself in 4 months.
> how relevant/applicable you think the studies you linked to are in the general case?
This is really hard to say. Many processes take place when programming, and many programmers have theories about why it is hard and how to make it easier (as the entry article citing cognitive load, which is a good methaphor, imho). So far, I know of many such scientists who are trying to isolate the different effects. For example, I am focused on identifier names, as I find them to be impactful. Their meaning can't be analyzed automatically (even with sound nlp techniques which are relatively limited), and the programmer is totally free to name their variable names what ever the hell they want. I am sure that in comprehension of programs, identifier names play a big role, but when I encounter "clever code", with weird recursions, counterintuitive measures, or plain magic (https://en.wikipedia.org/wiki/Fast_inverse_square_root) the value of identifiers are limited, or, in other words, there are other things going on that impact my comprehension BESIDES identifiers. How they interact, I cannot say for sure, but if complex code has no clear identifiers, it becomes complicated.
I believe that each of the effects in isolation is relevant, but I am not sure which one is the most dominant, or, for that matter, whether there is ONE thing that will solve all problems.
"That's because no one even knows how to start doing "science" in this direction."
No true, there is an entire field of usability science. It is all still limited to experimental learnings as opposed to theoretical deterministic knowns.
Currently usability testing is only be applied to end users. There is no reason task analysis and the Jakob Neilson's 10 heuristic rules of basic usability cannot be applied to software itself. I apply the science of usability to written software.
Yes, but how many "usability studies" are done for writing software? I've seen onle a few and people write articles like this without any proofs daily.
Science is hard, especially if done properly. The least they could do is to spit teams in two groups, based on whether they use a technique or not (e.g. unit testing, using OOP) and see if it makes any difference. It's far from perfect, but better than nothing.
> That's because no one even knows how to start doing "science" in this direction.
I suspect the reason almost no one do SW science is that nobody really cares. Most people just repeat some dogmas they like, e.g. they say: "you broke liskov substitution principle, that's bad" without any proof.
I guess people just like flamewars (who doesn't :)
As my other comments outline, there is some science, although you are right in that empirical work on software engineering is a very small niche. Most people just report studies like:
"Hey, look what I cooked up in my basement using haskell".
The largest improvement you can make to reduce the cognitive load of your code is to move to a functional language with immutable data and optionally static typing. Empirical data about bug tendencies by language http://macbeth.cs.ucdavis.edu/lang_study.pdf is IMHO indirect evidence of cognitive load issues.
Well, there's somewhat of an argument to be made that immutable data structures are a bit less performant than mutable ones... but it still seems to be the way forward, especially when you take any sort of concurrency into account
The real distinction I am trying to make is that objects are a higher level, cleaner and more composable way of organising code - much more so than the "module and record" nonsense most FP people seem to push.
Writing good technical articles: if a part is probably the most important part, put it first (or nearly first) in the doc & don't waste the readers time congratulating them for spending 2 minutes reading.
A "maintenance programmer" is a programmer whose job is maintaining a system (bug fixes and adaptation to relatively minor environmental changes) that is viewed as complete and basically static, either without major new development expected or with major new development done by separate teams in as-needed projects. Often, maintenance programmers are employed by the organization owning the system, while new development is done by contracted teams.
It is a particularly common role in government and other enterprise shops, particularly those that still use a pre-Agile, civil-engineering-metaphor approach to software development.
As I understand the term it's someone who inherits the feature-complete codebase after the original author left it. The duty of such a programmer varies depending on the place and project, but it's generally someone who will be asked to fix the bugs and implement new features should the need arise.
There's nothing derogatory to the term, although it's good to have some sympathy for such people - enough not to write bad code when first coding the project in the first place.
This (arguably nice) post covers a small number of the points made in the book The Art of Readable Code: Simple and Practical Techniques for Writing Better Code[1]
I can recommend this to every programmer, even experienced ones, because even if they might know most of the things mentioned, it is presented in a very approachable, structured way and I think it always helpful, never boring and a diverting, easy read.
It also tackles these issues of "Gurus say" and "Everybody knows..." and tries hard to refrain from subjective matters, clearing up a few misunderstandings and old habits.
DRY isn't something you should blanket apply. There are cases where you do want to repeat yourself, e.g. to improve readability (avoiding metaprogramming, which is hard to reason about and will break your IDE) or because two things aren't actually semantically related (trying to force an abstraction means you need to undo it later anyways when the implementations diverge).
I like code that reads like a Dick & Jane book ("See Dick. See Jane. See Dick run. See Jane run.").
However, it appears to be trendy to write insanely difficult to read code. To use the analogy, Shakespearean code. Instead of one line of code doing one thing the developers will write a ton of functionality into one line of code by using fluent and method chaining. As someone reviewing the code I have to keep this mental stack of what the code is doing and it just becomes too much to process.
I can't up-vote this enough. As someone that started doing development in the late 80's, I find this style of coding to be infuriating. It completely destroys the ability to map lines of code directly to function calls without resorting to manual coding rules that require that each .function() be on a separate line, and makes debugging way harder than it needs to be. And, for what purpose ? To avoid a local, temporary variable ? Do these developers think that their code is faster this way, or...what ?
It's "write-only" code - good luck to the next guy that has to read it.
And, don't get me started on: if <Constant> = <Variable>... :-)
> And, don't get me started on: if <Constant> = <Variable>...
That seems to be quite the pet peeve with many and I quite like the style in certain scenarios: namely the common one where the constant is a simple literal and the variable portion is a longer (no, not excessively long, just.. longer ;) chain of things, a call within a call or some operator etc. It's as if to signal "there is some crunching/intricacy here but look, it's just to check against this simple value here right at the start". Kinda reassuring. Likewise find it quite readable for checking for magic strings / code-monikers (of course to be avoided but sometimes not when dealing with certain input formats etc) and especially when a couple of such in a row are tested..
I like your example a lot. It's easy to imagine a monstrosity of an equation stretching off to the right, but knowing the solution (or at least a solution) can constrain the knee-jerk reaction of "oh, that looks complicated"
When learning C in college, I was specifically taught to use CONTSTANT == variable. Accidentally omitting a '=' is common and putting the constant on the left side ensured the compiler would catch the mistake.
I would argue that by using CONSTANT == variable, you are reducing the cognitive load of the compiler, but increasing the cognitive load of the human reading the code (who has to mentally flip the expression around). I think the original article intended to say the same thing.
I don't get it. A test for (in)equality is commutative unless you're inducing side effects (i++) - and then side effects are going to be the big "cognitive load", regardless of which side of the comparison they occur. Am I dyslexic because (null != foo) is exactly as easy for me to read as (foo != null)?
And there it is - thank you. I knew there had to be a reason, but couldn't quite figure out why and the example in the article (null != variable) was different enough to confuse the situation.
I haven't written anything in C in quite a while, and completely forgot about inline assignments. Which brings me to another pet peeve of mine, inline assignments... ;-)
To get you started, I'd like to compare my problems regarding `if <Constant> = <Variable>` with yours. This assumes you aren't the kind of lunatic to actually assign a constant inside of an if's condition.
1. I can't articulate exactly why, but that should totally be flipped. The constant should be on the right side! `if(3.17 == possiblyPi)` would make me seriously question the author's motives.
2. It's personal preference, but in almost every situation I'd prefer to use a switch over an enum than lots of constants.
I can't articulate exactly why, but that should totally be flipped.
Should it? If you accidentally use an assignment operator (eg = ) instead of a comparison (eg === )[1] with the constant on the left your code will throw an exception, which is what you want. If you put the variable on the left and accidentally use an assignment then you'll overwrite the variable with the constant value and return true, consequently executing whatever is in the block of code that the comparison is supposed to be checking for. You don't want to do that. That could be really bad.
What you are seeing is people taking specific C codying styles into other languages. This is a cargo-cult style and probably in detriment of your code base.
On the other hand, it makes perfect sense in a C code base. The purpose of this is to transform a semantic error into a syntactic error. In C, both this expressions are legal but semantics is different:
if (A == B) //Compare B to A, decide on boolean result
if (A = B) //Assign B to A, then cast B's type
//into a boolean value (non-zero true,
//zero false) and decide on that.
In theory, it should be possible to identify every instance of A=B, but then it is hard to tell if it is a typo or the actual intention of the original programmer. If you compound this with the tendency to write complicated code, you find monstrousities like this one:
if (!A & B = C == D || E == F = G)
I am sure there's a language lawyer that can tell you for sure that the above means. I, on the other hand, can only be sure that this will compile as long as B and F are L-values.
Putting constants before variables in comparisons is relevant in Javascript for the same reasons that it is in C. Perhaps even more so, since the browser compiling the code won't have -Wparentheses to lean on
This 'reason' is as old as C, and it's always seemed to me like tying bells to your shoelaces because it would be bad if you forgot to tie them and tripped. I.e. the 'solution' is orthogonal to the problem. If you can train yourself to put the constant first, you can train yourself to use the right operator.
> If you can train yourself to put the constant first, you can train yourself to use the right operator.
Right... that's assuming you work solo. In that case, you might as well be using some higher level language and sidestep the whole issue.
Most C programmers out there work in teams, in long lived projects... that means teams with rotation of personnel. This is a fuckup waiting to happen.
I get you guys do not like axes, and it is really Ok. But forgive me if I am skeptical of your axe redesigns until the day you actually go out and do a full hour of wood chopping with that fancy bastard sword of yours.
The argument applies just as well to each member of society. Imagine the savings in law enforcement if every one of us just followed the rules all the time!!!
You also have a tireless automated assistant who can check for this: the compiler.
Clang warns about this by default:
warning: using the result of an assignment as a condition without parentheses.
note: place parentheses around the assignment to silence this warning.
note: use '==' to turn this assignment into an equality comparison. "
gcc gives you similar warnings with -Wall or -Wparentheses:
warning: suggest parentheses around assignment used as truth value [-Wparentheses]
Why not stick with the more natural phrasing and let the machine handle the tedious work?
Sorry, I should have made it clear that what I posted was pseudo-code, so just assume that = is equivalent to == or whatever in the target language.
1) Yes, my issue was the constant being on the left side. I've seen it done a few times, and I'm sure it's probably just a "I did it that way when I first started and it stuck" or something similar, but it just never looks right to me. I just want to scream "But, you're comparing the variable !!!!". :-)
2) Constants are useful when there are going to be gaps in the values (message dispatch codes), but yeah, I also agree here - enums are always the way to go, if you can do it. Switches are also great, but can be problematic at times due to the way that they aren't always implemented in a consistent manner among various languages with respect to fall-through and breaks.
Hmm -- you're talking about programming in the small, as in line-by-line style.
Cognitive load also affects devs at the scale of a whole project, i.e. understanding how the program fits together across different modules, across state changes and across time.
What's the dick & jane solution for project-scale organization?
Bingo. People like to hold forth on the small scale - but what about communication between components?
I watched "The Art of Destroying Software" [1] the other day and I've been thinking about the concepts a lot. Thought provoking talk - apart from the surveys he keeps conducting.
Making a nice method isn't that great. But a nice "component" or "module" or "service" - that's the key. Try and break your program up into little programs. But not too little... maybe that's the way?
personally I prefer without chaining, makes much more sense as its what I've always seen until the last ~5 years.
The other part you didn't mention is that you have to amend all the functions like getFish, skin etc to return the object which I think makes their signature unnecessarily less clear.
Yeah, just because something looks like chaining doesn't mean it's really repeated calls on the same object. Even with some static-checking of types, it could also be a copy of the object.
While blocky, the repeated thing.do() format is explicit about what object is being operated on.
o = window.FindCanvasForObject(obj)
o.Color(RED);
o.MoveTo(10,30);
o.LineTo(50,20);
(Where, in C++ I would enclose this inside a { curly block } to give o the right type and make it local, and in Python I would add a "del o" at the end; and yes, I reuse 'o' to mean 'object' in these cases as much as possible, occasionally having "ox" and "oy" when I have x and y objects to deal with simultaneously).
I think it provides the fluency of method chaining with the readability of "standard" code, but this style seems to get scorn from both the chaining-loving people and the chaining-hating people. Oh well, different strokes etc.
Just to nitpick, those aren't quite equivalent. The second should be:
o = window.FindCanvasForObject(obj);
o = o.Color(RED);
o = o.MoveTo(10,30);
o = o.LineTo(50,20);
Which leaves more room for error in the general case - if the API gives you a new object each time, and you fail to assign it every time (or say you have multiple and get the letters mixed up, or you do this rarely and forget), you lose some of the config. It can be fairly easily missed in code reviews. When it's all chained together, there's only one possible interpretation if it compiles and only has one assignment.
But I can see where you're coming from. Chains can be abused rather horrifically. IMO part of that is because it's hard to make helper functions, e.g.:
window.FindCanvasForObject(obj).Color(RED).MoveTo(10,30).LineTo(50,20)
# plus
def move_line(thing, start, end):
return thing.MoveTo(start).LineTo(end)
# leads to
move_line(window.FindCanvasForObject(obj).Color(RED), (10, 30), (50, 20))
It breaks the straightforward left-to-right interpretation. So instead, people just make the chain longer and longer, forever.
(not as much of a problem with a language with open classes, but modifying existing classes for ad-hoc helpers is usually frowned upon too.)
You are correct, of course; the only API I used this with on JavaScript kept returning the original object (and that was documented behaviour) so it didn't matter.
Your point about helper functions is spot on. They break the form in C++ (and Python if you don't modify classes), but with this form it is still much better than breaking a fluent style line.
That particular chain breaks a rule that I find very helpful: when you see a vertical column of leading dots, those methods should all be acting on the same object. When you introduce a child object, indent another level.
The fact that calling .MoveTo(10, 30) would return the same object it has been called on, and not, for example, a handle to created movement animation, is not exactly obvious.
Modern pattern of methods which just return the objects that they've been called on, by default seems to be very trendy, but I fail to see how is it helpful.
Er... on the window, as the indentation and API tells you.
It may be "not exactly obvious" to you, but having been doing GUI programming for a long time, this makes complete sense to me. It is also, of course, a matter of personal style and preference.
(Aside: some friends tell me, a vi user, that the way Emacs works is obvious. I disagree, but then, I don't use it).
And as for "modern"... I've been using this since C++ gained references.
The indentation misled you here. The first method call returns a child canvas object that the other methods act on. See my comment nearby in this thread for an alternative way to indent that avoids this confusion.
Same way that you use any other API: by reading the docs. This is at least a very common pattern; it's highly likely that whoever maintains this code will have seen it before. And if you're using an IDE, the IDE will even tell you this as you hit '.'
BTW, it doesn't have to be the same object: if memory is abundant and CPU is not, it can often make sense to create a fresh object with each call. Doing this lets you share partially-constructed objects without worrying that mutation will result in surprising effects:
let box = NewCanvas()
.DrawLine(50, 0)
.DrawLine(50, 50)
.DrawLine(0, 50)
.DrawLine(0, 0)
let scene = [
box.Fill(Color.RED).TranslateTo(200,300),
box.Fill(Color.BLUE).TranslateTo(100, 300),
box.Fill(Color.GREEN).TranslateTo(400, 500)
]
scene.forEach(box => box.Draw())
That doesn't apply to any of the code examples posted so far in this thread: they would all require documentation about side-effects, shared state, immediate vs. delayed effects, etc.
I disagree. You've created a variable for no reason. I hate having more variables than needed. I have to worry about things like its scope, its mutability, anywhere else it might be used, etc etc. It's pointless to me.
Like another poster below, I do agree you should spread that out over a few lines though:
I love that term "Shakespearean code" — beautiful exploitation of the language, that when analysed deeply is amazing, but on the surface is just really fucking horrible to read.
Just so this is clear, in Java this pattern is very present, especially since Java 8 (Streams). Before that, there was the Builder pattern[1]. It's also present in C++/C# in the same form.
The if ($null -ne $blah) one is STILL RELEVANT. In PowerShell for example it differentiates between an empty array and either retrieving an array's contents, or only the nulls from an array.
If this was C, it should be NULL, and it is almost always better to just write `if (variable)` to check for NULL pointers instead.
If this was JavaScript, this check includes undefined too. Not sure why it didn't use triple equal.
If this was just talking about placing a constant value to be compared before a more complicated expression, I really don't see a convincing argument for either. Does switching the order reduce the cognitive load that much?
I'm the accidental maintainer/guardian/dungeon keeper of a bunch of scary code at work, which happily does:
if (false != aBooleanVariable)
I still can't parse that without stopping and thinking (and sometimes cursing the original author). To me, with booleans, this can only sanely be written:
if (aBooleanVariable)
Code with literals is often worse than functionally equivalent code without; literals are complexity. And I hate literal comparisons with true and false for booleans. Aargh.
// This was useful in C to avoid accidentally
// typing variable = null. These days it will
// confuse most people, with little benefit.
The use of "was" and "these days" in the link shows that the author of this piece is in a tiny little bubble of development, and is far from being able to give general advice for programming. C is not past-tense. C, C++, Objective-C, these are all still widely used today by modern professionals. I'm very tired of articles and authors which claim to be representative of all programming or programmers, when they're isolated to a tiny little bubble. Especially when they only know JavaScript.
> Don’t code “your way”. Just follow the coding standards. This stuff is already figured out. Make your code predictable and easy to read by coding the way people expect.
I'm not a C dev, but is "(null != thing)" still a convention? (Based on your comment it sounds like yes). If yes, it seems to me like he's saying: keep doing that!
Nowhere does he claim that his advice about this convention applies to every language (this would be silly) and not writing C should not preclude someone from giving programming advice.
The "generally applicable" advice that he does give is exactly that: general. Is it possible to write a blog post about code quality/complexity/insert-thing-here that applies to all circumstances? I don't believe it is. It doesn't mean that there is no value to be gained from exploring concepts that may apply.
> I'm not a C dev, but is "(null != thing)" still a convention?
Configuring the warning your compiler gives for 'if (x=y)' to generate an error instead is a vastly better convention that completely supersedes Yoda style, IMO. 'if ((x=y))' remains available for those who really want to assign in conditionals.
I also don't understand this, nothing wrong about Yoda conditions and I don't understand the 'cognitive load' argument since the condition is just as readable. And: Visual Studio 2015 does not warn in the case of "if (a = 5)", not even at the highest warning level, only the VS2015 static code analyser catches this.
With Visual Studio 2015 update 3, and /W4, I get "warning C4706: assignment within conditional expression" for stuff like "if(a=5)", both in C and C++.
I cannot generate the warning via command line flags only (including getting very insistent with /wall /w14706 /we4706 ) on VS2015 update 1, presumably a bug.
Interestingly I cannot seem to enable this warning at all via the command line. EDIT: From bellow, possibly a VS2015 U1 bug fixed on-or-before VS2015 U3.
What is wrong with Yoda conditions?
The very name is giving you a hint: they will feel natural only to Yoda.
To humans, they will feel as they're written backwards.
True. I was speaking of C++, which also accepts bool [0] (or more specifically, something that can be converted to a bool, which a null pointer can be [1]).
Actually [0] has a good example of when the if (Foo* a = ...) syntax is useful (dynamic cast)
To paraphrase the comment above that line, they say its purpose is to avoid accidental assignment. That's not an issue with "not equals" though, but that may just be their point.
As for whether it's more readable, I'm skeptical: It may save you going through some of the condition, but I believe getting used to it would make you more likely to overlook parts of the condition. Plus, the way it reads is the opposite of how you'd say what it does in English.
F̶i̶n̶a̶l̶l̶y̶,̶ ̶o̶b̶s̶c̶u̶r̶e̶ ̶a̶r̶c̶h̶i̶t̶e̶c̶t̶u̶r̶e̶s̶ ̶m̶a̶y̶ ̶d̶e̶f̶i̶n̶e̶ ̶a̶ ̶n̶o̶n̶-̶z̶e̶r̶o̶ ̶N̶U̶L̶L̶ (see to3m's reply). Using NULL makes it easier to find null pointer checks and conveys semantics (pointer vs integer).
To provide a different opinion, I'm not a fan of the macro NULL because the C spec leaves it rather open to compiler authors how to expand it. It says it must be 0, but they don't specify the type. It could be 0, 0L, (void*) 0, 0UL, 0LL, etc.
However, the spec does say that only the null pointer evaluates to false and all other points are true. So doing if(ptr) is much more well-defined than the NULL macro.
If anything, NULL's opacity is another argument for doing the comparison. NULL is guaranteed not to equal any pointer to an object/function, and the result of the comparison will be an int 0 or 1.
It's a poor example when testing for truthiness because it can be written more concise.
Imagine you're checking if the value is equal to a number or string. Placing the variable on the RHS avoids accidentally assigning a value to it if you mistype the comparison operator.
Nowadays you should do it the readable way and let the compiler detect problems for you. If you use -Wall on any modern compiler, this will be caught.
There are some weird use cases when you actually want assignment in the condition statement, but all those cases could be added with an extra line, e.g. the valid statement:
The former constrains the scope of someFoo to if statement, while the latter does not. Minimizing variable scope is one of the things I find really helps reduce cognitive load, so I'm not sure I'd want to make that replacement.
Code Complete is an old book now. Is it still good advice?
In my new project I see a lot of code like below. I hate it but I'm not sure if I'm old fashioned or correct in thinking it should be 4 or five lines. Should I reject a code review for stuff like this?
return (HadoopSummary)ScopeCoordinator.getInstance().findObject(Scope.getFirst(), new Path<String>(SCOPE_PATH.split("\\.")));
I still hear recommendations for Code Complete so I assume it's still useful, but haven't read it myself. A somewhat old book I like is Working Effectively With Legacy Code, I think its main premise and prescriptions can help a lot of codebases out there even if not all of them, especially those in functional languages. (Namely, your codebase will be better the more you get it under test and the more you leverage OOP design principles.)
Your code snippet looks like normal Java code to me. :) Not great that it's so common but it's at least not unusual... Being one line or more lines for that piece of code doesn't really matter to me but I'd prefer the single line in this case: I'm viewing it in an IDE, I've got more than 80 columns, and the pieces of syntax are easy enough to spot I don't need vertical cues. (Unfortunately rainbow parens still seem to be a minority preference.)
My own quick context-free review of that: is it testable in junit? Can you substitute a mock (without using something like PowerMockito) for ScopeCoordinator.getInstance() and for Scope.getFirst()? May be better to make the instance a member variable that you can mock by just passing a different one in the constructor. Why is it Scope.getFirst(), unless this method is explicitly about finding the first of something so it's clear in context? For the 'new Path<String>(SCOPE_PATH.split("\\."))' part, that looks like it's going to be the same every time and not dependent on any runtime code so why not make it a static member? (Or an instance member you can mock, or maybe the enclosing method can take a Path as an optional param with the default being the static one.) Can the design be redone to avoid the type conversion or is it too late?
Not counting the fact that the book is old, i would generally not recommend it.
The contents and topics have not changed that much but the book is just rather horrible to read.
For whatever reason the author decided to ramble on for way to many pages instead of getting a point across in a concise manner. The whole thing should have been done in 300 pages instead of 900+. You will just start flipping pages without reading them and then put that gargantuan behemoth of endless words and sentences on the shelf (or return it to the library, delete the ebook or whatever).
That code does not look overly hideous (apart from the split magic string) just reformat it into a couple of lines.
> Code Complete is an old book now. Is it still good advice?
Software isn't a mature, rigorous field relentlessly marching forward into the future. Silicon engineering is, sure. But our field is constantly constantly rediscovering stuff from the 60's and 70's, and going through fashions and fads.
A software book being old really doesn't matter to me IMO. We still don't really know what we're doing yet.
Stone Soup: all you need to code well are just these few little pebbles of wisdom. Oh plus 15 years of learning how to apply them intelligently in any given situation, and subject to any given constraint. At which point you don't really need my advice.
These articles are addictive but I suspect reading Code Complete one page a day might be more useful and ultimately more enjoyable.
> Libraries and tooling can also be a barrier for new developers. I recently built a project using EcmaScript 7 (babel), only to later realize that our junior dev was getting stuck trying to figure out what it all meant. Huge toll on the team’s productivity.
While I agree that libraries and tooling an be a barrier[1], I think getting junior devs up to speed with the (latest version of a) language is part of the toll you're accepting when hiring a junior.
[1] Although this still holds true if you write the library yourself, so be reluctant in getting rid of libraries that save you more than they cost you.
The day the cognitive load is at an ideal point is the day you will lose your relevance to society, because it will be easily automated. I know that day will come; but I'm in no hurry given the current state of affairs with capitalism and a global oligarchy trying to enslave the rest of the specie.
Meanwhile, I'll be happy writing obscure and complicated code that I won't have to maintain. Giving the industry leverage for the near future.
At the risk of making too much over the section (and ranting risks in general :)) I get the sense that "Keep your personal quirks out of it" strays dangerously close to "don't learn anything new, don't encourage the team to do it either". Of course I agree with not being clever for the sake of clverness, and not going against the overall style of the team with your preferred style. But the article goes on:
"The problem is that people just want to fix their bugs and move on." Yeah, screw that, maybe if people spent some time learning better coding techniques they wouldn't have so many bugs? Take for instance this "trick":
Problem: any of those method calls can blow up with NPE, because they were written long ago by other not so careful devs and you can't simply rewrite. I see this all the time. Or a variant where foo.bar() is null-checked so they can call doZap(), but they still do (or edit it to do later) the rest of the chaining of doZap().extractZorp().toString(). When something inevitably does blow up, you get your bug to fix and then move on, but wouldn't it have been better to not have the bug in the first place?
It's not even that devs don't realize that code could blow up with a NPE, a lot of the time they do, they just don't want to do the ugly "solution" up front (that someone will end up doing when they fix the bug and move on anyway) of all the intermediary variables and if scopes checking for nulls (or a NPE exception handler in the middle of their logic) and convince themselves it probably won't ever be null. The Optional 'trick' lets them be lazy (low syntax overhead once you understand what map() and flatMap() can do) and safe.
Without even bringing up streams and lambdas, a nifty trick that appeared in Java not that long ago is the for-each syntax (which prevents all too easy to happen off-by-one errors in a loop counter). I keep up with language developments, I'm going to use new expressive capabilities in my code (when they're helpful -- again I'm on board against cleverness-for-cleverness'-sake) and anyone who has a cognitive load with it ought to learn it well enough so there is no load and we can develop more solid code. Ultimately I concede the point I've heard from Haskell or Scala advocates that as you practice all that Type power becomes less troublesome, I'm just not willing to invest the cognitive effort up front to get to that point since I think the tradeoffs aren't worth it for my use cases. The fact that I find a lot of Scala to be incomprehensible is a fact about my state of mind, not a fact about Scala or the developer who wrote the code.
In the end these aren't even huge issues. The worst bugs aren't often the result of presence/absence of good code or capabilities (security bugs are probably a big exception), they often happen before coding even begins and accumulate over time with more and more edits to a system without stepping back to see if the original design makes sense for the current system or whether we've been stapling things together. We focus too much on these small details about how it takes 30 extra seconds to parse a too-terse line of code that would have been easier to swallow if it was 5 lines and ignore the fact that we've got 30 classes for this feature (so modular and testable) that could have been done in maybe 30 terse lines of a more powerful language with perhaps some extra cognitive overhead upfront.
Don't blame me for the fact that competent programming, as I view it as an intellectual possibility, will be too difficult for "the average programmer" — you must not fall into the trap of rejecting a surgical technique because it is beyond the capabilities of the barber in his shop around the corner.
Dijkstra (1975) Comments at a Symposium
Whenever I read code written in the "so boring it cannot fail" camp I get exhausted. This code is inherently procedural and rolls itself into a giant ball of mud -- composition is difficult to achieve so as requirements change the code accrues more loops and conditionals until it is nearly incomprehensible.
It might start out neat and clean but rarely will it stay that way.
Good, non-leaky abstractions are key. This can even be achieved with procedural code but I think functional programming techniques like pure functions, immutable values, and a sound type system help a great deal... even at the expense of the initial "cognitive load," it takes to learn how to employ these tools.