I will admit immediately that the title of this blog post is somewhat sensational. I do not consider myself an enemy of convenient things. I love syntactic sugar in my programming languages as long as I know the translation is principled. I certainly don’t think that any programming language should be made purposely difficult to use or learn.
But… I can’t help but feel that a lot of the time, when people set out with the goal of creating a system that is convenient and easy to use, they end up with something that is neither. It seems to me that the languages most people think of as “convenient” are really those with the most features that look convenient and easy in isolation, but that take painstaking care to use properly when combined with other seeming conveniences. There is something to be said for building systems that make intuitive sense to people, but intuition is too unstable to build a core foundation on — combine a bunch of intuitions, and you might get something that doesn’t even make intuitive sense.
For example, take equality comparison in languages like Perl or PHP, where comparing values of disparate types causes some coercion to take place before the comparison happens. The user might expect two similar things (like a number and a string encoding that number) to compare as equal. But when this intuition is encoded into the language, we destroy lots of other intuitions that a user might have about equality, such as transitivity (“if a == b and b == c, then a == c”) or symmetry (“if a == b, then b == a”). By conveniently accommodating one of the user’s intuitions, we’ve decoupled the “==” operator from the most important intuition of all: “are these two things the same?”. So if a user of the language wants to test for two things being the same, it requires a nontrivial and inconvenient analysis of what comparison operators are available in the language.
Look at the table of weak comparisons here. There are no obvious rules like “Falsy things are equal to each other” or “If two things are equal, then anything equal to one of them is equal to the other” or even “two things-that-represent-numbers are equal if they represent the same number”. There’s just a giant table of arbitrary booleans that you must memorize if you want to use nonstrict equality.
Another misfeature often incorporated in the name of convenience is for functions to silently return some kind of “sentinel value” on failure, where by “sentinel value” I mean something that can easily be distinguished from a successful return value given the semantics of the function, but that nevertheless supports all of the same operations that would be supported by a value returned in case of success. For example, in C, a function to search an array for a particular element might return that element’s index if it is found in the array, and -1 otherwise. Clearly -1 isn’t a valid successful return value of the function — index -1 isn’t even part of the array. But for the program to know that there has been a failure, it’s the programmer’s responsibility to check for one before the return value is ever used. Failure to do so is invisible — using a “sentinel value” without such a check, by definition, doesn’t crash the program, since the failure value supports all the same operations as a successful value. If the program later goes on to crash or do something nonsensical, it could be difficult to trace the error back to its source.
Interoperation of Different Types
Contributing to this problem is the practice of having every built-in type support many more operations than it should. In Python, for example, a string supports the interface of being “multiplied” by an integer — something like
"hands" * 3 evaluates to
"handshandshands". Similarly, in many dynamic languages, every type supports the interface of “being used as a boolean”. In some languages like PHP, even the null value supports some operations that are surprising, such as being compared to integers. These behaviors, while they may seem convenient and easy when you want them (why shouldn’t you be able to repeat a string three times?), mean that a string is not a good error value for a function that normally returns integers, and nothing is a good error value for a function that normally returns a boolean, and
NULL isn’t a good error value for any function, because in each of these cases if the user forgets to check for an error, the language will offer them no help in spotting their mistake.
This is especially a problem because it seems, intuitively, that (for example) integers are different enough from strings that they should be okay to use as an exceptional value — and dynamic language designers and library writers often do use “different enough” types as exceptional values. But because both types support too-diverse operations, the language offers no help in formalizing the programmer’s intuition that “I want an integer here, and if for some reason this variable is a string then that’s an error”. Instead, the user has to make the relevant checks (which can often be difficult to get right, if the language goes too far out of its way to make different types interchangeable) manually.
So what instead?
All of these features have a kernel of goodness about them. Sometimes we really do want to test things like “is this string, interpreted as a number, the same as this other number?”. It’s nice to be able to check for errors using the return value of a function instead of wrapping some code in a
catch block. And it’s good to be able to write functions that work on lots of different types of similar data and treat all the different types the same. But trying to make all of these nice features implicit is a false convenience — it hides important details of what’s going on from the user, causing much more trouble than it gains.
So how do we get features that are actually convenient and easy to use? One thing that helps is to make them explicit.
For example, don’t coerce things before a comparison unless the user asks for it.
11 == toInt("11") is hardly less intuitive than
11 == "11", and arguably is even more intuitive — the former reads as “is 11 the number represented by the string ’11′”, whereas the latter reads more like “is the number 11 the same thing as the string ’11′”, not to mention that requiring
toInt saves all the tangled madness of PHP’s weak comparisons. All we have to do is have the user tell us how they want to compare things, instead of trying to guess.
In Haskell there are types for explicitly representing the possibility of failure, which can’t be used like ordinary values unless the failure has been acknowledged and handled. (This could be done in most languages that aren’t C, but almost never is.) So instead of returning
-1 on failure to find the index of an element in an array, we return
Nothing (which sounds suspiciously like
null but for the crucial difference that it doesn’t inhabit arbitrary types, which means there is no risk of accidentally using
Nothing where you meant to use an integer). And in case of success we return
Just 7 (for example) instead of
7. Since neither of these is an integer (one is an empty box representing failure, and the other is a wrapped integer that is never transparently unwrapped), the user can never forget to check whether there was a failure. If they do, the compiler will point out their mistake.
And instead of making built-in types support lots of different operations, you can have functions that allow the user to ask explicitly for the various behaviors that those operations can have for different types.
repeat("hands", 3) is like
"hands" * 3, but makes explicit that you’re expecting a string here — this is especially important if you’re using a variable instead of
"hands", because otherwise it’s not immediately clear what the “multiplication” is supposed to do. Similarly,
if isEmpty() ... is a drop-in replacement for
if  ..., except in those languages for which
 is truthy. Do you know off the top of your head which scripting languages treat
 as true and which treat it as false? But with an explicit
ifEmpty it’s obvious what circumstances will lead into which branch of the conditional.
My intuitions about the “right” way to design a programming language are pretty complex. I’m not sure I could describe them completely to myself, let alone to someone who doesn’t live in my brain. But I’m pretty sure that there should be mathematical rigor involved, even if you’re designing a language that will never be used by mathematicians, even if you don’t want your users to have to learn any math to use the language.