We usually think of a computer system ending at the screen and keyboard, but the user is part of the system. If the computer and user don’t get on the system isn’t working — it’s a frustrating, surprising, and even infuriating experience.
The principle of least astonishment is a design principle the emphasizes the importance of predictability. Software should behave in ways a user has come to expect.
“If a necessary feature has a high astonishment factor, it may be necessary to redesign the feature.”
Pick up an entry-level psychology textbook and you’ll learn about the idea of Learned Helplessness. In short — a bunch of psychologists spent their valuable time zapping maze-imprisoned rats with high voltage tasers. There was no pattern to their zapping, it was done at random. The rats slowly spiralled into a malaise of confusion, frustration and depression due to their inability to control the situation.
Control is the key factor here. The rats couldn’t start or stop doing something to change the outcome — they had zero control — so they just gave up on their pretty miserable lives.
A lot of software aligns with this sentiment — although (in most cases) I’m sure it’s unintentional. Users wander around some maze of a programmer’s design, trying to figure out the best way to get their job done. Each time a button doesn’t do what they expect, a search doesn’t return a sensible set of results, or half a day’s work is lost because they forgot to click the save button, a tiny notch of frustration is logged. Over days and weeks, these fragments of frustration build up until, one thankless day, the laptop gets thrown out of the 12th-floor window in an utter rampage!
Software should be predictable. The user has to feel in control, and unpredictability takes that away. Stick to the usual conventions, and make software as intuitive as possible.
Software should be transparent. It should be possible to build a simple mental model of what the application is doing. If it has murky corners it’ll sneak up on you by surprise.
Take the following (admittedly terrible) function to print everything in a list.
def print_ls(ls): while ls: print(ls.pop(0)) ls = [1,2,3] print_ls(ls) # uh oh! ls is empty
At a cursory glance, you might think it’s just a harmless function to print the content of your list, but there’s an insidious side-effect lying in wait. By using pop to get the elements from the list the function is stealing all your data! When you go to use it after the print, there’s nothing left.
Functional programming attempts to combat side effects through immutability. You can’t just change the content of a list. If you want to mutate, you (in general) have to copy the data first. The original copy is left untouched.
Rust takes a different approach called ownership. To allow a function to change inputs, you have to move the data into that function, transferring ownership of it. Only once owned can a function mutate data.
But in general, most languages allow unfettered mutation of data. Be a good coder by adhering to the principle of least astonishment. Make it clear, through naming and documentation, when your code mutates an argument. Even better, tread carefully and don’t do it — if you need to change data return a copy instead.
- Principle of Least Astonishment — Wikipedia
- Principle of Least Surprise
- UX Planet
- Controlling Your Environment Makes You Happy — Joel on Software
This post was written as part of a series on laws of software development for #PragProWriMo 2021 run by the The Pragmatic Programmers.