November 26, 2018
Evolve JS! But don’t expect to get out of local maximums. And we really need a language to have a standard static type system that is fully integrated with the compiler and that features are designed around.— Jordan ⚛️ (@jordwalke) November 20, 2018
A view of React growth (from Stack Overflow Trends)
At the time of its creation, there was no language support for classes, so React provided one:
createClass and started promoting the idiomatic way.
React elements are immutable:
Once you create an element, you can’t change its children or attributes. An element is like a single frame in a movie: it represents the UI at a certain point in time.
And props are read-only:
Whether you declare a component as a function or a class, it must never modify its own props.
But considering the declarative and unidirectional flow of data in React, it became a common practice to use referential checks with immutable data structures instead of structural checks, in order to optimize the performance of complex applications: see “Optimizing Performance: Using Immutable Data Structures”.
In this case, the language did not provide solutions. So a feature that generally belongs to the language domain suddenly became a concern for the language users. How does one achieve immutability in a language that doesn’t provide support for it?
Which takes us to the second language feature.
As with immutability, the ecosystem came up with solutions to fill the gap. However, implementing a type system is no easy feat, so in this case there were not that many options. Only three ended up appearing, both backed by some of the world largest companies:
All of them are really powerful, and have helped thousands of teams to scale their apps to levels that otherwise would not be possible, or much less efficient at best. But the lack of a language provided solution puts again a large responsibility on the users of the language, which are forced to make a quite difficult choice.
These teams also assume part of the maintenance costs –upgrades and library type definitions–, and some risk in case the tool they end up choosing ultimately loses traction over time.
Unfortunately, there is no solution in sight for any of the features mentioned above, even at a standard / spec level.
Regarding immutable data structures, Sebastian Markbåge made a proposal back in 2015, but there seems to be no progress since then.
These two features –immutability and type systems– might seem disconnected, but they are both examples of guarantees the language runtime could use to improve performance. Third party immutability and type-checking can’t be relied on by the runtime to improve language performance.
The ceiling of performance optimizations that can be applied at runtime is influenced by these two features working together, but also by others that might seem less relevant at first, like circular dependencies. Elm, OCaml or Haskell for example don’t allow circular dependencies between different modules, which imposes a limitation on users, but also liberates them from the responsibility of removing those circular dependencies as the codebase evolves. It also gives room to the build process to be much faster and incremental.
It has to be noted that the problem is not only language features that might be lacking, part of the problem is also language features that exist today but should be removed. For example, certain optimizations are just not possible when any kind of polymorphism is allowed at runtime. These optimization-blocking features are a big impediment to improve the language too, because there is no way to make progress without introducing breaking changes.
When the architects of a programming language have full control over the type system, data structures and compiler they gain a freedom of design that results in a better experience and performance for the language users. When the ownership of those parts is removed from the language owners, and instead gets fragmented and distributed across the language ecosystem, it becomes much harder to achieve the same experience and make progress.
Ultimately, every participant in the ecosystem ends up paying.
Let’s call language debt the assumed costs (explicit or implicit) by product companies for the maintenance and mitigation of the implications mentioned above. Language debt can be considered part of the classical definition of software technical debt:
Technical Debt = Language Debt + Product Debt
Language debt has a different nature than product debt:
Examples of activities connected with language debt might be:
Evolution of product and language debt over time
From freelancers, library authors, small startups and huge corporations. Everyone has been obliged to roll up their sleeves and figure out in which ways they could get around those issues as they grew their products, usually reinventing the same solutions, or fixing the same problem over and over. The existing overlap between Flow and TypeScript is the most notorious example of the consequences of such a massive delegation of responsibilities from the language architects to the language users.
So how to identify language debt as part of a product team?
One way to identify language debt is to be on the look for discussions around immutable libraries, performance of basic data structures, or type systems. These discussions might feel sometimes like bikeshedding, although it’s not the case at all. The feeling is understandable, because:
Another hint that points to language debt is the formation of teams that go through the codebase to increase type coverage, work on the creation or integration of some immutable library, or attempt to remove circular dependencies to increase the performance of the developer tools.
These teams generally include the name “infrastructure” in their name, like Facebook “Developer Infrastructure” team, which was behind the creation of Flow. The purpose of these teams is in essence to pay off language debt, and those efforts should be categorized as such. Doing otherwise will mean language debt is implicitly assumed as any other product need, which won’t facilitate the evaluation of the total size of that debt.
Also, please note that while the features discussed in the article are very useful, none of them are a hard requirement either! 🙂 A lot of companies and teams have built incredibly powerful products without them.
Regardless what path you decide to follow, I hope this post at least will make easier to identify and account for language debt, in order to make more informed technical decisions about the real costs of the available paths.
One idea is to include “language” in the name of the teams or projects that are in charge of paying it, so the cost of this kind of debt becomes more explicit.
Another idea is to use the two indicators above (pseudo-bikeshedding and pseudo-infrastructure teams) to be more aware when language debt is surfacing, and be able to react as soon as it does, as otherwise it will probably compound over time.
Thanks for reading 🙌 If you have any comments or suggestions, please let me know on Twitter.
Keep shipping! 🚀
Many thanks to Richard Feldman and Jordan Walke for reviewing an early version of this article.