Rust's language ergonomics initiative

Aaron Turon has a nice blog post about Rust's language ergonomics initiative. It contains a description of how the Rust language design team currently thinks about "implicit" features (those that do something without the user having to write anything explicitly at this point in the program), how to design such features or detect issues with existing features.

There are three dimensions of the reasoning footprint for implicitness:

Applicability.
Where are you allowed to elide implied information? Is there any heads-up that this might be happening?
Power.
What influence does the elided information have? Can it radically change program behavior or its types?
Context-dependence.
How much of do you have to know about the rest of the code to know what is being implied, i.e. how elided details will be filled in? Is there always a clear place to look?

Various existing or proposed Rust features are then discussed and critiqued along these evaluation axes. I found the blog post interesting on several levels and thought it could be a nice starting point for discussion.

One thing that is important to keep in mind is that, despite the generality of the claims made about programming language design, this text is mostly about a set of design tools that has been conceived to solve a specific category of ergonomics problem. LtU regulars will be quick to point out that the interaction with programming tools, for example, is not discussed or considered at all while it is a central aspect of ergonomics in many cases (including, I think, implicitness decision). I would personally recommend trying to stay within the approximate scope of the Rust discussion (generalizing to other languages and systems), rather than discussing all aspects of "Language Ergonomics" at once, which would result in discussions all over the place (but with less people interested in each sub-discussion).

A parting remark on the end paragraph which I foud interesting:

And, perhaps most importantly, we need empathy for each other. Transformative insights can be fragile; they can start out embedded in ideas that have lots of problems. If we’re too quick to shut down a line of thought based on those problems, we risk foreclosing on avenues to something better. We’ve got to have the patience to sit with ideas that are foreign and uncomfortable, and gain some new perspective from them.

Clearly Aaron Turon is getting at the tone of online discussions which can become heated, even for details of programming language feature design. But what I think of reading those lines is that such discussion are helped by one aspect of our work as language designers, which is to produce tests and criterions to evaluate language features (and, yes, shut down some proposals more quickly than others). I fully agree that, in a foreign land of creativity, one should not judge too quickly or too harshly. But I think that the development and wider adoption of these design tests would not only result in seeing the flaws in some proposals more quickly, it would also let people iterate more quickly on their own designs, applying the tests themselves to recognize the issues without having to go through public feedback (and potential harshness).

I see similarities with the notion that mechanized proof assistants made it much easier to teach how to write rigorous mathematical proofs to beginners, as they can get direct feedback (from the computer) on whether their proof attempts are correct, without having to ask for the explicit approval of another human (their teacher or their student colleagues), with the various inhibition effects that it created. Having this internal feedback is what the design principles, laws, and various meta-theorems that our community developed enable, and it is one of the side-benefits of formalism.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Big +1

Despite some of my past statements in disagreement with many Rust design decisions, I'd like to say: They are quite clearly doing excellent language research. But even more importantly, they are doing stellar community management. I hope this ergonomics initiative is wildly successful because as much as I'd myself rather write low level code with Terra, I'd also much rather debug colleagues' Rust than C/C++.

Isomorphisms

At least a sub-part of the question "what to make implicit?" must deal with whether a particular isomorphism be dealt with implicitly of explicitly. I contend this issue is not merely one of ergonomics, but involves mathematics too.

For example in Felix the type isomorphism T * T * T = T ^ 3 is implicit. A tuple of three values of the same type is an array. It's not merely isomorphic to an array. It IS an array. This choice has consequences. Is the choice even sound?

If we carry this through we require T = T ^ 1, and 1 = T ^ 0. This may seem reasonable but the consequences are dire. Consider a function operating on an array, T ^ N, and set T = U ^ M. This raises the issue is (U ^ M) ^ N = U ^ (M * N)? Or should that be U ^ (N * M)? And conversely, if you see a function f: T^N where N=1 and T=U^M, does it iterate once over a single U^M or over U many times?

Its clear, that even though A * B is isomorphic to B * A you'd never make such an isomorphism implicit. On the other hand, almost all functional programming languages make the lift of a function to a function value implicit to the point where most programmers are unaware of the difference, and some compilers appear to throw the difference out too early (Ocaml I suspect).

So before we even ask about ergonomics, we have to ask if making an isomorphism implicit is sustainable.

For Felix arrays I don't know the answer. However at least part of the failsafe is that for a polymorphic function you can, and sometimes *must* explicitly specialise the type variable. Clearly the implicit isomorphism raises issues which suggests it is a bad idea. But in common case is its significant simplification. So perhaps it is OK, if an implicit operation has ambiguous corner cases, provided there is a way to resolve the ambiguity?

[More examples next post]

C/C++ casts

This is another example. In C you can implicitly convert in both directions between a T* and a void*. In C++ the conversion from a void* to a T* requires an explicit cast:

void *pv;
int *pi = (int*)pv;

As you can see, this make some code much more verbose, and I once spent several days converting a C library to C++ just adding these casts so it compiled. Was the removal of the implicit conversion a good idea?

But it isn't just a matter of ergonomics. Stroustrup introduce a more refined collection of casts in an attempt to make it easier to reason about a particular conversion. I had a huge argument with him about his choices because the alebgra just doesn't work. For example you have to write this:

int const *pic;
void * pv = const_cast<void*>(reinterpret_cast<void const*>(pic));

which is stupid IMHO. The idea is that you have to use a const cast to cast away const, separately from changing the pointed at base type with a reinterpret cast, but this makes no sense at all if you're casting away the base type entirely, indeed the idea of a void const* is nuts because you cannot do anything with it without casting it.

C has implicit conversions which make reasoning difficult. C++ takes this to an extreme: one argument "normal" constructors specify implicit conversions. Derivation specifies implicit conversions. You can even define operator methods to specify an implicit conversion. The situation is so unreasonable C++ has an "explicit" keyword for constructors to disable the implicit conversion. Throw in overloading and the mess deepens .. and now add templates and all hope is lost. Zero (literal 0) is so ambiguous a specific nulptr was introduced.

There are rules. They're hard to follow. But implicit operations in C++ can never be eliminated because templates depend on them. Some here might criticise the type system as being unsound .. that is a minor issue which rarely has any consequences compared to the difficulty of reasoning in the presence of so many implicit conversions.

So there is another dimension to the arguments about what should be implicit: one must not only consider the underlying algebra of the existing language but ALSO the algebra of possible extensions. Almost ALL implicit operations collapse under some extension.

I have to write one of these in Felix:

for i in 0uz ..< a.len ..
for i in 0.size ..< a.len ..
for i in 0 ..< a.len.int ..

because there are no implicit integral conversions (and the type of the length of an array given by the len function is size, not int). Its a real pain compared to C/C++, and the pain gets worse in more complex expressions. It isn't just a burden figuring out the types involved, one must choose where to do the conversions. Worse, if some types change, the expression has to be edited. I chose to disallow implicit conversions so that overloading would be easier: overloads require an exact match up to type variables precisely so the implicit deduction of the type variables can work directly by unification. But the choice has consequences.

Thinking about tools is still relevant

A way figure out context dependence is to look at whole program transformations from the whole language to a restricted dialect of the language in which the implicitness is not there.