Cognitive Biases in Software Development
smyachenkov.com
smyachenkov.com
Also, it just seems as if many are unable to step back and look at the broader side of things when coding. Asking questions like: Are we using the right concepts here? Did we develop sufficient abstractions? Case in point: I was just refactoring a code base for a client where the code was paved with a `MemoryHolder` type, where simply `Buffer` would have been the better name for the same concept.
A Holder type is a managed (RAII/garbage collected) wrapper that has a reference to some unmanaged/primitive object. If that primitive object is some notion of a contiguous region of memory, then MemoryHolder is a perfectly good name for it.
A name like "Buffer" might not automatically tell the users that the type is managed, or that it wraps some underlying primitive type. That may or may not be relevant to the user, but it's not obviously always irrelevant, at least.
So your college probably need some more challenge. Put him on binary (assembly) optimization or something, where he doesn't have to come up with variable names.
That said, naming things is probably one of the most difficult tasks in programming.
It seems a bit too similar to the idea that "most people simply cannot read".
We all accept that nobody can read without instruction, practice, and feedback. Why is coding somehow different? If anything, I think reading is more foreign/difficult, because coding is explicit thinking, and we all think. Whereas reading is a completely synthetic act that starts with arbitrary symbols that must be memorized by rote before you can even take the next step of using them.
None of us start out life being literate, but few people lack the ability to become literate. Why is coding different?
Most people can read in that they can translate text into sounds. However most people cannot read if require that they can accurately comprehend what the text says, just look at the results of reading comprehension tests, a majority scores horribly on them.
> Why is coding different?
You can get by just fine in life without accurately understanding text, but you can't get by just fine as a programmer without accurately understanding what code does.
Most people can modify a recipe, if the modification is natural or easily comprehended. For example, adding extra ingredients to flavour a food. But to invent one from scratch would be akin to creating a new type of food, or new way to cook, or a new way to combine cooking methods.
Remember that most people require extensive schooling for the better part of a decade in order to learn how to read.
The same holds true for literature, I think. Surely, there are many people how can read and write a piece of text. However, there surely is a qualitatively difference between a gossip article from the magazine and Shakespeare's Hamlet.
Yes. Lack of reading is observable even inside code bases themselves.
Some contributors are more prone than others to not read the codebase they're working on and break patterns, use different file naming and folder structures, disregard currently existing code and re-implementing things from scratch, use a completely different code style, use tabs instead of spaces.
I don't think it's fair to assume that those people are doing this out of malice, or even to push their own style, because as soon as you mention that to them, they admit they were just not paying attention.
To elaborate on the sibling comments, coding is generative work instead of passive consumption like reading. This split becomes easier to see when looking at a bunch of domains.
Most of people can learn to read a book but very few can write a good book. Similarly, it's easier to re-tell someone else's funny joke than be creative and write original comedy.
Many people can learn basic physics equations like "f=ma" is "force equals mass times acceleration" but fewer are able to be generative and discover new physics equations that are accepted by the science community.
Most people can listen to music and appreciate it, but a smaller percentage can play instruments. And then within the set of musicians, an even smaller percentage can compose new original music. There must be something more to it than "training and practice" to explain why a 19-year old Chopin can compose sophisticated piano compositions while most 70-year old professional concert pianists that have performed for decades with more repertoire than Chopin have written no notable original music.
Coding may not be as hard as creating "Theory of General Relativity" but it's not the same as reading literacy.
Likewise, a lot more emails and discussion comments and school reports are written, per volume, than masterpiece books.
{people who can look at a real world problem, choose an appropriate physics model, and apply the right equations, correctly}
and
{people who can look at a real world problem, choose an appropriate logical model, and apply the right language/frameworks/code, correctly}
are probably really quite similar sets.
I do not think parent poster is referring to junior developers or just people on the streets.
Even though they work as developers and have experience they still don't get it. Just like people who mechanically read symbols and put it into words but don't get the meaning behind paragraphs or whole stories.
I think you're probably right, but in a strictly semantic way: I think it's feasible that nearly everybody (excluding, say, the severely mentally handicapped) could learn to program to a reasonable level of proficiency, with enough effort. However, most people won't, so it ends up being the same as if most people can't.
https://www.oecd.org/skills/piaac/Country%20note%20-%20Unite...
I think, most people have potential to be able to code. By "can" I mean if they applied themselves to learning.
The problem is there's just no easy way to somehow transfer the knowledge and experience of what it means to actually code. The Doening-Kruger effect is in full force because you can't tell if you can code until you can. Moreover, almost every technology today must cater to complete beginners so it is easy to write a Hello World but then there is no way to tell you how to put your solution together. There is nuggets of wisdom but you need to have some prerequisite knowledge and experience to make use of them.
It is easy to see that I can't sight-read music. I mean I am amateur flautist but for some reason I just can't "get it" (but I also did not try hard). I know you can sight read because there are a lot people that can just take a printout and start playing it without previously studying it.
The same is not true with programming. It is not easy to see people do programming -- you only see results that you do not understand until you actually can code. You can appreciate the music but you can't appreciate the code until you actually can code.
It does not help that the whole industry is crazy right now. There is very small proportion of people with actual experience because of exponential growth of number of developers. The salaries are inflated and newcomers are getting paid as if they were experts in other industries. If you are getting paid a lot it means you must be valuable, no?
And if you actually are experienced and try to offer a newcomer some guidance in writing code that they won't regret having written a year from now, they'll accuse you of being a "perfectionist" who's wasting time trying to obtain some theoretical optimization.
My time spent teaching makes me think otherwise. I saw so many smart, motivated young people who simply could not get their homework done. This was especially discouraging since my alma matter has a very strong focus on pedagogical programming.
There's something about programming that "clicks" for some people. They can look a the little pieces and immediately understand how to put them together. If you don't have that spark it's going to be an extremely challenging journey.
Unproductivity that feels viscerally very productive.
Alternatively, command line tools tend to be created and increase the productivity of the more technically inclined, but are opaque to the less technical.
Which effect is the GibbonsRCool's Law? Or both? GibbonsRCool's Laws of Skill Drag and Acceleration?
Criticizing someone for being biased is an ad hominem; dismissing their arguments because of it is committing the genetic fallacy.
In the extreme, it reminds me of this blog post, about how knowing about biases can let some folks universally dismiss others they disagree with (and thus manage to never learn or consider alternative ideas): https://www.lesswrong.com/posts/AdYdLP2sRqPMoe8fb/knowing-ab...
More and more, I write code I'd almost be embarrassed for colleagues to review. But for the type of work I'm doing (poorly defined, highly volatile, potentially short lifespan), I can't justify anything more.
It doesn't feel good and it's still hard to accept that I'm doing the right thing.
If the language I'm working in supported FP, then my world would be much better.
Sometimes I don't really know what I'm doing, just getting all tests to pass. Then once it's working, when I know why it works, I will clean it up by removing unnecessary variables, double negations, and name the magic numbers, etc, and try to make it as simple as possible.
You cannot imagine how I'd value having really dumb looking code in my life.
Yet here I am, swimming in a swap of cleverness that I can't refactor nor will ever be able to wrap my head around.
I envy you.
It is generally easier to write dumb code that is consistent. It requires less mental energy and is often a very good starting point for refactoring into something more declarative and DRY.
The pain comes from inconsistent code, often either based on wrong previous assumptions, premature optimization or time pressure.
Assuming FP means functional programming and not function pointers, what language these days doesn't support functional programming?? Are you coding in AWK? Seriously almost every language but C supports higher order functions, anonymous functions, and recursion (though recursion is an anti-pattern and iteration is superior).
It felt nasty, it felt ugly, but when time came to actually keep up the desired functionality, put a break and comb the system, it was great to have a single place from where to extract common functionality from. At this point, it was obvious what functionality needed extraction.
It's sad that too many people are so afraid of technical debt that they demand perfection at first try. At their first try, they don't even know what perfection is.
And I mean anywhere: "If the language I'm working in supported FP, then my world would be much better" :) I've seen the fuckups that are possible in FP, and indeed worked with a guy who used it to make things as complex as possible. You can really produce nasty code because of it's much-touted compositionality in the hands of a dickhead can generate horrors. Map within a map composed with a reduce composed with... in one statement.
Add in statelessness, which can complicate things in some cases, if said prat pushes statelessness due to the latest blog article he read, it can get worse.
From memory: "against stupidity the gods themselves struggle in vain".
"He who considers too much will perform little."
The perfectionist's curse? I have to say there's a core of truth to that! Analysis paralysis IOW.
This. This is the reality of software development, especially in a startup. You have to quickly adapt your code to the <not well defined> client's needs and to the business constraints, because if there is no cash, the company wouldn't exist anyway.
You can't do that with "perfect" code.
Actually, I would say that the exact opposite is true: only perfect code is perfectly adaptable. The times I want to yell at my past self are the times that he wrote something quick and dirty that I now have to rewrite because the requirements have changed.
The question of whether to use an inefficient, readable implementation or an efficient, cryptic implementation can mostly be solved with the correct class topology. If classes are SOLID, then it is easier to justify a cryptic implementation of one method as that is hidden from most other developers. A class that will be inspected by others more, such as a business logic service layer, should lean towards inefficient, readable code.
Likewise, the question of whether to reuse or roll your own can also mostly be solved with a good architecture. Logically separating components makes it easier to tailor the implementation to the use case, and replace if necessary.
As always, breaking the problem into smaller problems is most of the battle.
Sadly software development as it is taught and measured by interviews, is mostly about programming and algorithms. The real art and value in software development is interface design and system architecture design.
Valid (in my opinion) lessons I cherish today are keeping individual parts of a code base simple and understandable, documenting and testing a lot and preferring simpler solutions over complex ones.
I abstract far more carefully now and I don't mind a few extra lines of code that make it easier to digest at a glance.
Also experience with redactoring away a wart only to find there is now a wart somewhere else.
If you are building a system that will be used for years and needs to be extensible than it makes much more sense. Just like it would make sense if you got your PCB back and needed to change features or add new ones.
Elegant code is easily just as bad as spaghetti code. I can't even begin to quantify how many hours of my life were wasted because someone thought it was more important to make something elegant rather than understandable. I get that it's satisfying to make something that "makes sense" if you've built a mental model of the problem from the beginning, but if it's incomprehensible to others(without serious devotion to figuring out what is going on) then it might as well be crap in the first place. At least spaghetti code can be fairly easy to hack because there's usually lots of duplication and specialized code, making it straight forward to step through with a debugger and make a change without mysteriously borking the entire app. Elegant code, ironically, can be more flimsy because it's usually written assuming that the system stays perfect, and changes to the system reveal single points of failure.
Clean code can be written without necessarily going overboard with elegance to the point where it's not easy to understand. Even dirty code can be workable given documentation(can just be comments explaining what things do) and consistency.
In a recent interview question I was asked to find the largest three numbers in python list. My first attempt looped through the starting list, comparing the number to the previous minimum number in the result list - replacing the number if it was larger. I needed a get_minimum(function). It was the obvious solution that came to my head at thr time.
An hour later I realized a far more elegant solution was to sort the list and slice the last three elements. It felt far more elegant to me and was as easy to understand as the initial obvious solution that came into my head.
(Though this is why I dislike these type of interview questions. the first solution in an interview situation is not always the best or final one)
Edit: Stack Exchange seems to agree that readability is part of what makes code elegant. https://softwareengineering.stackexchange.com/questions/9791...
I think we should maybe aspire to find a frame of reference for evaluating when to change and not.
During development, I find iteration can be useful, i.e. when you have code that's 'hot in your mind' and can be re-worked.
Another key that the article doesn't seem to reference is encapsulation. It's one of the most golden concerns in software.
If 'ugly' is confined to a single function and doesn't leak outside - then it's almost pointless to re-write it.
On the other end of the spectrum, if an API is 'ugly' it leaks all around the code inside and out. Re-write is more expensive, but could possibly be more worth it.
Now that I've been away from mandated code reviews for quite a while I can understand why reviewers go to the trivial. Firstly, "code reviews" are really "system reviews". It's difficult reviewing a project whose requirements you don't know with a design that came about through iterative design and possibly in a language you don't use.
My current team is small and split into an integration group (mine) of two people and a UX group of two people. My group partner and I look over each other's projects and stay up on the requirements as the project progresses. But it's completely informal. Yet it seems to work better than having a group of 2, 3, X number of randomly chosen developers from my department.
The whole point of naming things is that you don't have to dive in this 60 LoC function, but rather can use its name to know what it does, and make a global mental model.
If you're not naming things correctly, you effectively make it almost impossible to review the design, and that's why people starts with asking about names.
Reviewing names is the exact opposite of picking local stuff. Names have no importance locally, but they have tremendous one globally.
So, please, use good names ^^
(I'm not trying to be exhaustive. The examples above are common ones I've seen regularly and they make me cringe.)
I don't need to know the variable names used in these cases. The very act of not using a relational database properly or not handling exceptions is easy to spot.
It's good to have someone else looking at your work in established projects. Pair programming provides some value to overcome this problem.
Overcome your weakness, you'll become a better developer.
> I call this, Developer Inertia:
should read
> I call this "Developer Inertia":
Then these guidelines should be followed, no need for long discussions during code reviews.
Variable name does not follow guidelines => Mark it as an issue, defer to the guidelines move on.
A standards-compliant name that interferes with understanding is not okay. A name everyone gets, which happens to not follow a rule in some rulebook, is probably fine.
Use a good name and be done with it. You're naming a variable, not the title of your Magnum Opus.
Edit: I think people are reading too much into the initial dismissiveness and not going past the 1st line.
More important than picking the perfect name:
- Being consistent with the naming (you called it a 'bolt' keep calling it a 'bolt' for the same type of object and for its life through the code path. If bolt is not a great name you can change later.
- Being easy to remember and type. There's no point with TurningWheelThatGoesSqueak and having a TurningWheelThatGoesSquak and a TurningWheelThatGoesSquewk this will only make the developers go crazy. Simplify.
And yes bikeshedding is bad. I just find it too ironic that parent's username reminds me of the place that took bikeshedding to the extremes.
Sometimes the failure at finding a good name (and taking 30 minutes to name your variable) means your abstraction is not good and/or your variable encloses multiple concepts.
Of course, a short-spanned internal-only variable used just once or twice doesn't deserve a 30-minutes debate; OTOH a public variable which could be externally exposed by a class could require a bit of thinking.
You can read a sample chapter online, which is the chapter about naming. Highly recommended! https://leanpub.com/elementsofclojure/read_sample
You're naming a variable, and that name is probably the most important documentation about that specific point in the code. A lot of the time a useful name will be quite obvious, so you should use one. If you see code that has variable names like a, i, myVar, value, etc then it's usually a sign the developer didn't think very hard about the code that they were writing. Using a name that gives some context to the data the variable should hold is massively useful to the next developer to work on the code (which is usually you, so you'll be thankful you did).
You can always read the context and see 'for value in list_of_values' or understand what's it that you're working with right now.
Yes, be more explicit on the tricky parts, but sometimes i = i+1 is just fine.
Except they're not as good as 'index', which is obvious and provides some meaning, so why accept single character name?
I have an eslint rule that blocks single character var names on my projects. It makes my team develop good habits, and no one has ever complained about it. Our code is very readable.
Because readability suffers with repeated long variable names
There's a reason why math uses i,j,k and x,y,z and it's not to be petty.
Even in the meta-sense it is being discussed here.
There is something about naming that we like thinking about, and fine tune our thoughts on, more than a simple need for legibility.
There is a sense of ownership in picking a name.
Way to open up a constructive discussion ;)
Any extreme of naming is bad. Spent zero time thinking about naming and you end up with a codebase where the same thing is called differently depending on how the person felt that day. Or core data structures are called "node", "element" or "link", words that are already overloaded and should be avoided.
On the other extreme, thinking too far about naming leads into bikeshedding and no work getting done.
So as with many other things, balance is the thing that gets you the furthest.
// TODO it works, but it's ugly, rewrite
function init() {
// some code
}
What is ugly, what would rewrite accomplish? Excellent example of bad commenting. Comments are ideally unnecessary, so bad comments just litter code and stink it up even more. No code will live forever anyways, and it says something about someone when they falsely believe in perfection.I also go back sometimes and do just that. without the comment I forget 95% of the time.
I used to have this problem all the time with the managing director:
MD: Company X has a really nice and simple help system. Why can't we do the same?
Me: We can, but we need a product owner, a designer, a developer and a tester full time on this for at least a couple of months.
MD: I don't understand why we can't just copy what they did.
What was your reply?
Sample: https://i.imgur.com/QUO48fn.png
Websites can write stylesheets specifically for light or dark preferences. Browsers currently derive this information from the operating system's preferences.
[1]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pref... [2]: https://drafts.csswg.org/mediaqueries-5/#prefers-color-schem...
On the contrary, there's software written all the time that has a realistic lifetime of a few years before it's either replaced with a 3rd party alternative or rewritten by some disgruntled programmer. Deadlines and business priorities often mean that foregoing what most of us would tout as "best practices" actually makes sense. Some software is only used by 10 people at a time, and can be maintained by one or two engineers. It probably makes more sense for the engineers to choose what's best for them and the company over what most people would say are the right things to do. There are people I know who don't write tests for their software(something I don't know that I would ever do), but each piece of software they write is small and has a lifespan of a few years at most, and the advantage to removing barriers to development is that they can make changes very quickly and get things done.
If anything, FAANGs have resources and intertia to actually work on technical debt. Smaller companies will just churn and churn and churn until they bog down into unmaintainable mess of a code and can't respond to market changes anymore because their codebase is impossible to change or maintain.
Then they'll usually call us to fix their issue and be angry when the answer is "there's no easy way out of the mess you caused" while their competition is moving ahead of them.
I often consider path dependence in product development. The decisions we face for any given circumstance is limited by prior decisions and experiences, even though past circumstances may no longer be relevant.
It is part of human nature to be hugely biased to a completely absurd degree and software engineers and scientists are not immune to this effect.
The worst that I've seen this in (to the best of my ability, as I'm cognitively biased too) is in design patterns.
Typical scenario:
Object instantiation requires 50 parameters to be fully realized. Some coworker suggested that we instantiate an object with empty members and create 50 methods on an object that each take one parameter to load the object.
Apparently having a function that takes 50 parameters create the object was a code smell because it didn't have a fancy name. That fancy name was "builder pattern."
If you can't see how utterly stupid builder pattern is for this case... well... cognitive bias.
You cannot win against the prevailing wisdom. Try having a public mutable field on a class, and watch people lose their ever-loving minds. But why do we have more than half of the classes' exposed with getters and setters? Mmmmfh.
I think the problem is that it is really hard to reason about software in the large. So we reason about software in smaller and smaller granularity, e.g. classes. And then we want to load down every single class with armor as if it needs to be protected against the wild hordes of unwashed masses who will rampantly mutate it. But most classes naturally fit into a larger unit, a cohort of classes, and they are coupled with each other, and they mutually share state. You can't understand the whole thing by staring at a single class, anyway. But it feels like we are doing the right thing because we are told every class needs armor. Nevermind that now every class is 3x bigger, and so the whole thing is 3x bigger, which makes it 9x harder to fit in your head! Uggh!
Not a language option? ... Oh, so simple ... and yet, so far away.
Why? Because the instantiated object requires all 50 parameters to be fully realized. Having to call 50 separate setters means you can create an "invalid" object where only 23 setters are called. Why allow this to happen at all? It's like a null value, why allow that value to exist on a type?
If you need all 50 parameters to be fully realized, then the logical thing to do is to only allow the object to exist with all 50 parameters in place. A constructor with 50 parameters insures that this fact is reality. It's that simple.
Yet even you, with the answer right in front of your nose staring you in the face still thought that you 50 setters is okay with syntactical sugar! That's how powerful cognitive bias is with design patters. "Builder pattern" is a word that lends a sort of artificial aura into what is essentially stupidly dividing up a constructor into 50 setters for no good reason.