The Death of Programming Languages
26 Aug 2022Everything that has a birth will end in death. Programming languages are no exceptions.
In this post, I argue that one major factor that accelerates the death of a programming language is the degradation of the qualities of programs written in the language, such as reliability, performance, maintainability, and security. The degradation happens when bad code patterns become mainstream in the community via frameworks, libraries, tutorials, etc.
The Death of Programming Languages
How can a programming language die? Well, literally a language dies when no new programs are written in the language. As new programs must be written to keep the world running, it means users are moving to other languages. Such a move can happen for many reasons:
- Learning curve
- Quality of programs: reliability, performance, maintainability, security.
- Ecosystem: availability of libraries.
- Tooling: compiler, IDE, debugger, profiler, etc.
- Productivity: succinct syntax, expressive language feature, tools, libraries, tutorials, community/support.
It is clear that not all the factors above are determined by the inner qualities of the language itself. For example, better tooling depends on investment which in turn depends on the business success of the language, and the latter is a complex technological, social, and economic process.
While the rise and fall of a programming language is a complex phenomenon, the degradation of qualities of programs in a language will accelerate its death. This becomes clear when we examine the success of new programming languages:
- Java over C/C++: portability, productivity, reliability
- Scala over Java: productivity, reliability
- Ruby over Java: productivity
- Rust over C/C++: reliability
- Go over Java: performance
- TypeScript over JavaScript: reliability
Note that when users move to a new language, the dominating reasons are mainly about quality of programs written in the language. Tooling and ecosystem are seldomly mentioned as the major reason for switching to a new language, though they will be certinaly important for the survival of a language.
Early adoption of a new language usually happens in scenarios where the concern for quality of programs outweighs the concern for tooling and ecosystem. If the latter is vital for the adoption of a new language, there can hardly be any successful new languages.
Looking from another perspective, if the quality of programs written in a language is degrading, then there will be more incentives for users to move to another language, thus accelerating the death of the language.
Dimensions of Quality
The quality of programs has many dimensions, such as reliability, maintainability, performance, etc. Historically, the language war happens between reliability and performance: Experienced engineers keep writing in assembly code for better performance sacrificing maintainability and portability. It took almost two decades for the wide adoption of high-level languages. Some explanation say that the world has to wait until the old generation of engieers retire. Advances in hardware should also have helped in settling the dispute.
No one can deny the importance of performance. However, programmers need to make informed decisions when facing the tradeoff between performance and reliability. In a lot of cases, a slight difference in performance does not matter much to the end user, but reliability is vital. Meanwhile, architectural and algorithmic improvements should be preferred first before considering inefficiency in the runtime/language.
When it comes to maintainability, it is important to avoid unnecessary abstractions and heavy-weight frameworks. A common mistake of both functional and object-oriented programmers is to abuse OO and FP design patterns for marginal benefits at the price of simplicity and maintainability of code. To be clear, the most important abstractions for a program must be abstractions for the problem domain, which I sometimes call concrete abstractions (inspired by the name Concrete Mathematics). Mathematicians have the same opinions here, as put by G. H. Hardy in A Mathematician’s Apology:
It is not mere ‘piling of subtlety of generalization upon subtety of generalization’ which is the outstanding achievement of modern mathematics … too much tends inevitably to insipidity… mathematical ideas also become dim unless they have plenty of individuality.
Maintainability of a project is determined largely by the cognitive load for understanding the code base. Programmers’ budget for cognitive load is limited. Each new abstraction in the program is paid by the budget. The lavish abstractions in heavy-weight frameworks drain that budget quickly. Pragmatic programmers use the budget wisely by creating simple and powerful abstractions.
Productivity Vs. Quality
In the domain of web applications, the concern for productivity sometimes is bigger than the concern for quality of programs, as evidenced by the slogan “move fast and break things”. Therefore, in the domain of Web programming, there is a tension between productivity and quality in the choice of languages.
Many startups love Ruby because of the productivity gain in using Ruby on Rails, which is enabled by the expressiveness of Ruby and its succinct syntax. Now statically typed languages catch up with light-weight web frameworks, and Ruby gradually loses traction. Twitter’s migration from Ruby to Scala created a crisis for Ruby. Now Ruby 3 also embraces a static type system, but the incentives to move to Ruby are not as strong as before.
The focus on productivity is justified in prototyping. In throw-away prototypes, it is fine to choose any language and tool that can get the program run quickly. However, for evolutive prototypes, I do not see a reason to choose dynamically-typed languages any more — scripting or interpreted languages should be typed too!
The Health of Languages
The quality of programs — e.g., reliability, performance, maintainability, security — depends not only on the language, but also on the idiomatic code patterns advocated in the community by mainstream frameworks, libraries, and tutorials.
For language designers, there is a always a tension between the concern for quality of programs and the concern for productivity of programmers (not mention the well-known tension between performance and reliability). Some language features, e.g., meta-programming, may boost productivity but harms readability and maintainability of programs when used without care. As another example, higher-kinded types boosts reliability but compromises readability and understandability as its usage increases.
While it is difficult to come up with a general principle for balancing the quality attributes in designing new language features, special care must be taken when a language feature may potentially undermine all quality attributes of programs. Runtime reflections in Java and runtime metaprogramming in Ruby are two such examples. These language features compromise program performance, program understanding and reliability, thus should be avoided to maintain the health of the language.
The biggest damage to the health of a language comes from bad code patterns that plague the ecosystem — libraries, frameworks, and tutorials. Java reflections is the typical exemple. The two popular Java programming idioms, namely service-provider pattern and dependency injection heavily depend on Java reflection. While such programming patterns cut down syntactic verbosity, they result in compromised performance, maintainability, reliability, security and portability of programs. If most applications, frameworks and libraries are written this way, the future of Java is endangered.
The quality degradation of programs is not only a sad news for the language and its community, but also a bad news for the society, as the programs are underpinning the world. All people in the community — language designers, library authors, teachers, programmers — can make an effort to safe guard the health of the language by avoiding language features and code patterns that compromise the quality of programs.