Advice for beginning coders

By Charlie Loyd, 2004–2006.

These are all the tips I have for smart beginning programmers. I wrote the first version for a friend who quickly turned into an adroit developer, and although I can’t take credit, that should be fair evidence that reading this won’t seriously stunt you. (Also, that’s why the possibly over-familiar second-person imperatives.) Everything is general enough to apply to any good language and programming style. It might be bad advice, though; I’ve only been programming since about 1998 and I’ve put at least one infinite loop and two security holes on live webservers. Keep your wits about you.

In the order in which I could find the least jarring segues:

  1. Code defensively. (That is the whole of the law; the rest is commentary.)
  2. Make the guts modular.
  3. Make the interfaces useful.
  4. Optimize the right things.
  5. Take taste seriously (the heartwarming finale).

Code defensively

Take creative risks as you compose programs. Experiment and dabble. Do things sideways. But keep the code itself sane and reliable in structure and detail.

Write programs to take a little abuse. When he designed the Triga nuclear reactor, Freeman Dyson distinguished between the engineered safety of a system of protections to keep a device working right and the inherent safety of a design unable to break. When you pull all the control rods out of a Triga core – which would start a meltdown in most reactors if they didn’t have failsafes – the fission goes into negative feedback and it shuts itself down. Likewise in code, an inability to do the wrong thing is safer than any safety checks.

If you have a general string function, say, that works on every argument except zoot, do not add a special case. Rewrite the function. Special cases (for things that aren’t actually special, like documented keywords and division by zero) amount to engineered safety, and over time they cause trouble.

Thrash input. When you prompt the user, parse a string, read from the disk, or otherwise use non–hard-coded data, you should feel a pang of doubt and have to reassure yourself that the information is going to stay sealed away from everything that doesn’t rely on it. If you have to encrypt a password, translate {&, <, >} to {&amp;, &lt;, &gt;}, or similar, do it right away. Input that anything hinges on, like command-line arguments, config files, and interactive instructions, should usually parse into a strict internal form if it has to hang around at all.

Don’t let a healthy tendency to persecution mania turn you against the users. Trust them to know their own minds. Give them whatever you can which they are allowed to have. In interactive programs, tell them more than enough to make informed decisions. Don’t open the possibility of reading their e-mail, overwriting their files, et cetera, except by request. By the same token, never let what a user says affect anything other than that user’s property.

Make the guts modular

Good big code needs really good small code. Write chunks that talk to each other. When an algorithm is distinct from its surroundings, or comes up several times, make a function out of it. When something happens in two phases, clean up the first phase before starting the second. Think of cells and membranes. If something goes wrong, the problem space is already partitioned; when it works fine, it’s easier to read and change.

Don’t be afraid to make functions taking many arguments (or very complex arguments, like objects festooned with methods and metadata). It can get out of hand, but it’s good strategy to make state explicit – to have functions depend on their arguments, not on obscure global variables or anything else non-obvious. It also keeps variables from leaking, say, out of a password-encrypting function into the scope of a window-drawing function. (Try to keep a model in your head of what depends on what, in every sense. Function A depends on user input, function B calls function C, the class D needs library E and function A, and so on.)

When you can, write your chunks so they implicitly check one another. If you want explicit sanity checks, put them at the ends of the chunks more than at the beginnings. Then you catch problems early and near the source, not whenever and wherever they happen to first do harm.

Besides distinguishing sibling operations, divide strata of abstraction. In programs more than a few screens long or which do more than one thing, the sets of functions that take input, do the work, and give the output should have their own zones. Be especially sure of this when working closely with outside apps, like databases, because they will change eventually and you really don’t want to have to find and check every contact you have with them every time you upgrade.

This is often called portability, but it’s a more general virtue. In web projects especially, each of several big codebases you run on top of will probably get a major upgrade at least yearly, and that’s a port even if you don’t think of it that way.

Make the interfaces useful

Classical GUI apps often suck because the designers assumed everyone else would know what everything meant and wouldn’t want to see a single pixel left insignificant. Web and text apps are similar. If I can’t use a program without reading all the docs and paying attention all the time, I probably won’t.

Once you’ve fully inflated the phase space in which your code gives correct results, contract the surrounding space in which it doesn’t give an error. If you know it can’t handle some conceivable valid case, even if it isn’t your fault, exit with a useful note. Do not expect anyone – even you in a year – to read the documentation where it says that such-and-such is not handled properly. Do not, by default, use non-obvious heuristic corrections (maybe they typed everything one key to the left…). In a utility that might be used as a filter in a pipe, include-ed, et cetera, do not print errors to stdout instead of stderr (or the equivalents).

Just quit and say why.

Optimize the right things

Keep a paperweight handy where you code. Whenever you think this might be slow; better get close to the metal just in case, take the paperweight in your left hand and slam it on your right knuckles. Bandage your hand, take a walk outside, eat a healthy meal, look at some art, catch up on sleep, and don’t come back to the computer until you can think the right way around.

Code so slow that it never terminated would have nil overall utility, but it does not follow that the fastest possible code would have the highest possible overall utility. Speed may occasionally be your primary concern, but it can never be your only concern. The same things that keep you from designing dedicated hardware for each algorithm – money, time, compatibility, maintainability, flexibility – should moderate the urge to squeeze the last few nanoseconds per microsecond out of code. Keep perspective. Find sound compromises. Stay cool. The NetBSD project, which has a knack for sanity, says:

Microoptimizations can play a part in any system, but design is even more important. Rewriting a routine to speed it up by 80% may sound impressive, but that routine may have only been using 5% of the CPU time. Looking at the larger picture and saving 10% overall by redesigning the way an operation is carried out gives over double the benefit. There is room for both in NetBSD, but we prefer getting a design right to tweaking a poor implementation.

If you’re really worried about speed, profile your code and find out what’s taking the time. If you’re already sick of hearing this, too bad. Modern hardware and software cache, pipeline, buffer, mode-switch, initialize, and run maintenance in strange places. Computer science isn’t really a science any more than it’s a branch of math or storytelling, but the name is a reminder of its descriptive, deductive, statistical side. It’s not hard to find emergent, or at least consistently weird-seeming, behavior.

If something just isn’t fast enough, switch to a less abstract language. Vanilla C or Common Lisp or Forth is better than a higher-level language viciously hacked for speed.

Take taste seriously (the heartwarming finale)

Find and understand good code. Understand what makes it good. The ideal program is perfectly adapted but perfectly general, runs in an instant, wipes away entropy (leaving no unsightly streaks or stains), and works so well that what you thought were bugs are expressions of some higher truth. There’s good discussion – the C2 wiki, random folk wisdom, Taste for Makers, Rob Pike’s notes, $(man perlstyle) – but really you have to get your hands dirty.

You have a whole landscape and ecosystem to explore. Forget epigrams about design and make stuff the real way, by agglutination and evolution and occasional conflagration.