The personal software stack
The term “software stack” almost universally means “the technologies that a complete piece of software utilizes to run”. It’s the frontend framework, the web server, the database, the programming language. They are selected based on “macro-scale” criteria - what the company already uses, what’s popular in the industry at the moment, what will fit the project best.
However, there’s also a second stack - the “personal” one. Every programmer has their own, tuned to fit their workflow best - it is the operating system, the editor, the version control system, the window manager, the shell… The software stack is decided in the planning phase of a project; the personal stack grows with the programmmer. As they learn and develop new and existing skills, software developers tune their personal software stacks to utilize them as efficiently as possible.
There is always a clear gap between an idea and its implementation, not only in programming. Chances are, if you were to stroll into a furniture store in a foreign country, where nobody speaks your language, it would be quite a challenge to ask if they have any chairs - even though you might know exactly what a chair looks like, you wouldn’t be able to translate your thoughts into words. The same goes for software development - computers don’t speak English, at least not yet.
It is the goal of every piece of software to minimize this mental gap. In psychology, if the barrier between ideas and their execution is almost non-existent, it is said that you’ve achieved “flow” - it is a state of complete focus on a task, when your efficiency and concentration are at their maximum. In the simplest definition, an ergonomic piece of software is one that doesn’t break the flow.
Ultimately, flow is based on the balance between perceived skill and perceived challenge - as long as both are kept high, our software is “flow-friendly”. How can we then influence that?
The perceived challenge is particularly crucial to understand. Making software more complex for no valid reason does not increase the perceived challenge - it increases the actual challenge in using it, resulting in a relatively less powerful tool; a tool that makes it harder to achieve certain results than other, less complex alternatives; the tool should be kept simple. To increase the perceived challenge, we want the user to feel like they’re utilizing advanced skills and features - and to make skills and features seem advanced, we should make them powerful.
Building tools out of simple but powerful features is only one part of the solution, though. We also have to consider the perceived skill.
More often than not, flow gets broken every time you have to pause your task. If you’re typing out a complex program, or a blog post, or documentation, and need to look up the signature of a function, or use a feature of your editor that you don’t remember the keyboard shortcut for, you have to pause. Minimizing the number of those pauses is achieved through learning and better software ergonomy, and influences the user’s perceived skill - which is directly proportional to the number of features they have internalized.
The learning process and internalization
The learning process has three stages:
The programmer is aware that there is a way to easily search-and-replace globally in Vim, but can’t remember the exact command; they write it down, or google it, and start using it in their workflow.
The programmer understands why the command works the way it does. They know that
:enter command mode, then
%for every line
globally”. They don’t have to google it or consult their printed-out cheat sheet anymore, and consciously use it whenever they need it.
The tool becomes so natural to use that utilizing it is no longer a conscious effort. It becomes pure muscle memory, and requires virtually no thinking on the programmer’s part. It never breaks the flow. The user starts learning new features.
Users very rarely, if ever, learn features one at a time. We are usually learning multiple skills simultaneously, at different rates and stages. Due to that, it’s important for the tool to reuse the fundamental skills for the more advanced techniques, thus both making them more approachable, and hastening the internalization of the basics.
A tool can only be as efficient as its user; someone with years of Haskell experience will write Haskell code better and faster in Nano than a Vim guru who never used the language. However, an user can also only be as efficient as the tools they use - consider an example of someone who always takes a minute to write a line of code. Regardless of what language or editor they use, they cannot write a 600 line program in less total time than 10 hours.
We cannot improve our users, but we can give them better tools with a higher skill ceiling and lower skill floor; that way, they’re useful regardless of how familiar the user is with them.
An example of a tool with a high skill ceiling could be C++ or Vim. They’re nearly limitless in their capabilities, but take a long time to be proficient with. The user can probably write a “Hello, World!” program in C++ or navigate their way around a text file in Vim without much googling, but they very quickly run into pointer arithmetic, the heap and the stack, or motions and the command mode.
Examples of tools with low skill floors are Notepad, or even Twitter - it takes only minimal, if any, effort, to understand and internalize all of their features.
The learning curve
We cannot evaluate only those two properties, though. There’s also the learning curve. “Skill floor” defines how easy it is to start using a tool. “Skill ceiling” defines how powerful the tool is, and how many features it has. The learning curve is what describes how much effort it takes to actually use all of them.
C++ has a logarithmic learning curve. It is easy to start with, but quickly becomes a labirynth of implementation details, pointer arithmetic, macro abuse, and enigmatic compiler and linker options.
Vim has a linear learning curve, albeit starting at a pretty high point - once the user knows the basics of Vim, they can discover and utilize more advanced techniques as they need them, one by one.
Python has an exponential learning curve - it is very simple to start using and be competent with, but the most powerful features - metaclasses, more obscure magic methods, the Python/C API - require a deep understanding of how the language itself works.
Haskell has an almost flat learning curve - it’s daunting to get to know, but becomes natural as soon as the programmer “switches” to the functional mindset.
Notepad has no learning curve - a new Notepad user is no less efficient than someone who has been using it for years.
There is no clear answer to “which curve is the best”, as long as there is one. The best learning curve is the one that matches the learning rate of the user - as long as the user’s proficiency is constant with regard to the tool’s perceived complexity, they are in a state of flow.
The goals and the means
Assuming the user learns at a constant rate, the mythical perfect tool has a zero skill floor, an infinitely high skill ceiling, and a linear learning curve.
Software we strive to write should have a skill floor as low as possible, so that it’s easy to be introduced to. Some ways to lower the skill floor are:
- Good documentation and tutorials - the simplest way to make someone learn the way you want them to is to guide them.
- Keeping basic functionality simple - I shouldn’t have a PhD in type theory to write a “Hello, World!”
- Leveraging common practices and behavior - there’s a reason
- Making advanced features easily available - I shouldn’t have to recompile an editor with specific flags in order to make it have syntax coloring, or memorize a 40-key sequence to rename all occurences of a variable.
Some ways to raise the skill ceiling are:
- Making your tools as hackable as possible, through editor plugin APIs
(or even better, writing the editor itself using the same API and exposing
access to its internals), programming language FFIs, and making the language
itself extensible (
operators, classes, macros). No matter how many features you add in, someone will always need something that isn’t there, so give them the ability to implement it.
- Generalizing the tool itself - basically, the Unix philosophy: “Expect the output of every program to become the input to another, as yet unknown, program.”
- Comprehensive documentation - everything should be documented, even the inner workings.
Some ways to smoothen the learning curve are:
- Using the same techniques for both simple and advanced features - an
example would be Python’s object handling (everything is an object, classes
are created by
type, objects get created by
__call__ing a class, etc) or Vim’s motions.
- Good package managers - so that using a feature someone else wrote is as
Plug '...', and doesn’t burden the user with learning the inner workings if they don’t want to. Additionally, it should be as easy to remove a package as it is to install it.
- Clearly defined best practices - so that the user spends time learning how to use a feature, and not refactoring their source code.
It all seems intuitive and obvious, but it’s fun to actually spend a few hours considering the ergonomy of your software from time to time.
What sparked this post? Why, GNU Make usually screams about no defined rules and refuses to do anything if you don’t tell it how to make a file, but in some cases it actually works according to some obscure predefined behavior.
Which, sometimes, is worse than not working at all.