I enjoy learning new programming languages, because that tends to unlock new ways of thinking about solutions to problems.
I have realised, however, that I can only maintain fluency in about three programming languages at a time. If I try to become fluent in more than that, I tend to perform worse in all of them. 11 Note that I’m talking about fluency, i.e. performing reasonably well in a whiteboard coding test without having to look up common standard library methods. I have basic communicative competence in many more programming languages, but that is a less interesting measure, in my opinion.
In the spirit of making my choices explicit to myself, in order to better learn from my own mistakes, I’d be interested in seeing how my preferences in programming languages change over time, and in particular, for what reasons.
Spanning the PL Space
But first, how will I even pick only three programming languages to maintain fluency in? I have chosen to try to span as large a portion of the programming language space as possible. I’ll try to explain by example:
Picking something like Java, C#, and Go would by that measure be beyond useless, because those three languages are part of the same category almost regardless of how you look at it. More useful woud be to pick something like Java, Objective-C, and EcmaScript, because the three target four different platforms (PCs, Android, iOS, web browsers) among them, and there’s at least one static and one dynamic language in there.
Even better, then, might be to switch out Objective-C for Swift, because then they’re also spanning two different paradigms (object-oriented and functional)! By performing these Pareto optimal swaps22 I.e. improving in one area without losing something in another., we can get a list of three languages that can be applied in as wide a variety of situations as possible.
In the end, these were the three different categories of languages I wanted represented in my repertoire of fluency:
- Low-level code
I want to be able to drop down and do low-level programming when it is necessary for performance reasons, memory constraint reasons, or otherwise.
Current choice: Ada
- Maintainable code
I want to be able to write programs that are maintainable and obviously correct33 In the tongue-in-cheek way meant when writing code that has “obviously no deficiencies” rather than “no obvious deficiencies”., even if at the cost of some performance.
Current choice: F#
- Dynamic code
When fast prototyping is required, it’s hard to do it better than with a high-level dynamic language, where you can pull all sorts of dirty tricks to get what you want, even if performance drops through the floor.
Current choice: Perl (and R)
The reasoning for each choice and its evolution is detailed in the following chronological listing.
: Ada (low-level code)
Ada for low-level programming was the easiest choice for me. It is an incredibly well-designed language that seems to get even the tiny things right. This is in fact one of the reasons I found it hard to learn: popular languages tend to squish different concerns together into a single concept44 As an example, it is common to implement the various mechanisms of object-oriented programming by simply going “Everything is done with classes!” which is easy to remember (you just use the single tool you have been provided for everything), but makes it harder to deal with the cases where you wish you had been given multiple tools instead.
Ada gives you all the tools, clearly separated, but easy to combine with each other.
: Haskell (maintainable code)
It took me years to warm up to Haskell, but once I did, I rarely wanted to use anything else. There’s a freedom to knowing that once your code compiles, it probably does the right thing. Or at least, if it crashes in production, it will be because of an interesting error, and not a silly mistake that should have been caught long ago.
Writing Haskell code feels a bit like coming into a project that has been written with a rigorous set of fast automated tests. Almost everything I do wrong gets caught by the compiler right away.
Of course, it also helps that Haskell might be the highest-level language I have used. When writing code in other languages, I sometimes encounter situations where I think to myself, “Dang, I want to do this thing, but the language is not powerful enough to let me, so I have to write a bunch of boilerplate to work around the issue.” That never happens with Haskell. If I want to express something, the language lets me.
: Common Lisp (dynamic code)
I’ll admit: I bought into Beating the Averages early on. What Paul Graham described about the expressitivity of Lisp sounded great and I wanted in.
And to be fair, as much as this statement will upset people, I still think he is mostly correct. A competent Lisp programmer is going to code circles around anyone else in a project with a short timeframe.
My main issue with Lisp is that while it is powerful for expressing run-time computation, its type system is a farce in comparison. And that, in my experience, kills any chance for Lisp code to be maintainable in larger projects with contributors of various skill levels. A powerful type system is an absolute requirement for complex code, as far as I’m concerned. The reason is simple: the type system massively reduces the set of possible states the developer has to consider at each point of execution.55 Granted, I have never tried to maintain a large, complex Lisp application, so I might be doing everything all wrong. I just imagine you’d have to write automated tests that basically amount to type checking all definitions anyway.
: F# (maintainable code)
F# has replaced Haskell as my language of choice for writing maintainable code. This is a pragmatic choice grounded in the fact that I can write F# as part of my current employment, which means that maintaining fluency will be easier than for Haskell, which I would have to practise in my (already limited) spare time.
When I started exploring and trying to understand the .net environment as part of this employment, I was actually surprised by how similar F# and Haskell are. Compared to Haskell, F# de-emphasises theoretical concepts, purity, functional programming, and non-strict evaluation, but still encourages all four, to varying degrees. I think this makes F# hit a sort of middle ground which makes it more acceptable to the industry and softens the transition from traditional C#.
Perl (and R) (dynamic code)
Having mentally debated back and forth between the Lisps and Perl for dynamic code, I decided to just try using Perl for my day-to-day dynamic programming needs for a week.
I have been more productive than any other week the last few months.
See, the majority of my day-to-day dynamic programming needs as a reliability engineer revolve around munging text, cleaning up log files, counting things, creating statistics. This is what Perl really excels at. Perl is a clear winner now that I’ve actually cared enough to try it.
Note, however, that I’ve chosen to go with Perl 5, and not Raku. The reason for this is its ubiquity. I can just open a new file on a remote machine in Emacs and type out a Perl script, and I know it’s ready to run on the remote machine.
Unfortunately, I don’t think I’ll ever be as comfortable testing hypotheses and plotting things in Perl as I am in R, so the two of them will probably have to complement each other and their combination will be my choices for dynamic languages.
Candidates That Came Close
These are some other programming languages that I didn’t pick, but that were also hard to rule out, as well as a brief explanation of my reasoning.
This list also hints that the category that has been hardest to settle on has been the one for doing quick, dynamic, prototyping code. This is not too surprising: it is easier to design languages for quick prototyping than for writing maintainable, long-lived, complex code – naturally, the selection of languages available for the former will be larger.
C# (maintainable code)
For being an object-oriented, Java-like language, C# is well designed and clearly ahead of Java. Since C# is another language I have written as part of a full-time employment, it was close to making the cut. But given that F# gives me everything C# does and more, I doubt there’ll ever be a situation in which I will pick C#.
Python (dynamic code)
I have worked full-time for a couple of years writing Python, so my Python is still fairly strong, and it is a solid candidate for dynamic code. However, Python is a dynamic language that tries hard to play with the static boys and be used for maintainable code, which puts it in an awkward in-between position where it’s (again, in my opinion) not suited for quick prototyping, nor maintainable code.
R (dynamic code)
R has one strength, and it is not to be underestimated: ggplot2. Given how easy it is to visualise data in R (which is 50% of my quick prototyping needs), I have considered becoming fluent in it. Unfortunately, unless I spend time vectorising my algorithms, I’m going to fight with the R ecosystem, and I’m not prepared for either.
Perl (dynamic code)
I mentioned that 50% of my quick prototyping needs is visualising data. The other 50% is text manipulation, and Perl is very good at that. I have been tempted, multiple times, to just dive into Perl 6 and see where I emerge. The reason I haven’t taken the plunge is that I view Perl less as a general-purpose language and more as a text-manipulation language, and I’m not sure if it’s worth becoming fluent in a less general language.
Emacs Lisp (dynamic code)
If Common Lisp is a general-purpose language, and Perl is a text-manipulation language, then what is Emacs Lisp? Perhaps a bit of both? This, coupled with the fact that I have to know Emacs Lisp anyway to hack on Emacs-related code, is a rather convincing argument that I should have Emacs Lisp as the dynamic language of choice instead. I’ll admit I don’t even have a strong counter-argument here. I can totally see myself being very productive in Emacs Lisp, for multiple reasons (at least one of which being that I can provide a simple ui through Emacs.)