How Big Should a Programming Language Be?

Recent posts
Structured Editing and Incremental Parsing
How I Prepare to Make a Video on Programming
pizauth: HTTPS redirects
Recording and Processing Spoken Word
Why the Circular Specification Problem and the Observer Effect Are Distinct
What Factors Explain the Nature of Software?
Some Reflections on Writing Unix Daemons
Faster Shell Startup With Shell Switching
Choosing What To Read
Debugging A Failing Hotkey

Blog archive

Reading the thought-provoking “Patterns & Abstractions” post reminded me of a long-held opinion I have about programming language design: we have a tendency to keep adding features to a language until it becomes so big [1] that its sheer size makes it difficult to use reliably. Since most of us spend most of our time programming in one language, it can be difficult to see a common trend amongst languages in general.

Language size over time

Let me start with a concrete example. I started using Python around version 1.3, though the first version I really remember was 1.4 [2]. Python 1.5 was a real improvement, adding the assert statement, nested packages (“hierarchical modules” in the release notes), and the re regular expression module — things that I suspect nearly every modern Python programmer finds useful. At that point, Python was still a relatively small language, to the extent that you could reasonably expect to store nearly every detail about it (and its implementation!) in your head.

Python 1.6 added support for unicode — support that was clearly useful, but which turned out to be sufficiently awkward that (somewhat) fixing that awkwardness turned out to be a major cause underlying the schism between Python 2 and 3. As Python 1.6 turned to 2.0 and beyond, this formerly small language kept adding features, many of which I find useful (e.g. list comprehensions), but each of which has inevitably made the language bigger.

In retrospect, Python 1.5 was probably the last version of Python that someone with my limited brainpower could reasonably expect to fully understand. By the time of Python 2.7, Python was definitely a big language, and it has only grown in size since then. I doubt there are many people who could claim they understood every detail of Python 2.7, and there is probably no one who claims that of recent versions of Python 3.

The criteria for language features

How did Python become a big language? The initial version of Python was small, and the first several versions deliberately tried to maintain that smallness: most feature suggestions were explicitly rejected on the grounds that they would make the language too big. At some point (perhaps between Python 1.6 and Python 2.0?) it seemed to me that something changed: features were not rejected solely on the grounds of making the language too big, but on the grounds that they didn’t fix an important enough problem.

The difference between these two criteria might seem minor, but “reject a design if it doesn’t solve an important problem” implicitly downgrades the size of the language as a concern in and of itself. As soon as a language’s designers stop worrying about the size of the language, it seems inevitable to me that the language will grow almost without bound. Certainly, Python looks set to keep on adding new features.

Although I’ve used Python as an example, I could have chosen many other languages. For example, although Java was never what I’d call a small language, it had many years of fairly minor language changes before adding generics to Java 1.5 — that seemed to be the point at which Java began its journey to the large language it now is. Haskell started off as a fairly small ML-inspired language, but is now a very large language (albeit one can opt-in or out from many features in the standard compiler). JavaScript has gone from a long-weekend design to a rather large language. I know a lot less about languages like C# and the like, but I wouldn’t be surprised if they’ve gone, or are going, through something similar.

These days I do most of my programming at Rust, a language which is already big, but which is perhaps now being forced to think about whether it wants to become very big or not. For example, if preventing Rust getting too big is a goal, it’s hard to imagine the recently proposed keyword generics initiative being accepted, even though it clearly solves a problem. On the other hand, Rust has already added dedicated support for async/await, so is it a good idea to cleave the library system in two? Whatever the outcome is, some people will be unhappy — these decisions aren’t easy!

Why does this happen? Well, no programming language is perfect: there are always use cases it doesn’t support well. In many cases, growing the language in size helps it better support those use cases better. Since there are an unbounded number of potential use cases, there is thus always “more” language design that we can do, each time making the language a little bigger. As Patterns & Abstractions points out, one continual temptation is to move “patterns” (i.e. standard idioms of use) to “abstractions” (i.e. language features). There are advantages to such moves: languages can, for example, give better error messages and better optimise “abstractions”. But there is also a cost: patterns do not affect a language’s size, but abstractions do. Only some users of a language will encounter a given pattern, but nearly every user is at some point confronted by nearly every abstraction. This quote from Bjarne Stroustrup in a 1992 document on suggested C++ extensions is relevant to many languages:

Please also understand that there are dozens of reasonable extensions and changes being proposed. If every extension that is reasonably well-defined, clean and general, and would make life easier for a couple of hundred or couple of thousand C++ programmers were accepted, the language would more than double in size. We do not think this would be an advantage to the C++ community.

Must all languages grow in size?

There are some interesting exceptions to the trend of languages growing over time.

Lisp-based languages, particularly those in the Scheme tradition, tend to maintain fairly small language cores. However, because of the extensive use of macros, it’s sometimes difficult to know how to classify “language size” in such languages. I’m far too out of date with, say, modern Racket to have an informed opinion on its size.

Lua was, and is, a small language — and, probably not coincidentally, so is its implementation. Interestingly, as far as I have been able to tell, one of the ways Lua’s designers have kept the language evolving is to regularly force (generally small) breaking changes on its user base. It’s an interesting trade-off, and not one that has been taken by any other language that I can think of.

Perhaps the best known (partial) exception is C. Modern C is a surprisingly different language to the original C, but that evolution happens rather slowly these days, with new versions of the C standard coming out around every 8-12 years. Each new versions tends to add few new language features, or even standard functions, and tends to put at least as much effort into clarifying parts of the language’s semantics that have either always been unclear or which have been rendered unclear by new hardware (see Why Aren’t Programming Language Specifications Comprehensive? for some examples).

My impression of C, as someone from the outside occasionally looking in, is that those responsible for the C language standard probably consider themselves to be more “maintainers” than “designers”: they want C to be a useful language in the modern world, but they also clearly want to balance that with not letting C grow too much bigger.

One advantage C has is, ironically, C++: anyone who wants “C but with lots of new features” can be told to go and use C++ instead of growing C in size. Although it’s difficult to distinguish cause from effect, perhaps this has dissuaded those people who like adding new features to languages from considering proposing them for C, when C++ is likely to be far more receptive to their suggestions. Certainly, C++ is a huge language which keeps on growing: Bjarne Stroustrup’s recent-ish plea to “Remember the Vasa” strikes a cautionary note that few would make about C.

Summary

Language designers face challenging trade-offs. Their languages can always be made better for more users by adding new features, but doing so makes the language bigger. At some point, languages become big enough that few users can understand all of the language — but nearly every user of a language will at least occasionally encounter nearly every feature. When users encounter features they don’t understand, their reactions can vary from surprise to worry — if they notice at all! The larger a language is, the easier it is for users to misuse it without even knowing it.

There is no easy way for us to to evaluate whether a new feature is worth increasing a language’s size for. It’s also easier for those people who will benefit from a feature to advocate for it than for those people whose lives might (unwittingly) be made worse by it to advocate against it. Even talking about “size” in anything other than a vague way [3] is hard: quantitative metrics like the size of a language’s grammar or compiler do not always correlate with the inherently qualitative measure of “cognitive load” that two new features may impose on humans.

Perhaps in an ideal world, language designers would at some point declare their languages “complete”, capping the language’s size. However, hardware and other external factors change in ways that sometimes force even the most conservative language to adapt. Even a language like C, which evolves slowly, still evolves. And for as long as a language must evolve, it must have designers. And for as long as a language has designers, there will always be the temptation to add shiny new features.

However, I think language designers can learn something useful from C’s example. While we can’t declare our languages “complete” we can at some point declare them to be in “minimal evolution” mode. This is decidedly less sexy than, and offers fewer job opportunities to language designers, than continually adding new features — but it also allows users to focus on the tasks they want to program, rather than having to deal with ever more complexity. Slowly, over time, new languages will come along which take the best ideas, simplify and unify them, and can keep the field moving forwards. In general, in my opinion, that’s shown itself to be a more successful tactic than hoping that ever-bigger languages can satisfy all users.

Newer 2023-03-23 15:00 Older
If you’d like updates on new blog posts: follow me on Mastodon or Twitter; or subscribe to the RSS feed; or subscribe to email updates:

Footnotes

[1]

Astute readers will notice that this post talks about language “size” rather than “complexity”. In general, these two criteria strongly correlate, but of the two “size” is marginally more objective. But, if you prefer to use “complexity” instead of “size”, you can do so without changing this post’s fundamental meaning.

Astute readers will notice that this post talks about language “size” rather than “complexity”. In general, these two criteria strongly correlate, but of the two “size” is marginally more objective. But, if you prefer to use “complexity” instead of “size”, you can do so without changing this post’s fundamental meaning.

[2]

I don’t have a precise chronology to hand, and at the time I was using an unusual platform at the time – Acorn’s RISC OS – so there was probably a delay between the “main” release and it being available on RISC OS, muddling my memory even further.

I don’t have a precise chronology to hand, and at the time I was using an unusual platform at the time – Acorn’s RISC OS – so there was probably a delay between the “main” release and it being available on RISC OS, muddling my memory even further.

[3]

There’s a reason I haven’t defined “size” precisely in this post — whatever definition I use won’t work for everyone! I’ve hoped that your intuition of “size” is sufficient to understand the overall points I’m making.

There’s a reason I haven’t defined “size” precisely in this post — whatever definition I use won’t work for everyone! I’ve hoped that your intuition of “size” is sufficient to understand the overall points I’m making.

Comments



(optional)
(used only to verify your comment: it is not displayed)