The common objection to the idea that programming languages should be powerful enough to express almost any idea or abstraction is that this will result in certain programmers on a team – who are either much smarter than everyone else, overestimate their own intelligence, or are simply undisciplined – writing code that nobody can understand using all sorts of unnecessarily advanced features. Thus, they suggest, the correct solution is to dumb down a programming language, to hobble it, cutting out power and abstractions and concepts left and right to prevent bad programmers from getting their hands in the cookie jar.
The downsides to this approach are:
- You often ultimately end up needing these features anyway, and so they slowly creep back into the language by the back door, often in a more complex and ad hoc way than they would have been if they'd been included from the start. Just look at the development of Go (the recent introduction of generics and iterators) and Java (where do I even start) over time, or the way Java programmers liberally sprinkle compiler plugins and decorators everywhere.
- It makes your programmers dumber.
- The consequence of your language not having a concept or abstraction when it's really needed or particularly natural for a given problem or domain is that you end up writing more code, that uses awkward and ill-fitting, counterintuitive ontologies, and can't communicate tacit knowledge well because it's so caught up in low level details and getting around the language, so that the resulting code is often far harder to understand than it needs to be.
However, all of these problems will probably be a smaller issue and/or occur less frequently than undisciplined programmers doing ill-advised tricks on the company codebase. So we do need to figure out a way to handle those programmers while avoiding these problems. How?
Luckily, there is a solution: these downsides to the language-level technological approach to solving the problem of undisciplined programmers are actually symptoms that can lead us to a better solution. They're symptoms of the fact that this language level solution is actually a category error: applying a technological solution to a social problem. The solution to undisciplined programmers is to discipline them, not to try to eliminate the need to discipline them in the first place, because that solution is like chopping down a tree in order to trim it – it gets at the root of the problem, sure, but it's too rigid, too absolute, too far-reaching for the actual category of the problem it's trying to solve.
How might we go about disciplining the undisciplined programmers, so that they don't use unnecessarily powerful features? In my opinion, the core of the process should be code reviews. The whole problem with using overly powerful features in your language, after all, is that you might produce code that your peers can't easily understand – and by extension, that future you probably won't be able to understand either. So before you can merge any code, why not ensure that a certain number of your peers have to sign off on it, and that they have to actually read and review your code for over complication before they do so? Why not set up a linter that flags the use of any features the project team as a whole has decided are likely to be misused, and directs the attention of the code reviewers to those locations in the code, and if they can't immediately see the need of whatever construct you're using, or can't immediately understand it, or if there is even one workable alternative to using that feature that doesn't have significant other downsides, then you have to go back and rewrite it using a simpler feature? Perhaps we could even have linters that automatically suggest using less complex features, when that can be ascertained, and that can be put into the CI system?
The benefit of this is that there's more flexibility – if a feature or abstraction is truly need, it can still be used – but also that it essentially "magically" adapts to the level of experience, technical expertise, intelligence, and preferences of your team, instead of having to use the coarse-grained proxy of a language with limited features. That's the benefit of using a social solution to a social problem: it tightens up the gap between the desired outcome (your team members being able to understand what you've written) and the metric/process used to achieve it (actually just asking your team members), and allows for the necessary level of flexibility.
An objection to this might be that the peers one relies on in code review processes might be anti-intellectual, themselves undisciplined, or blindly following some cargo cult software practice (such as Clean Code), such that the process becomes a farcical waste of time. However, again the solution to that is not to accept that programmers will always on average be bad and dumb as some immutable state of the world that we're just helpless to do anything about, and use absolute technological solutions to solve the problem. The solution, once again, is to use social solutions to social problems – namely, invest in your programmers. Give them mentoring, on the job training, set aside a portion of their paid time to learn and improve, perhaps with textbooks or even courses you offer them, things like that. Try to foster an environment where programmers want to stay working on a project for a very long time and become experts in that particular project – maybe offer pensions if they work somewhere long enough? – so that all the developers can get to know each other and form a common understanding about how to write and design code. Why in the hell are we as an industry optimizing for a revolving door of strangers unfamiliar with a code base and domain and unfamiliar with each other, who we assume are all under-trained, because we don't invest in their training at all, to be doing all of our huge, complex, and long term projects? It's absurd! (Well, until you remember this is capitalism we're talking about – don't want those workers to get too much leverage!)