Posts Tagged ‘types’

JavaScript is a very interesting language

Friday, January 28th, 2022

The other day I learned about indexed setters in JavaScript. It hadn’t occurred to me that you can do the following:

var z = 5;
Object.defineProperty(Array.prototype, 0, { set: function(y) { z = 42; }});

and the interpreter will happily accept it. This code mutates the global object called Array.prototype, overriding the (implicit) setter method for the property '0'. If I’m to compare this with a more conventional programming language (using C-like syntax for illustrative purposes), this would be like if you could write:

int n = /* ... */;
int[n] a;
a[0] = 5;

but the semantics of the last assignment statement could involve arbitrary side effects, and not necessarily mutating the 0th element of a, depending on the value of some global variable.

Most languages don’t allow you to customize this behavior. In JavaScript, it wasn’t an intentional design decision (as far as I know), but a consequence of a few design choices interacting:

  • Everything is an object, which is to say a bag of key-value pairs (properties)
  • What’s more, properties aren’t required to be simple lookups, but can be defined by arbitrary accessor and mutator functions (setters and getters) that can interact with global mutable state
  • Numbers and strings can be cast to each other implicitly
  • Inheritance is prototype-based, prototypes are mutable, and built-in types aren’t special (their operations are defined by prototypes that user code can mutate)

It’s not immediately obvious that together, these decisions imply user-defined semantics for array writes. But if an array is just a bag of key-value pairs where the keys happen to be integers (not a contiguous aligned block of memory), and if strings are implicitly coerced to integers (“0” can be used in place of 0 and vice versa), and if specifications for built-in types can be dynamically modified by anybody, then there’s nothing to stop you from writing custom setters for array indices.

To someone used to modern statically typed languages, this is remarkable. Programming is communication between humans before it’s communication with machines, and communication is hard when you say “rat” when you really mean “bear” but you won’t tell your conversation partners that that’s what you mean and instead expect them to look it up in a little notebook that you carry around in your pocket (and regularly erase and rewrite). If that’s what you’re going to do, you had better be prepared for your hiking partners to react with less alarm than is warranted when you say “hey, there’s a rat over there.” If your goal is to be understood, it pays to think about how other people will interpret what you say, even if that conflicts with being able to say whatever you want.

And if you’re trying to understand someone else’s program (including if that someone else is you, a month ago), local reasoning is really useful; mental static analysis is hard if random behavior gets overridden in the shadows. More concretely, the kind of dynamic binding that JavaScript allows makes it hard to implement libraries in user space. For example, if you want to build a linked list abstraction and use arrays as the underlying representation, the semantics of your library is completely dependent on whatever modifications anybody in scope has made to the meaning of the array operations. So that’s not an abstraction at all, since it forces people who just want to use your data structure to think about its representation.

This came up for me in my work in progress implementing the tuple prototype methods — the most straightforward ways to re-use existing array library code to build an immutable tuple abstraction don’t work, because of the ability users have to override the meaning of any property of an array, including length and indexed elements. The workarounds I used depend on the code being in a self-hosted library, meaning that it has to live inside the compiler. It makes sense for the record and tuple implementations to live inside the compiler for other reasons (it would be hard for a userland library to guarantee immutability), but you can easily imagine data structures that don’t need to be built into the language, but nevertheless can’t take full advantage of code re-use if they’re implemented as libraries.

Now, plenty of people who use and implement JavaScript agree with me; perhaps even most. The problem is that existing code may depend on this behavior, so it’s very difficult to justify changing the language to reject programs that were once accepted. And that’s all right; it’s a full employment guarantee for compiler writers.

Even so, I can’t help but wonder what code that depends on this functionality — and it must be out there — is actually doing; the example I showed above is contrived, but surely there’s code out there that would break if array setters couldn’t be overridden, and I’d like to understand why somebody chose to write their code that way. I’m inclined to think that this kind of code springs from dependence on prototype inheritance as the only mechanism for code reuse (how much JS imposes that and how much has to do with individual imaginations, I can’t say), but maybe there are other reasons to do this that I’m not thinking of.

I also wonder if anyone would stand up for this design choice if there was an opportunity to magically redesign JS from scratch, with no worries about backwards compatibility; I’m inclined to think there must be someone who would, since dynamic languages have their advocates. I have yet to find an argument for dynamic checking that wasn’t really just an argument for better type systems; if you’re writing a program, there’s a constraint simpler than the program itself that describes the data the program is supposed to operate on. The “static vs. dynamic” debate isn’t about whether those constraints exist or not — it’s about whether they exist only in the mind of the person who wrote the code, or if they’re documented for others to understand and potentially modify. I want to encourage people to write code that invites contributors rather than using tacit knowledge to concentrate power within a clique, and that’s what motivates me to value statically typed languages with strong guarantees. I’ve never seen a substantive argument against them.

But what’s easier than persuading people is to wear my implementor hat and use types as much as possible underneath an abstraction barrier, to improve both performance and reliability, while making it look like you’re getting what you want.

“The sad truth is, there’s very little that’s creative in creativity. The vast majority is submission–submission to the laws of grammar, to the possibilities of rhetoric, to the grammar of narrative, to narrative’s various and possible structurings. In a society that privileges individuality, self-reliance, and mastery, submission is a frightening thing.”
— Samuel R. Delany, “Some Notes for the Intermediate and Advanced Creative Writing Student”, in About Writing

Even when languages offer many degrees of freedom, most people find it more manageable to pick a set of constraints and live within them. Programming languages research offers many ways to offer people who want to declare their constraints ways to do so within the framework of an existing, less constrainted language, as well as inferring those constraints when they are left implicit. When it comes to putting those ideas into practice, there’s more than enough work to keep me employed in this industry for as long as I want to be.