Writing good code: how to reduce the cognitive load of your code
chrismm.com
chrismm.com
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.
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.
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.
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).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].
[1]: https://sites.google.com/site/unclebobconsultingllc/blogs-by...
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.
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.
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.
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.
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.
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.
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!".
[1]: http://www.catonmat.net/blog/difference-between-edsger-dijks...
[2]: Sometimes I wonder: Is there a "named law" for this? If not, why? It's never just a prototype.
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.
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.
Maintainability or the cognitive load of the reader or writer completely depends on the proficiency of the reader or writer.
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.
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.
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)validated_items = [x for x in items if is_validated(x)]
validated_items = [_ for _ in items if is_validated(x)]
where _ is used as a safe variable that can always be clobbered. (Though this does also clash with its use as a gettext function for strings).
validated_items = items.select { |item| is_validated?(item) }
Though I'd expect a check for validation to be an instance method, so it'd probably look like: validated_items = items.select(&:validated?)Ruby seems to be going down the same path as Perl in trying to make all possible combinations of characters valid programs.
my @validated = grep { is_validated($_) } @items;
I'd argue the Perl version is clearer... assuming you know its syntax, that is. validated_items = items.select(&method(:is_validated?))
(Typed on my phone but I'm pretty sure that's the right syntax)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...
To me the name is crystal clear.
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.
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".
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.
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.
Rules of thumb only go as far as your thumb. Sometimes a clever abstraction makes everything clean and obvious, as opposed to a dumb abstraction.
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.
Be Hemingway, not Nabokov.*
*not to say Nabokov wasn't clever.
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".
Peace is God
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?
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.
Programming has none of those things.
http://www.linusakesson.net/programming/kernighans-lever/
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.
My comment there is still relevant here:
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 don't quite see how cognitive load is a thought-terminating cliche though.
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.
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 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:
https://brains-on-code.github.io/ (I am the first author of this piece).
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.
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.
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.
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...
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.
> 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 :)
"Hey, look what I cooked up in my basement using haskell".
- which to me, is engineering, not science.
I am sorry to disagree, but this is just not true.
http://www.ptidej.net/courses/inf6306/fall10/slides/course8/...
http://www.cs.kent.edu/~jmaletic/papers/EMSE12.pdf
https://link.springer.com/journal/10664
https://scholar.google.com/citations?view_op=view_citation&h...
I have to say this:
https://brains-on-code.github.io/shorter-identifier-names.pd...
My supervisor has to say this:
http://pi.informatik.uni-siegen.de/stt/34_2/01_Fachgruppenbe...
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
I agree.
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.
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.
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.
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.
1: https://www.amazon.com/Art-Readable-Code-Practical-Technique...
KISS being higher priority than DRY.
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.
It's a personal opinion, but I had to share.
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>... :-)
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.
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.
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.
[1] If you use == then you are bad and wrong.
re [1] - javascript isn't the only language == isn't bad and wrong everywhere!
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.
And you can test your code.
Ideally, of course. But pragmatically mistakes happen. The point is that there's no cost - so why not use an explicit order and the right operator?
And you can test your code.
A lot of developers don't, or they only test for simple success cases that wouldn't catch this class of bug. It's scary.
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?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.
Seems bad style to me, but also contrived. Whereas
> PI == validate(parseFloat(fetchUserInput({ timeout: minutes(1), defaultOnCancel: 0.0 })))
as pseudo-code example seems rather more insta-grokkable to me than flipped. Get my drift? And it's lit==varExpr
but I also generally use languages that don't have equality and assignment operators that are so similar that people are prone to mixing up the two.
No danger in languages allowing for FP-style immutables (whether via `readonly` or `const`) luckily
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 find it perfectly natural to read "if 5 == x:" without having to flip it around.
(And it's not about "cognitive load" on the compiler, it's about fail to error not fail to silent success.)
I'm quite happy that all the languages I use regularly disallow assignment in ifs, at least.
So no, even including side effects testing for (in)equality is commutative.
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... ;-)
Me.getFish.skin.debone.flour.salt.fry.eat
It would be something like Me.getFish
.skin
.debone
.flour
.salt
.fry
.eat
I find this more readable than doing it without chaining like this: fish = Me.getFish
fish.skin
fish.debone
fish.flour
fish.salt
fish.fry
Me.eat(fish)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.
While blocky, the repeated thing.do() format is explicit about what object is being operated on.
window.FindCanvasForObject(obj).Color(RED).MoveTo(10,30).LineTo(50,20). ...
I much prefer: 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.
window
.FindCanvasForObject(obj)
.Color(RED)
.MoveTo(10, 30)
.LineTo(50, 20)
This avoids the repetition of the local variable name, while still making it clear what's being called on the object.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.
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())Best APIs are written in such an obvious way that you don't need to read the docs to read the code and understand what's happening.
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.
So I would format that like this:
window
.FindCanvasForObject(obj)
.Color(RED)
.MoveTo(10, 30)
.LineTo(50, 20)
Or more compactly: window.FindCanvasForObject(obj)
.Color(RED)
.MoveTo(10, 30)
.LineTo(50, 20) 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.)
I most often use this with C++, where it is
{
CanvasObject *o = window.FindCanvasForObject(obj);
o->Color(RED);
o->MoveTo(10,30);
o->LineTo(50,20);
}
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.Like another poster below, I do agree you should spread that out over a few lines though:
window
.FindCanvasForObject(obj)
.Color(RED)
.MoveTo(10,30)
.LineTo(50,20)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?
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?
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 (null != variable)
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?
Imagine that it was this instead:
if (5 != variable)
As the author points out, this was typically used by C and C++ developers to prevent accidental assignments inside conditional statements.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).
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.
// 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.
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'd rather call it a trend than a convention.
Close enough works for horseshoes but not for C. It has to work 100% of the time to be acceptable (int variable = 0;)
Maybe he wanted to check for undefined too? Most often you want to treat it the same as null.
if(Foo* a = GetAFoo()) { /* Do something with a non-null foo */ }
is perfectly valid code.Actually [0] has a good example of when the if (Foo* a = ...) syntax is useful (dynamic cast)
[0] http://en.cppreference.com/w/cpp/language/if [1] https://ideone.com/im4uVn
(C4706: https://msdn.microsoft.com/en-us/library/7hw7c1he.aspx)
#pragma warning(error: 4706)
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. 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. return (aBoolean != false) ? false : true;
So ridiculous that it made me laugh.if (aBooleanVariable) will return true for any truthy value, not just 'True'.
Of course I still prefer writing my if statements that way, it's just something you have to watch for if something does go wrong.
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:
if(Foo* someFoo = getFoo()){
could just be replaced with Foo* someFoo = getFoo();
if(someFoo){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.
This is called Yoda conditions: https://en.m.wikipedia.org/wiki/Yoda_conditions
Edit: Many people already commented the same thing. Sorry for the noise!
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("\\.")));
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?
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.
That code does not look overly hideous (apart from the split magic string) just reformat it into a couple of lines.
These articles are addictive but I suspect reading Code Complete one page a day might be more useful and ultimately more enjoyable.
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.
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.
"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":
String blah = Optional.ofNullable(foo.bar()).map(Clz::doZap).map(OtherClz::extractZorp).map(OtherOtherClz::toString).orElse("");
The normal "just leave me alone and let me code and fix bugs and get on with life" equivalent is: String blah = foo.bar().doZap().extractZorp().toString();
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.
Most devs can't handle overuse of new tech. It seems like the junior qualifier was put into place in order to get away with this bit of hypocrisy.
The introduction of new tools will absolutely lead to increased cognitive load until the entire team is familiar with it.
var = "%s code %s are bad"
var %= "Stupid", "tricks"Is the mouse cursor an actual HTML element moving according to pre-recorded session?
Really would like some more details on this. I've never seen anything like it.