Cognitive loads in programming
rpeszek.github.io
rpeszek.github.io
Assume the developer working here just had a 45 mins sync, has a meaningless repeat meeting about vapourware in 10 mins and a 2 hours town hall meeting after lunch... and still have to deliver that mess of a requirement you made him promise to deliver before any of these interruptions were even on his calendar!
- Always aim for declarative programming (abstract out the how it does it),
- limit the depth of the function calls (rabbit hole developers...),
- separate business logic from framework internals and
- choose composition over inheritance.
- Also avoid traits and mixins (run from the dark magic)
- don't over document the language or framework, only the eyebrow raising lines, the performance sensitive stuff and the context surrounding the file if it isn't obvious.
- name stuff to be easily grepable
Easy rules to go by, (there are probably more), they can make or break your ability to work, so that you can get interrupted 12 times an hour and still commit work.
I don't find these in books, just decades of sweating and pulling my hair "why does it have to be so hard!?" I have met plenty of senior devs who naturally do the same thing now.
The code size fallacy is a prime example of the wrong way to look at it. Plenty of extremely large code base in C++ are far more manageable than small JavaScript apps.
Mixing boilerplate framework junk with your intellectual property algorithms "what makes your software yours" is a sure way to hinder productivity in the long term.
You write code 3-4 times. You read it 365 times a year.
One last thing I recommend if you deal with a lot of interruptions and maybe multiple products, various code bases... keep a
// @next reintegrate with X
The @next comment 'marker' is a great way to mark exactly which line of which file you were at before you left this project, for a meeting, for lunch, for the day, etc. And it allows you jump back into context by searching for @next and go. Also since it's visual and located, your brain has a much better time remembering the context, since we're good with places and visual landmarks.It's far more efficient than roughly remembering what I was doing, looking at the last commit, scrolling endlessly through open files. Don't commit @next though :)
For example, if your code involves a lot of linear algebra, then operator overloading the sensible operators is probably a good thing. But don't use operator overloading just to save some keystrokes.
Over-abstraction is another such case. Try to avoid adding abstraction layers until you need them.
I'd also add "make the code as self-documenting as possible". That means being verbose at the right times, such as writing meaningful identifier names.
And of course "avoid global variables". I've seen people use singletons with a read/write member, which is as much a global variable as any other.
I forgot to write that one. In fact, I'd say, try to write everything as stateless as possible. That means no shared variables, functional style programming whenever possible (and logical, not for the sake of it).
Debugging a code base with global variables vs. passing what it needs is far easier to reason about when reading the code, but also easier to write tests for. Very good point.
State is evil, but of course you need some otherwise your software doesn't do anything real.
If a state is passed through dozens of function calls, this may indicate that it is shared by too many parts of the code, or that it is managed at the wrong layer of the architecture. When using global state, these potential problems are difficult to spot.
Choosing to ignore input data does not reduce cognitive load. You are not supposed to ignore the core function of your work, and call it a good practice.
It seems you're mixing things up. "State" in this context doesn't mean caching results. "State" in this case means that your code has internal state that you can't control, thus your components behave unpredictably depending on factors you can't control.
Picking up your caching example, what would you say if your HTTP cache either returned your main page or some random noise in an apparently random way, even though you're always requesting the exact same URL?
If I'm not writing pure functions, then there is some piece of information whose lifecycle suddenly requires care and feeding, especially if there are environmental factors making that state more 'interesting' than the code in view.
- Make your stateful components "dumb". The cache should just have dumb put,get,delete methods.
- Make your "smart" components stateless. Your component deciding which items to cache or remove or invalidate should be passed all the state they need to make their decisions as immutable parameters. This will make your lifecycle management code much easier to test or log.
- Keep the part gluing the dumb state and the smart functions small
In the end you still have a stateful component, but as a whole it should be easier to work with. For example, it's much easier to test time-based lifecycle decisions if you pass in the current time than if the component grabs the current time from your system clock.
Write some simple code that does the thing. Then debug it. Then profile it. Then swap out for some more efficient algorithms if necessary in the hotspots. Then look at microoptimizations. In any case, don't spend more time optimizing that it will save in runtime across the lifecycle of the code.
One problem is that saying to write in a "functional style" is not a reliable way to communicate intent today. You must qualify it with an explanation that includes/emphasizes the "no shared variables" part. Otherwise it's ambiguous, because it's not uncommon to come across programmers who make heavy (over)use of closures and refer to it as functional. It's the "literally" vs "figuratively" of the programming world; people say one thing that turns out to be the exact opposite of what it means. This should be called PF ("pseudo-functional") to contrast it with FP (what you're talking about).
Operator overloading can be totally useless in linear algebra applications as the algorithms for basic linear algebra operations (read BLAS) aggregate multiple algebraic operations (axpy, gemv,gemm).
I've written renderers with both operator overloading and without, and the readability of the former is, to me, vastly better.
grug write readable code instead. readable code maybe need clever now, but no need clever in future
No, no, no! Do not do this because then I have to go read all of the abstractions to actually figure out what the hell my computer is actually doing.
It seems nice in theory, but I’ve spent way too many hours stepping through over-abstracted declarative code with my debugger, getting frustrated at the opacity of it all.
These leaks are usually performance related.
IO has not been following moores law. So you can see high level abstractions around IO leak like crazy.
And also, there is no "retiring" going on. Retiring something because of a bad abstraction almost never happens, whether it's because of performance or not.
The bad abstractions stay forever. You just don't notice it. You just master the abstraction and keep on using it.
Learning about the leaks and learning how to manipulate the high level abstractions to get around the leaks becomes an intrinsic part of the culture. It becomes so intrinsic that most people can't even wrap their mind around the flaw in the abstraction. They get defensive if you talk about it. It's invisible, but it's still there.
SQL is the most leaky abstraction I have ever seen (in terms of performance). Learning to deal with it and all it's leaks are the norm now. People don't even refer to a query planner as a leak. The technical debt is so entrenched within our culture that it's actually considered to be a great abstraction. And who knows? Maybe mastering SQL with leaks and all is not a bad way to go. But mastering SQL involves dealing with a declarative language and manipulating it in ways that leak into the underlying implementation. No amount of optimization has fixed this.
SQL isn't the only thing with this issue. For SQL the crappy abstraction is just performance related. But you see other abstractions continued to be used because simply it's too hard to retire them. Javascript, CSS, HTML, C++. Python2 (finally this has been retired). Basically it's everywhere.
Abstractions are rarely retired. Abstractions with leaky performance problems are also never retired.
(Actually I think 99% of micro optimisations are harmful, it’s the higher level algorithmic optimisation that gives the best gains.)
In that case, I'm with you, I'd rather see a pile of imperative code...
We should as an industry be teaching people how to build good abstractions, not avoiding them at all costs.
Then it’s not declarative
> The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise. (Djikstra)
It's really hard to give a good example (because it's not an easy problem), but I really want to see a high-level procedure that describes (declares) every important thing that is going on (from a business standpoint). Then ideally, I only have to dive into each function once to get enough context on the main procedure. Any further dives are library/util-type calls (again, I'm talking super ideally here).
And even though I used the term "procedure" twice there, I'm talking about either FP or OO styles.
And of course (oh boy) the context can change everything.
I also feel that Local Reasoning is a very powerful capability in code. In fact, Martin Odersky the creator of Scala pointed to an article that headlined Local Reasoning as one of the clearest explanation of Why FP matters (https://twitter.com/odersky/status/1271182333467602945?lang=...).
Any abstraction (here I include complex types) is a trade-off against local reasoning. Anything that makes the reader of the code drift far away from what they're currently reading on the screen is a hit against local reasoning. So they've to be introduced judiciously.
Maybe that is the juice that makes Java the 800lb enterprise gorilla that Java is, but that juice seems hardly worth the squeeze of stack traces that go on for multiple screens.
spreadsheets are a great abstraction on calculating values based on tabular data.
ORM is not such a great abstraction on top of SQL.
I agree wholeheartedly with that. I'm not sure that's the direction an ORM should be targeting the abstraction in the first place. It seems more productive to abstract the code away from the point of view of the database than to abstract the database away from the point of view of the code.
I think the database's query results should be exposed directly, so that the developer could control the DB properly and directly (such as get access to cursors, etc)
Or this
Line of code <<<<<<<<<<<<<<<
With pointing, which breaks it.@next is a nice way to formalise a break though.
I've seen the word "trait" used to mean different things in different language communities. Do you mean it like in Rust, in Ruby, in Scala, or in C++? (Since you mention mixins, I'm guessing like in Ruby?)
Too true - I remember working on a PHP project where traits weren't allowed and there were maybe two times that traits would have made life easier but they were easily worked around.
Check out A Philosophy of Software Design, 2nd Edition https://a.co/d/dZ8PJpt
You've not been reading good books then.
Stuff like "separate business logic from framework internals" is one of the main messages of any book on software architecture, including Bob Martin's classic Clean Architecture.
Then "choose composition over inheritance" is Object-oriented programming 101.
Then the suggestions to not over comment and "name stuff to be easily grepable" are key takes of Bob Martin's Clean Code.
They are all good takes, but hardly unknown.
If I have meetings loaded I am telling whoever is planning the sprint I am reducing my load for meetings. We, as software engineers, cannot allow this nonsense to continue. If I am interrupted 12 times a day I am bringing it up and the next sync. If it doesn't get fixed I am leaving. No one pays me to sit in meetings all day. If you want code delivered leave me alone or burn through young guns that don't know what they want yet.
While your post has a lot of good meat it is undermined by this single statement. It's far easier to work when you don't need to proactively reduce cognitive load to caveman levels because developers are by-and-large overworked.
I understand and agree with your frustration, but I don't agree that the statement undermines the parent comment at all.
https://github.com/microsoft/codetour
Basically record the last 5 minutes of your work and replay as code tour.
One other alternative is Idea/Rider's Local History feature which is basically a parallel repo of your entire project that automatically saves a snapshot of your changes every time you save a file. Super useful for all kinds of backtracking, and you don't even need to remember to use it until you actually need it.
> limit the depth of the function calls
These two are at odds with each other. The more declarative your code is, the deeper your function calls will be. At the extreme case, you will have an DSL-like code that has an intractable stack trace.
Front-end developers hear "no abstractions is bad!", and they don't take into account that something like React already provides an abstraction more powerful than all the abstractions put together in some other programming environments. Having a localised section of UI bundled with everything driving it in a single 20 line file is absolute light years ahead of the dumpster fire of the 2010 web, which in turn was probably light years ahead of whatever people were doing with IE and Netscape in the 90s.
So they do crap like separate into "logic" and "display" components, or they import 6 heavily abstracted libraries on top that give virtually no additional benefit and just add 8,000 lines of third party documentation to the already heavy cognitive burden other developers on the project have to understand the code.
Likewise even though I've never worked on one, I'm sure there's projects in more 'low level' environments where a single good functional abstraction could wipe out 200 lines of code in a flash, and the 3rd party libraries available are actual huge upgrades. If that's all you've ever worked on and you hear an (actual good) front end dev completely shithouse 3rd party libraries you'll likewise be as terrified of their advice as I am when I hear people talk about something being underabstracted.
When in a debugger, I can throw a breakpoint in, inspect inputs and outputs, if they are what I expect, there is (generally[1]) no reason to dive into those other files.
When reading the code from source outside of a debugger, as a new person to the code base, I agree with you and the exact problem you are describing.
[1] Reasonably written code without a bunch of hidden global state.
I will say, this did not get me promoted at Amazon.
Can a developer who is familiar with the code, effortless make a change in an area where change was expected at the beginning?
Assuming a typical, minor bug that turns out to originate from the usual suspect places in the code (database query, logical error, incomplete implementation of a requirement, off-by-one/calculation error). Is it usually easy to narrow things down, and to spot the error?
That means the lead architect should regularly try implementing minor features or fix minor bugs and/or pair up with colleagues who do, and draw conclusions about the code accordingly.
Another measure I use for code quality is: how afraid am I of breaking something when I make changes? This is where typing and tests shine.
I started out with straightforward and somewhat messy code, but after adjusting that 3 times, I ended up writing something that is more or less a toolset to deal with the problems that data had. The code of the tools is convoluted, but when I have to adjust some things every now and then I just use the tools I built anyways so who cares.
Abstraction in programming should be seen like certain devices, helpers etc. someone would make use of in woodworking. Of course it costs you some time to built them, but it can make your life a lot easier, because ot makes results more consistent and testable.
I've never heard this expressed before and it goes against my experience. My entire goal is to find a simple solution to a messy problem. I can think of a lot of simple solutions to messy problems. If programming required me to come up with messy solutions to messy problems I wouldn't want to do it. Most programmers like that "a-ha" moment of a discovering a simple and elegant solution to a messy problem.
Highly optimized or robust code or code with a lot of edge cases is "messy" in the sense that there's a lot of details separate from the high-level concept that get mixed in and can't always be modularized or abstracted away or made simple. That's fine though if pieces of code like that are localized to the complex problem they solve. Simple is a relative concept that can change substantially. Simple code closer to hardware is messier that simple code at a higher level.
I once interacted with a developer that optimized everything. Looking at the code, it was impossible to know what actually needed to be optimized because everything was optimized or had the appearance of so.
The topic of cognitive load in software development is far from rarely considered and in fact it's been somewhat "popular" for several years depending on what communities and circles you participate it on- and off-line.
I'm surprised not to find any mentions to things like:
- the Team Topologies book by Skelton and Pais, published in 2019 where they cover the topic. Particularly of note here is the fact that Skelton has a Computer Science BSc and a Neuroscience MSc
- the many, many, many articles, posts, discussions and conference sessions on congnitive load from the same authors and connected people in subsequent years (I'd say 2021 was a particularly rich year for the topic)
- Dan North sessions, articles and posts from around 2013/2014 in which he talks about code that fits in your head but no more, referencing James Lewis original... insight. E.g. his GOTO 2014 session "Kicking the Complexity Habit" https://www.youtube.com/watch?v=XqgwHXsQA1g&t=510s a quick search returns references to it even in articles from 2020 https://martinfowler.com/articles/class-too-large.html
- Rich Hickey's famous 2011 Simple Made Easy talk https://www.infoq.com/presentations/Simple-Made-Easy/
I totally understand your point and appreciate you linking those resources, however I think it's important to remember that the author's post is from a personal blog, not from a scientific journal or arxiv.
Perhaps OP would've never posted this if he felt that his "contribution" wasn't novel enough. Additionally, there's a chance that the wording and tone the author used might speak to people who found the articles you mentioned opaque(and vice versa, obviously).
If the author, feeling the urge to write something up, had looked very hard for "prior work" instead of following the flow of their insights gained through experience, perhaps they would've felt compelled to use the same vocabulary as the source, which has its pros(forwarding instead of reinventing knowledge) and cons(propagating opaque terms, self censoring because of a feeling of incompetence in the face of the almighty researchers).
That's one of the great things about blog posts: to be able to write freely without being blamed for incompleteness or prior art omission.
On a different note, I think this may also highlight the fact that the prior work you mentioned isn't easy enough to find. Perhaps knowledge isn't circulating well enough outside of particular circles.
I am sure the topic has been considered in an academic setting. I would not feel qualified to provide a good reading list on the topic.
This is also related to code quality, thus it will have a ton of relevant work.
Thank you for the links, in particular to Skelton, Pais, I will have a look!
“So how do we make things easy? … There's a location aspect. Making something at hand, putting it in our toolkit, that's relatively simple. … Then there's the aspect of how do I make it familiar, right? I may not have ever seen this before. That's a learning exercise. I've got to go get a book, go take a tutorial, have somebody explain it to me. …
Then we have this other part though, which is the mental capability part. And that's the part that's always hard to talk about, the mental capability part because, the fact is, we can learn more things. [But] we actually can't get much smarter. We're not going to move; we're not going to move our brain closer to the complexity. We have to make things near by simplifying them.
But the truth here is not that there are these super, bright people who can do these amazing things and everybody else is stuck, because the juggling analogy is pretty close. Right? The average juggler can do three balls. The most amazing juggler in the world can do, like, 9 balls or 12 or something like that. They can't do 20 or 100. We're all very limited. Compared to the complexity we can create, we're all statistically at the same point in our ability to understand it, which is not very good. So we're going to have to bring things towards us.
And because we can only juggle so many balls, you have to make a decision. How many of those balls do you want to be incidental complexity and how many do you want to be problem complexity?”
https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
For example in Hamilton -- we force people to write "declarative functions" which then are stitched together to create a dataflow.
E.g. example function -- my guess is that you can read and understand/guess what it does very easily. # in client_features.py
@tag(owner='Data-Science', pii='False')
@check_output(data_type=np.float64, range=(-5.0, 5.0), allow_nans=False)
def height_zero_mean_unit_variance(height_zero_mean: pd.Series,
height_std_dev: pd.Series) -> pd.Series:
"""Zero mean unit variance value of height"""
return height_zero_mean / height_std_dev
To use `height_zero_mean_unit_variance` somewhere else, you'd then find it as a parameter to some other function, and that's the basic gist of how you'd develop a dataflow. To then execute the dataflow, you'd then in a separate file, decoupled from your transform logic, write some code to (1) create the DAG, and then (2) request what you want executed from it.In terms of reducing cognitive burden for maintainers, by forcing things into a function there's all these nice properties to be had for maintenance:
- unit testing is always possible
- integration testing is super easy, since you can add code, and then _only_ test that path without having to run your entire transform workflow.
- documentation has a natural place & and we can build a DAG visualizing the dataflow that you're looking at
- debugging is methodical since it's straightforward to map an output to a function, check the logic there, and then methodically traverse its dependencies because they are declared.
- you can wrap functions/inject things at run time easily
I am a PhD cognitive psychologist and a coder with 50+ year’s, and I’ve worked directly on the topic you’re writing about. There’s a LOT to say about your interesting piece. Way too much actually. We may have to take it offline. But I want to nod to this:
> In a parallel dimension ... computer hardware was based on SKI calculus. ... software has very few bugs, however, this universe has fewer programs, even fewer programmers, and the error messages suck.
This is perhaps the funniest hacker joke I’ve ever heard! (Although, the Lisp error messages are actually pretty great. But I get the joke regardless.)
[0] https://en.wikipedia.org/wiki/GOMS [1] https://www.researchgate.net/profile/David-Kieras
This applies to blueprints as equally as it does to implementations. The author notes the caveat in soundness of blueprints - not something a common developer can do much about - but if the developer designs blueprints with unintended behaviours - for example the author’s recursive let example - then they’re being very generous with the helpfulness of blueprints since they are no silver bullet to avoid shooting yourself in the foot.
The common case in software development is not a PLT enthusiast. Blueprints are just another way you can shoot yourself in the foot if you hold the tool wrongly. And the common case is not to understand the tool much more than at a fairly superficial level, so mishandling is all but assured.
This means pragmatically, there’s no substitute for an acceptable level of testing in a project.
I whole heartedly agree that we have to minimize extra steps when reviewing code, but you can only push so much of that burden back through space-time to the green field programmer.
For instance, in Pascal, you have to declare everything first. I'm used to that, so it matches my expectations. However, it also sucks because if you're looking in the middle of code the declarations aren't proximal to the first time they're used. There's an expectation that all the code will be read, which was fine in the academic world which gave birth to Pascal, but not in the world of million line systems.
There are tradeoffs that will take decades to get right, simply because we need to have time to gain enough perspective to adjust things collectively, as a profession.
[Edit -- Tweak link format per suggestions below]
[1] - https://en.wikipedia.org/wiki/Impedance_matching
[2] - https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_...
Option #1 -- formatted as a code block
[1] - https://en.wikipedia.org/wiki/Impedance_matching
[2] - https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_theorems
[3] - https://en.wikipedia.org/wiki/Off-by-one_error
Option 2 -- jumble of links[1] - https://en.wikipedia.org/wiki/Impedance_matching [2] - https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_... [3] - https://en.wikipedia.org/wiki/Off-by-one_error
-
Option 3 -- Extra line breaks everywhere
[1] - https://en.wikipedia.org/wiki/Impedance_matching
[2] - https://en.wikipedia.org/wiki/G%C3%B6del%27s_incompleteness_...
[3] - https://en.wikipedia.org/wiki/Off-by-one_error
-
None of those really seems right
[Edit] Sorry, I didn't know about the issues on mobile. it definitely wasn't trying to make it worse on purpose. I added the off by one link to make the examples long enough
The 2 biggest problems in computing... Naming things, Cache Invalidation, and off by 1 errors.
EDIT: Most of the parent comment wasn't there when I wrote my initial reply.
Of the three options, the third is the best of a bad situation since we don't get to cleanly inline links like on Reddit and other places. The first is just inconsiderate, especially to mobile users. The second is, as you noted, a mess especially when there are multiple links. The third lets you keep your original intent (clearly listing each link) while letting them still function as links. Which is better than the first option because it's not disrespectful of other people even if it does add some vertical whitespace.
That is the first problem. We should be asking several questions, and the value of their answers would be weighted against business goals.
How long will it take?
How will it affect future development (pace).
How will it affect the forced total rewrite date (a broader view of the previous question).
And my favorite, how difficult will it affect provability of system behavior? This point is greatly affected by frameworks which impose their own conventions (which change over time).
Basically everyone agrees with you but when it comes time to name stuff all humans suddenly forget everything and think brevity is some sort of required part of naming in code.
Anything using the ternary operation concept
Anything using recursion
Anything which has a self or implicit this in it, which is contextually undefined in the point of view, and has to be grokked as "the thing it should be, whatever it is"
Anything which says "you aren't meant to understand this"
The cognitive burden of being an (english?) speaker, with somebody who speaks in complex multi-clause sentences, with parenthetical statements but you don't know until the very last sentence WHAAT THE HELL IT IS they are on about. That guy. Thats generic functions in a function stack calling the one which says which of a million things this stack of calls is being applied to.
I have to re-read how to do lambdas more often than I care to try and remember. Every time I do, I ask myself why I'm doing it.
If you are one of the cogniscenti who don't suffer this cognitive burden please treat the rest of us kindly.
"oh, you mean you can't read music: how funny I can hear the entire orchestra"
Sometimes however, there will be a recursion, which dumbfounds me as well :) I still feel I improve though. Just the other day I came up with a pair of mutual recursive functions, as if it was nothing, to solve a specific problem, by splitting it into 2 subproblems (or maybe 2 states in an automaton).
Often there is a very clear idea behind a recursion, that, once one has heard it, becomes completely obvious. Maybe not always.
About ZIP: Imagine you wanted to work on 2-tuples made out of pairs of elements from 2 lists. That's what ZIP does. Maybe also works for more than 2 lists, I don't know.
But the moment I start seeing mutation, or for loops complex than a for each over a data structure (like nested ones, or while, or weird indexing) my brain completely shuts down. In my first professional job (F# developer) I had to look up how to write classes every time we created one (not often, to be fair).
I think it's all a matter of habit and familiarity, although I'll admit that finding places that program functionally (where I'm most proficient) is really hard
My coworker told me that my post is being discussed in Hacker News, I did not know! It will take me some time to digest what was said here. It looks like a lot of good discussion. Thank you!
DION was mentioned recently in a few threads, their demo[1] is quite impressive.
[1] https://media.handmade-seattle.com/dion-systems/ (watch the 1st video!)
I'm saddened to see YAML get a bad rap for effectively being shoehorned to fit a task that it's just bad at.
In my experience, YAML is a great format for legibly declaring a dict/array structure, with added benefits over JSON like anchors/references (effectively pointers, which thus allow it to describe a graph), comments, and overloads (great for defaults!).
But YAML has no facilities for conditionals, loops, or any logic. So these get tacked-on, ad-hoc, by systems that need them, and that pulls YAML towards Greenspun's Tenth Rule ("Any sufficiently complicated [system] contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp")
I'm glad better languages like Dhall exist for the problem-space that YAML is just not designed for.
My latest project is gaffing all that off and using .xlsx to hold configuration, openpyxl to load the .xlsx, and then transforming the content into a target format (terraform) in the python.
Let each piece in the system do what it's good at.
what the?! I would imagine that .xlsx is even worse than a plain text file for holding configuration. You have to open it with a spreadsheet editor, and how would you diff a binary format when committing into source repository?
Excel is on the desktop, and ot has diffing tools.
This is a work project, not something for regular GitHub release.
Althought I strongly dislike the idea of introducing Excel in any equation, I wholehearthly agree with that quoted statement of yours. However, if I were you, I would use csv format. It can be easily read/edit from Excel if that's a strict requirement, and in that way, you can keep a grepable plain text versioned in the repository (the csv itself). The best of two worlds I guess. Also, from the top of my head, the Python standard library has a module for reading CSV, so less dependencies too.
I was thinking that perhaps walking the readers of our code through our architectural decisions(and not just through what our code does) is a good way to lessen their cognitive load. This helps identify decisions that have been taken to look smart or because the foie-gras the author ate on that day went down really well with the Chardonnay and made them feel extra stylish.
This also helps us understand how well we know the tools we're using versus how much we do simply through pattern repetition.
It’s like how the increased economic productivity due to technology has not made the work week shorter.
I think my project Glicol (https://github.com/chaosprint/glicol) can also be an example. I try to use text-based style to represent synth connections and many musicians found it quite intuitive.
I have been very addicted to OOP and I tried to design another language with FP paradigm, but finally I landed at this graph-oriented live coding language as I feel the declarative style has less cognition load naturally but I want to avoid the "problematic" aspect of FP.
If the ideas are ideal, the program will be as small and fast as it can be, but if they are not then there will be an inefficiency.
That is to say, if someone shows you code that is smaller and faster, then it has more-ideal ideas.
Now few programs are built with ideal ideas[1] and people with "more capability" can use more simpler, less-ideal ideas to tell the computer what to do, but people with "low capability" must only use ideal ideas (or nearly-ideal).
But how likely it is someone with a low cognitive load capability could have the ideal ideas? If you think of programming as a purely incremental thing, then this must seem impossible, but if you believe in evolution then you can probably understand sometimes "weird things happen" and if the weirdness is successful, then you'll get more weird things.
It is for this reason I think the better programmer (more ideal ideas) probably has a lower cognitive load capability, but so does the worse programmer, so the important thing to focus on is a definition of "success", which on the long-scale will be different for the programmer than (say) the company they work for.
Put another way: If you want a job, then making sure you can program like others is important, but if you want to retire young, then making sure your program makes you money and runs forever is probably more important. What would your employer prefer?
[1]: If you aren't following what I mean by ideas here, Iverson calls them "tools of thought" and I can highly recommend reading: https://www.jsoftware.com/papers/tot.htm
There's a multitude of reasons why people can't handle cognitive load that all have different dynamics. The combination of traits I've seen lead to consistently less complex code is developers that are highly intelligent and hence more capable of building abstractions and who have a very low tolerance for frustration. The cognitive load of the spaghetti solutions they deal with grates on them a lot more than the average bear and since they are capable of seeing how to achieve the same task with less cognitive load they do it, so that they no longer have to deal with the frustration and can be comfortable.
grug write complex code to look smart => grug have to think hard every day forever
I always try to work at reducing cognitive load and one of the principles I employ is reducing the individual number of chunks in the system as a whole. There are a myriad of ways to do this. One of the most pernicious things code can do is to make us ask questions of it because as soon as a piece of code leaves you wondering it has just loaded a chunk into your working memory hence removed some of the available capacity. What's worse is that chunk isn't valuable on its own. Instead it's a placeholder chunk that takes you from wondering to wandering as you deload all the chunks you had in working memory and start a process of navigating around the codebase to build up enough connections between the pieces to where you can collapse your new understanding down to a single chunk of insight. You then reload all the chunks you originally had and now that your placeholder chunk has been replaced by your newly acquired chunk of insight you can finally reason about the code you were trying to.
This process sometimes takes seconds, sometimes minutes, sometimes hours, sometimes days, sometimes weeks, sometimes months. It's absurdly wasteful. Don't make me think! Opportunities to remove cognitive load are everywhere. You have a method that takes a parameter, what's the first thing a developer wonders about that parameter? Can it be null? If it can be null, how do these other lines of code behave with respect to that? If your programming language has an affordance that allows you to declare "the value of the parameter can't be null at this point" all of those questions drop away. System invariants are the same. You often see developers struggling with code that is trying to work out whether an invariant holds and what to do about it if it is violated at some point during the processing of something. The chunks of working memory used by those "but how do we handle X if Y is true at this point?" are cognitive load that you can completely nuke if you check the invariants hold right at the beginning and only do subsequent processing in the case that the invariants aren't violated. The conversation becomes "no need to worry about handling X here because if this code is running it means Y is definitely false".
There's all kinds of things like different versions of the same framework, or competing frameworks with the same affordances. All of that stuff adds to cognitive load. A developer asks another developer "why do we use both xUnit and NUnit in our tests?". A half an hour conversation between 4 developers ensues. It's amazing the difference you can make by simply subtracting differences!
Cognitive debt isn't the right metaphor, though the existing definition of it recounted in the article certainly reminds of the plight of an IC in the trenches. My term for this is "technical tax". A permanent % reduction in available capacity stemming from the cognitive load of the technical solutions. I'm always working to move the team and the org into a "lower technical tax bracket".
Edit: also I don't necessarily agree that intrinsic cognitive load cannot be reduced. It's only true on a technicality. Yes, that cognitive load is indivisible from that domain and there is nothing you can do to change that. But that's not the point. It isn't the domain that experiences cognitive load, it's the developer working in the domain. What we have to manage is the cognitive load the developer is experiencing and making sure it's at an appropriate level. What you really need to ask of yourself and your developers is "what is the total amount of intrinsic cognitive load across all the domains and pieces of software you are responsible for and is that appropriate or too much for you?". An absolutely classic problem where I work is that teams own far too many things. There's just no way to be across all of it effectively.
But this is a perfectly soluble problem and it is entirely and exclusively Alan's fault that everybody stampeded in the wrong direction to our current situation.