The Apprentice
Hat der alte Hexenmeister | With the weathered sorceror |
sich doch einmal wegbegeben | gone to follow his errands |
und nun sollen seine Geister | spirits he controlled before |
auch nach meinem Willen streben | shall now yield to my demands |
– “The Sorceror’s Apprentice” by J. W. v. Goethe, translation by Yours truly
One line of thinking I touched on my blog a while ago was the comparison of programming a computer with wielding magic. Today I want to explore what makes this simile very apt. In the eponymous poem the sorcerer’s apprentice tries to bend his master’s spirits to do his benign bidding, only to find that the unleashed spirits will follow through whether he wants to or not.
Computers are pretty similar to those spirits: To the uninitiated, they may look arcane, even scary (I remember my grandmother always telling me to keep safe because the computer might explode). To those in the know they just follow orders, damn the consequences. This makes them (mostly) simple to understand (although there is much going on below the abstractions on the surface, but that’s a topic for another few libraries of books). Yet the fallibility of the human mind makes them less than easy to command.
Like with magic, wielding source falls in several categories, from the “normal” programmer to the dark-arts-wielding blackhat. And often those factions are in conflict. Whoever has enough knowledge to subvert a system may command it to do their bidding. This kind of black magic affords immense power, at the cost of being in a legal gray area.
In the poem, the apprentice splits the broomstick with an axe, only to find that the severed parts still follow his orders that he unfortunately fails to withdraw. We now see something similar with current CPUs that allow us to run many tasks in parallel; yet we find that keeping track of all possible interactions overburdens our mental capacity.
A lot of programming (as presumably with magic) is keeping the effects in check. Experienced sourcerors have adopted a number of strategies to deal with the complexity, a few I will mention here:
- Keep it simple: The less moving parts, the less can go wrong
- Structure your code in a way that allows for the least possible amount of errors
- Test each part in isolation, then the whole in integration
- Ask the computer to validate your assumptions (this is usually done
with
assert
statements in your code, but may also use the type system or other static checks) - Always code defensively, i.e. checking every input at all levels
- Use a language that will help you keep complexity at bay, and especially the unsafety (e.g. Rust, but also Java, C#, etc.)
- Think before you code. Many propose either a top-down or bottom-up path to design. I personally like the “meet-in-the-middle” approach best
- Coding is a team sport. While a determined individual can often out-code a team on smaller projects, the amount of complexity the human brain can handle puts a hard limit on project size. Also one set of eyes may overlook things that another may see easily
Note that some of those strategies are for bug avoidance while others mitigate the risk of those bugs. Given the fact that some people have the capacity and motivation to use those bugs to their advantage (and your and your users detriment), it is highly advisable to adopt at least some of them.