Default immutability for parameters may lead to API inefficiency

Hi community,

I have been writing software for over 20 years and has recently started learning Rust. In general I agree that variables and fields should be immutable by default, but making paramters owned by the callee immutable by default seems an invitation to broken/inefficient APIs. (Note I am not talking about parameters passed as references.)

I encounterd this while practicing Rust by solving problems on LeetCode (https://leetcode.com/problems/h-index/):

    pub fn h_index(citations: Vec<i32>) -> i32 {

The most common solution requires sorting citations right away, which means one would have to copy the whole vector for no reason. It would make much more sense to have it as mut in the signature.

Practically though, very few people would have learned Rust as their first programming language, so it would be so easy to create this cursed signature by mistake. (Especially if they're designing APIs for other people.)

My thinking is, whether the function wants its owned parameter to be immutable or not is an implementation detail and should not be part of the signature. Ideally, one should be able to add a mut here without affecting the public API*. A analogy is the synchronized keyword in Java which is only useful on the implementation and not on the interface.

(* Not that it matters to this LeetCode problem. I am more thinking about fostering an ergonomic API ecosystem in general.)

Why so? You can just do let mut citations = citations; citations.sort(); - there's no copying here. Mutability of owned values is a property of binding, not of value itself, and bindings can be changed cheaply (this move would trivially be optimized away when possible, and even if it doesn't, it's just a small copying on stack, not the deep copy).

And this is exactly how it is now - for the external observer, adding mut in that signature doesn't change anything at all, and even if it did, one could just rebind the value inside the function, as I said before.

8 Likes

It's not part of the signature.

pub fn foo(citations: Vec<i32>) { … }
pub fn bar(mut citations: Vec<i32>) { … }
pub fn quz(_: Vec<i32>) { … }

all have identical signatures.

Not to mention that even if you put it immutable in the parameter, you can still mutate it:

pub fn h_index(citations: Vec<i32>) {
    let mut citations = citations;
    …
}
8 Likes

Thank you very much for this explanation. I didn't realise one can assign an immutable variable to become mut again. (I originally thought mutability is part of the type like in many other languages, but I guess not. I wish these details are better explained in the book.)

1 Like

&T and &mut T are crucially different types, but let mut and friends (mut or non-mut bindings) are more lint-like safeguards. If you have a non-mut binding, you can't overwrite it or take a &mut _ to it. But changing every non-mut binding of a compiling program to a mut binding doesn't change the semantics of the program.

Incidentally, most Rust learning material presents a "mutable or immutable" landscape, but a more accurate dichotomy is "exclusive or shared". Here's an article on that topic.

5 Likes

Notably, you can move something without it being mut.

The real-life analogy works well for me here. If I buy your book (gain ownership of it) I can scribble all over it if I want, even if you kept it pristine (never mutated it).

2 Likes

Thank you. I think this is the most clear solution to my confusion.

I have to say, it is a risky asymmetry that mut is baked into the type of references but not variables/parameters. This is different from almost all languages I know.

It is also curious that there's no way to say "this function takes ownership of (the life-time of) the argument but must not change it (without copying)".

Many do think that &mut should’ve been spelled &uniq from the start, rather than overloading mut. Some also feel that mut as a binding mode is not worth the hassle and bindings should just be always mutable.

Do note, though, the lexical position of mut in the two cases:

let mut a: i32; 
let b: &mut i32;

The former clearly pertains to the binding, while the latter is part of the type. [1]

This would not be a very useful contract, given that in Rust it’s typically not observable whether a function mutates something it takes by value (copy OR move).


  1. There’s actually another binding modifier, ref, that’s mostly useful in patterns. Speaking about patterns, any binding can be one, including function formal parameters: fn foo((a, b): (i32, f32)). This is also immaterial to the caller, giving a useful rule of thumb: in a formal parameter declaration, everything on left hand side of the colon is only relevant to the function itself, and only stuff on the right hand side is relevant to the caller. ↩︎

1 Like

Which languages are you talking about? In all languages I can think of (that allow mutations) mutability is a property of a binding rather than its type.

Or maybe are you talking about mytability of fields being a property of the field itself rather than inherited from its container's mutability?

1 Like

For the languages I am familiar:

In C++, a const T is a very different type from T (and a value that is already const cannot be easily made non-const).

Java has no language-level way to make arbitrary type immutable, but all the Immutable/Unmodifiable wrappers/helpers in the standard library are separate types from the modifiable variants.

Java and Javascript also have "immutable variables" (final and const respectively), but these applies to the overwrite-ability of the variable, and does not affect the mutability of the value in the variable.

Rust mut/non-mut bindings is like a mix-and-match of these behaviour.

In Rust, it's more about how shared access and mutability go or don't go together.

When you have an owned value, it's the one and only way of accessing it, so you are free to do whatever you want with it. Unconditional mutability because it's not going to interfere with anything else.

The uniqueness of &mut T has already been mentioned and it's a way to guarantee almost the same things as if it was owned. Just for a limited time. Sort of like a compile time lock instead of a runtime lock.

&T, even though it's shared, may also contain mutable parts, but they need some sort of restriction to avoid problematic reference aliasing. That could be an actual mutex, atomic operations, or cheaper options for single-threaded situations.

It's sort of a spectrum from completely unconditional to more limited forms of mutability, inversely proportional to how freely the value can be shared.

Rust’s mut bindings are really exactly like not-final/not-const in a sense.[1] The differences are that in Rust,

  • ability to overwrite a thing almost always[2] comes with ability to get &mut access to the thing and overwrite any of its parts too, and
  • there is no stable “object identity” by default, so “overwrite one of this struct’s fields with a new value” and “overwrite this variable with a new value that happens to have one field different and all the rest the same” are completely indistinguishable in effect.[3]

Together, these things mean that in Rust, “shallow immutability vs. deep immutability” is a very different concept. In Rust, (im)mutability is determined by the kind of ownership or borrow you have, not by the type of the structure owned or borrowed, and it is transitive insofar as the thing you are owning or borrowing uniquely owns its components. In Java and JavaScript, mutability is determined by the type of the value, or the declaration of the binding, you are considering modifiying the contents of — it’s never transitive.


  1. I am very familiar with the semantics of both Java and JavaScript, so I am saying this confidently. ↩︎

  2. Exceptions include Rc/Arc (shared ownership requires lack of &mut) and Pin (not handing out &mut is the whole point) ↩︎

  3. other than the larger overwrite possibly not borrow checking, and the smaller one possibly not being allowed due to field privacy. ↩︎

2 Likes

The Rust mut/non-mut bindings are a match with the Java/Javascript (final/const) that you listed. These are not very consequential, but non-mut bindings do prevent mistakes IME. The fact that you can Copy/Move a non-mut binding in Rust is different, but the thing to pay attention to here is the concept of ownership, which doesn't exist in Java/Javascript.

C++ const like Rust &T is a separate type. But they don't correspond exactly in several other respects. It's best just to learn the Rust rules for &T and &mut T, since they are unique to Rust and fundamental to using the language.

Ah right there's that C/C++ weirdness.

You can have these in Rust as well, this approach is not specific to Java, it's just more forced there.

These are not part of the type, they are a property of the bindings just like mut in Rust. The difference between them and Rust is what I was referring to in the last part of my comment:

The lack of this feature is explained in the Rust book, and I can live with it I guess.

(Not sure if it's exactly what you were thinking of, but) this RFC was accepted, so we may get read-only fields in some sense eventually.

1 Like

I swear I will revisit the implementation soon :sweat_smile: This is 100% on me