When we see a world-class musician flawlessly play challenging music, it can be tempting to imagine that they were always able to do so. A moment’s thought makes it obvious that they must have had to spend huge amounts of time practising basic techniques in order to reach that level. What’s less obvious is that they have to continue spending considerable amounts of time simply to maintain that technique, let alone expand it.
In contrast, in programming, we have only a haphazard notion of how one should go about obtaining sufficient technique to become good enough to write good software; and we have almost no notion of continued practise to maintain or expand that technique. Often overtaken by life events – notably, though not only, the requirement to earn money to support a family – many programmers implicitly finish the bulk of their learning early on, and some stop learning entirely.
As someone of rather limited talent, I have long been aware that if I’m not proactive about learning, I will fall even further behind where I would like to be. While some people enjoy practising programming with small puzzles (the programming equivalent of musical scales perhaps?), I am too grumpy to find them enlightening or enjoyable. Instead, I’ve found that real-world tasks that require me to do something new are where I most improve, and most enjoy improving, my programming skills. Fortunately, and at first accidentally, I’ve found that it’s relatively easy for me to continue practising programming on real-world tasks as part of my day-to-day life.
One habit I have is to try to automate most tasks that look automatable. Especially when I was younger, people would laugh at this tendency: “you’ve spent 4 hours to automate what you could have done in 2.” They were nearly always right in the short term, but what’s astonishing is how often, with only minor variations, the same tasks crop up repeatedly [1]. If that same 2 hour task crops up twice, I have broken even, and if it comes up three times, I’ve come out ahead.
When I started trying to automate things, I was pretty bad at doing so: I was notably less competent than my peers. But just as a musician gains compound interest from practising multiple hours a day, so did I. Each task I automated not only improved my basic programming technique, but also opened my eyes to new techniques and tools that might help me in future tasks. For example, a lot of the early things I automated boiled down to text processing: parsing input, transforming it, and outputting it in a different form. I didn’t know that there was a theory of parsing (let alone parsing tools!) but, gradually, I built up an understanding of reasonable ways to go about this kind of task. Something similar happened for other areas such as GUI and systems programming. Broadly speaking, in my teens and early 20s I was often running a net loss in terms of time; I’d broken even by my mid to late 20s; and by the time I was 30 it was very clear to me that my investments had paid off. Those more talented than I would probably have reached this point well before that time, but better late than never!
These days, mostly because of the time that other responsibilities require, I am more careful about which tasks I’m willing to invest time in automating. Experience has given me a better sense of what tasks are plausibly automatable and what tasks I might learn something more general from. I also have a wider palate of tools to call upon: for example, I now use clever tricks in my text editor [2] to perform some text processing tasks that I once would have written a program for. However, if a task looks automatable, seems like it might allow me to learn something more general, and isn’t covered by other tools, then I’m now more willing than I used to be to invest considerable time in it.
The first time I did that was for extsmaild. I was spending a lot of time on trains and planes and reliably sending email was a problem [3]. I then realised that this might be an opportunity to understand how to write a “proper” Unix daemon in C. It soon became clear to me that the major challenge in a daemon is how to keep going in the face of undesirable situations (including, but not only, outright errors): these days, extsmaild can recover from nearly anything except a lack of memory (which, given its frugality with resources, is an unlikely scenario). I had no idea how many different undesirable situations such a program could encounter [4] and consequently I have learnt a considerable amount from writing and maintaining extsmaild.
When, more recently, I needed a way of executing commands based on actions
on GitHub PRs, I felt I wouldn’t learn much by writing another C daemon.
Instead, I wrote snare in Rust,
because I wanted to understand if Rust daemons could be as robust as their C
counterparts. Broadly speaking my conclusion was “yes”. I also wanted to learn
about async/await, a technique I’d never previously used. snare incorporates
both an HTTP server (to listen to GitHub events) and a job runner (which waits
upon child processes, collecting their stdout/stderr output). I started
with the HTTP server, but I found async/await to be a frustrating
experience: even in a very small chunk of code, its tentacles spread more widely than I expected, and I found the
resulting control flow hard to reason about. When I got to the job runner I
reverted back to traditional Unix poll
ing. poll
is a horrible interface [5]
that’s difficult to use correctly, but for me it
has the virtue that it isolates the horrors to one place.
That wasn’t the result from practising that I expected, but it’s
useful nonetheless.
When I wanted to add a newsletter subscription to my website, I was briefly tempted to use an external service, before realising that I didn’t want to worry about the resulting privacy issues. Based on my snare experience, I felt that writing a Rust daemon to handle the backend tasks of a newsletter system would be relatively easy, but I had never written any JavaScript in a webpage: small though it was, the JavaScript that makes the subscription system more user-friendly taught me quite a bit. When I came to add a comments system to my blog, I was able to be slightly more ambitious in what I expected of JavaScript.
As that last example might suggest, some of my programming practise is smaller scale, though still nearly always on real-world code. For example, sometimes I will try a new, or different, library to see if it helps me solve a familiar task quicker or better. Sometimes I might look at a function I’ve written and try to rewrite it to make it easier to understand, perhaps using a language feature I rarely use. The time I’m investing is small, and the result of such practise is much more likely to be a “failure” than in the bigger cases, but there are successes and, small though they are, they accumulate over time. The most noticeable change, at least to me, is that I’ve gradually become better at writing code that is concise while, I hope, still being relatively easy to understand.
For me, at least, practising programming is something I do frequently. Fortunately I’m able to incorporate it into my day-to-day life, and fortunately I enjoy the process! Do you deliberately practise your programming skills? If so, how do you go about it? Feel free to comment below!
Acknowledgements: thanks to Edd Barrett, Lukas Diekmann, and Dan Luu for comments.
Footnotes
Perhaps because of my job, a lot of tasks happen once a year. One consequence of that is that I sometimes have to do a bit of searching to see where I left the program(s) that automated the task in the previous year!
Perhaps because of my job, a lot of tasks happen once a year. One consequence of that is that I sometimes have to do a bit of searching to see where I left the program(s) that automated the task in the previous year!
I have made far more use out of macros and rectangular blocks (sometimes called “multi cursors”, though that term also is used differently elsewhere) than I would ever have thought possible.
I have made far more use out of macros and rectangular blocks (sometimes called “multi cursors”, though that term also is used differently elsewhere) than I would ever have thought possible.
There have been many advantages to using mutt, and now neomutt, for over 20 years — but also some disadvantages.
There have been many advantages to using mutt, and now neomutt, for over 20 years — but also some disadvantages.
If anything, I undersold the lessons I learnt from this experience when I first wrote about them. For example, I had yet to encounter the concept of zombie child processes — a particular eye opener!
If anything, I undersold the lessons I learnt from this experience when I first wrote about them. For example, I had yet to encounter the concept of zombie child processes — a particular eye opener!
Richard Kettlewell’s collection of platform quirks around the question “when does a poll
ed file descriptor reach EOF?” is required reading.
Richard Kettlewell’s collection of platform quirks around the question “when does a poll
ed file descriptor reach EOF?” is required reading.