On the expressive power of programming languages (2019)
pwlconf.org
pwlconf.org
"The literature on programming languages contains an abundance of informal claims on the relative expressive power of programming languages, but there is no framework for formalizing such statements nor for deriving interesting consequences. As a first step in this direction, we develop a formal notion of expressiveness and investigate its properties. To validate the theory, we analyze some widely held beliefs about the expressive power of several extensions of functional languages. Based on these results, we believe that our system correctly captures many of the informal ideas on expressiveness, and that it constitutes a foundation for further research in this direction."
Highly recommended, FWIW.
Also, this talk seems wonderfully lucid.
I do not have an academic background in theoretical computer science and was nevertheless able to follow along his talk perfectly well.
For anyone interested in reading it, the paper by Matthias Felleisen that this talk is based on can be found on the author's website: https://felleisen.org/matthias/papers.html
(1) In the expressiveness debate, only the upsides of increasing expressive power are discussed (avoiding global rewrites) but the downsides are not (breaking intuitive expectations).
(2) The theory side and the UX side of language design communities seem to operate independently, to the detriment of the results.
The last example of an extension that increases expressive power of a language — adding truthy/falsey conditionals, over pure Boolean conditionals — is messing with my intuition! The talk obviously showed an example to prove this, and I follow the logic, but I still find it a surprising result. Maybe it chimes with one of the questions that was asked; the context required to show the distinction is engineered and quite “unnatural” (at the risk of being “hand-wavy”), which is at odds with the human meaning of expressiveness.
e1 == e2 iff
for all contexts C,
C[e1] halts iff C[e2] halts.
Which is a great trick to avoid having to compare contexts. Around 29:00 they discuss how to show 5 != 6.One question I have is: what if your language doesn't allow for ways to halt? For example, a language where you have no loops. That definition would imply everything equals everything, since all contexts eventually halt.
(The reason for this that if you are describing the semantics of a language using a collection of reduction rules, and evaluation gets to a point where no reduction rule applies, then the program gets “stuck”. You would normally implement this as a runtime error but semantically it has the same effect as an infinite loop.)
My go-to example is the hash table. Coding in C, one is always cobbling up hash tables, because C hasn't the expressive power to code and publish a generally useful hash table library. Many other languages sidestep the problem by building hash "dictionaries" into the core language.
A few have enough expressive power to capture the hash table pattern (and numerous variations on it) in libraries that may then be used almost as if they had been built into the core language.
Whenever a pattern shows up in many different programs coded in a language, it indicates that the language is not quite expressive enough to usefully capture that pattern in a library. Some languages make a virtue of the weakness, and call it simplicity. Others pull patterns into the core language. A few add expressiveness until it becomes possible to capture the pattern in a library.
Clearly the last is best, for a general-purpose systems language, because it enables capturing an open-ended range of patterns in libraries, and each capability joins the others, multiplying them. But it is also much harder to invent and to get right. Usually, too, the syntax needed to use a library version of a feature cannot be made quite as nice as is possible for a language built-in.
So, in C++ we see core support for exceptions, virtual functions, and coroutines, with pattern matching and reflection coming, but the type system is also continually strengthened to make more kinds of libraries possible.
A more expressive language might not need built-in exceptions or virtual functions, because those patterns could be coded into libraries and used from there. Rust has the Drop trait in its library, so does not need destructors, as such, in the core language.
Downsides to expressiveness of this kind include that interoperability may suffer, as there are too many ways the same thing could be achieved, and therefore little regularity. Performance may suffer if work cannot be moved to compile time; or, if it can, compile time may balloon.
So, some patterns are naturally better to pull into the core: particularly, those with an obviously correct, unique form; or that are needed to help integrate libraries from different sources. A Standard Library is a place to put such features that would not benefit much from adoption into the core.
Whichever course is taken, people will complain that the language is getting too big and complicated, and the standard library too big.