Language designers aim to optimize an experience for their prospective users, but the criteria and heuristics by which they ‘optimize’ are widely varied and inconsistent (often within a single designer). In this article, I’ll present two common points of contention and my opinion on them.
First is picking a flavor of freedom:
- Anarchy: A language by hackers, for hackers. The individual developer has privilege and power and ambient authority. Typing is a guard rail that developers are free to step across where it suits them. Ad-hoc language extensions and transforms are feasible by use of code-walking macros.
- Liberating Constraint: The language abridges the authority and power of individuals in order to protect system-scale properties such as safety, security, consistency, resilience, optimizability, and immunity to deadlock.
Both of these can improve developer productivity. Anarchy gives the individual more power in an immediate sense – there is rarely any need to redesign a poorly chosen set of abstractions to fit some forgotten corner case. Liberating constraint aides the developer by ensuring high-quality libraries, and supporting local reasoning about application behavior without reading the details.
This isn’t a binary choice. Some language designers will choose the constraint route but add an escape hatch, like unsafePerformIO in Haskell. Given a large set of system properties, a language designer might choose to protect some of them and leave others to developers.
I quite firmly favor liberating constraints, without any ambient escape hatches. I believe individual developers will achieve greatest productivity and flexibility by having a massive pool of libraries and resources they can use securely, safely, and effectively – even in the absence of ‘trust’. Capabilities and RDP demands do offer me a secure mechanism to provide reflection, meta-object protocols, and other ad-hoc effects if necessary – but entirely within the rules of object capability model and RDP.
A ‘crutch’ in language design is a solution to a problem of accidental complexity. Such problems wouldn’t exist with (a) the foresight to choose a better abstraction and (b) support for desired abstraction need without sacrificing other valuable properties (such as performance).
One example of a language crutch is keyword and default parameters, which simplify development work with large parameter lists. The function glTexImage2D takes nine parameters, for example, and rather than:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, pData)
We could use:
glTexImage2D(target:GL_TEXTURE_2D, internal:GL_RGBA, height:256, width:256, dataFormat:GL_RGBA, dataType:GL_UNSIGNED_BYTE, theData:pData)
I’m assuming here that the `level` and `border` parameters default to 0. Obviously this latter form is more verbose, even with the defaults. But many would claim it easier to read, write, and maintain – properties that some language designers judge more important than terseness. Developers might still have access to ordered parameters, if that is their preference.
Okay, so this was an improvement according to some people. But this is effectively a band-aid for the real problems:
- glTexImage2D does not use a texture abstraction. A simple refactoring could capture height, width, border, data format, data type, and the buffer itself into a value abstraction of type `Texture`.
- Two parameters – level of detail number, internal format – are effectively optimization switches that, under ideal circumstances, could be avoided or at least separated.
- The ‘target’ type is very often GL_TEXTURE_2D. It would be trivial to shift the case of GL_TEXTURE_CUBE_MAP* into a separate function.
In practice, it is feasible to reduce the code to simply:
glTexImage2D(texData), by separating the optimization parameters, shifting the cube maps to another function, and capturing the texture information in a dedicated value abstraction. I think most cases of large parameter lists have similar issues, such that better abstractions would avoid the issue. After all, DSLs are typically built of a medium-size vocabulary (e.g. 30-60 elements) of simple, composable functions and primitives.
Naturally, designers lack perfect foresight; languages and abstractions will be imperfect. There will always be some application for crutches or band-aids. The question is whether one should supply such crutches. Regarding crutches:
- The existence of a crutch reduces incentive to solve the problems. By nature, use of the crutch must be easier than solving the problem. Unattended problems tend to accumulate. Developers become more dependent on the crutch as the number of half-abstracted libraries grow.
- More features means more complexity. Due to feature interaction, this is a combinatorial concern, and thus the extra features will hide flaws and complications in the language design. For example, designers choosing named parameters rarely consider the impact on first-class functions and point-free function composition.
- Crutches tend to be ‘optional’ in a bad way: the decision has no semantic impact beyond apparent, immediate convenience to the user. They can easily become a source of decision fatigue.
Sadly, language designers and users tend to see use of a feature as proof of its success and utility, without ever recognizing the larger self-fulfilling prophecy or the implications of the fact that a language-crutch is so widely needed.
I do not favor ‘crutches’ in language design. In their absence, developers might instead use boiler-plate, frameworks, or design patterns. I’ve done my best to support proper abstractions in these roles: user-defined syntax (per module), reactive semantics (which covers many frameworks as plain old abstraction, and makes frameworks more composable), easy scatter-gather between data models (to maintain alternative views and abstractions), and securable interaction with ad-hoc foreign services (which can then cover corner cases).
I believe that avoiding crutches (and their obfuscating complexity) will make the weaknesses and pains of a language more obvious, make it easier to identify and resolve most problems by use of libraries, frameworks, and syntactic abstraction, and to catalog the solutions that did not abstract effectively or required too much external support. Developers will have greater awareness of weaknesses in their libraries, and greater incentive to fix them. As the libraries and services improve, so will developer productivity; they won’t be hindered by dragging a crutch.