Rust 2030 Christmas list: Inout methods

This is the third entry on my Christmas list for Rust 2030.

These articles are me fantasizing about what I’d like the Rust programming language to be like, if we had infinite resources to implement every possible feature. I’m not worrying about “is this important to implement right now” so much as “what should the language look like on the long term”.

For my third article, I’d like to talk about mutability duplicates.

Definition

“Mutability duplicates”, for lack of a better term, are functions that you need to write twice, where both versions do the exact same thing, except one of them has different mutability annotation. They’re the bane of any language where the type system keeps track of mutability.

In Rust, they might look like this:

fn get_value(&self) -> &Value {
  self.value
}

fn get_value_mut(&mut self) -> &mut Value {
  self.value
}

In C++, they might look like this:

Value* get_value() {
  return this.value;
}

const Value* get_value() const {
  return this.value;
}

C++ developers are annoyed enough about the duplication that elaborate workarounds have started to surface, like this StackOverflow answer:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Now, Rust developers are usually more easygoing about minor code duplication (my personal view is that if you’re only duplicating one or two line, adding abstraction to remove the duplication isn’t worth it), but still. This is annoying.

Mutability duplication creates tons of papercuts: it means you to remember to add _mut after every method call, it creates API bloat, vtable bloat, etc.

Is there a better way?

D’s solution: inout

In the D language, inout is a sort of “wildcard” of type qualifiers.

For instance, while the get_value example in D could be written like this:

Value* get_value() {
  return this.value;
}

const(Value)* get_value() const {
  return this.value;
}

The idiomatic way to write it is actually

inout(Value)* get_value() inout {
  return this.value;
}

Thus, there is no code duplication. Besides being shorter to write, this has a minor documentation advantage: a library user can read the prototype and know that the function can be called with a mutable or immutable object, and that the code executed will be the exact same either way (so, for example, the mutable version can’t lock a mutex).

How it works

I’m only passingly familiar with the D language, so forgive the imprecise language.

At the call site, inout acts like a wildcard, that gets resolved to a particular type qualifier. What type qualifier it gets resolved to depends on the types of the parameters passed to the call site.

For instance, given this code:


inout(int)* foobar(inout int* a, inout int* b);

// ...

foobar(my_a, my_b);

The type system takes the types of my_a and my_b, and finds the least restrictive qualifier that matches both these types. inout becomes that qualifer.

For instance, if my_a is const int* and my_b is int*, inout “becomes” const, and the function returns const int*. If both my_a and my_b are int*, then the function returns int*.

(There are more matches, but this is the simplified version.)

Inside the function’s implementation, inout is basically a fancy version of D’s const (which is roughly equivalent to C++’s const). There are some subtyping rules, but the main rule is that you can’t mutate an int if all you have is an inout* int.

Implement inout in Rust

With all that in mind, how could Rust implement the inout qualifier?

Well, first off, I think the rules should be a little different. D’s type system is different from Rust: whereas Rust has bi-directional Hindley–Milner type resolution, D’s type resolution is (I think) one-directional: return types are deduced from argument types, not the other way around. Also, D has function overloading and Rust hasn’t.

All of that is to say that I’m not sure D’s “least restrictive match” resolution would be decidable in Rust, and even if it is, it seems too complex and hack-ish for Rust.

Instead, I would propose a much simpler system: inout types are only allowed in methods, and their mutability must be the same as the self argument.

This should be relatively easy to implement in the type system (inout is basically one more constraint in type resolution), while covering 90% of use cases (almost all mutability duplication is in getter methods).

So the get_value example would look like:

fn get_value(&self) -> &inout Value {
  self.value
}

Transition and breaking changes

There’s currently a ton of types in the standard library (and, you know, in the entire ecosystem) that have duplicate xxx()/xxx_mut() methods. How would they transition to inout mutability?

The simplest solution would probably be to add the inout keyword to the xxx() version, and eventually deprecate the xxx_mut() version (except for cells and mutexes, where the internal code is indeed different).

This ought to be possible, because adding inout shouldn’t be a breaking change in practice. In theory though, adding mutability to types in existing code might result in subtle behavior changes. inout might need to be introduced as having no effect in existing editions, and be activated by a new edition, so that cargo fix --edition could detect any actual change.

(Or language maintainers could do a crater run to see if any code actually changes behavior in the wild, finds that none does, shrug, and implement the change immediately.)

Conclusion

inout is of those innovative D features that I can’t wait for Rust to integrate.

It solves a common use-case in a very concise fashion, though it needs to be implemented with care to avoid making language semantics extremely complicated.

I hope it or something like it is added to the language eventually.

Discussion on r/rust