Mutation in const fn


#1

Looking at the RFC and the recently-split tracking issue for const functions, there is hardly any discussion of mutation. However, because &mut T in rust denotes a unique reference, it seems to me that rust is in the unique position to actually allow mutation in const evaluation as long as it is not done through interior mutability.

As far as the compile-time evaluator is concerned, what is different between having a &mut T argument, versus adding a T to both the arguments and the output of a function? What problems could arise if &mut T was allowed?


#2

As a first step, I’d invite you to make a tracking issue to focus discussion there. Beyond that we’ll need to flesh out the design and any issues that may arise in relation to other language areas (possibly const generics) in an RFC. If you want to spearhead that I’d be grateful.

Nothing… I think; it’s akin to State T in Haskell.

You’d lose referential transparency as a general property of const fns; and given that we’re also giving up / have given up parametricity you have no means through parametricity to ensure referential transparency for generic functions.

However, since the use of &mut T is pervasive in Rust, including in the standard library, I’m inclined to make that trade-off to give up referential transparency in general cases but retain it for known concrete types. We still retain determinism and &mut T becomes akin to a State monad.

So I think if we work through any technical problems (cc @oli-obk and @eddyb) we might have, we should allow &mut T in const fns.


#3

With the current behavior, const items get replaced with their values, so there’s no danger in:

const FOO = something;
const BAR = f(&mut FOO);
const BAZ = f(&mut FOO);

(BAR and BAZ are equivalent)

Even if it were encased in a const fn. Ofc, things would be different if they were let instead of const, e.g.:

const BAR_BAZ = {
let foo = something;
let bar = f(&mut FOO);
let baz = f(&mut FOO);
(bar, baz)
};
const BAR = BAR_BAZ.0;
const BAZ = BAR_BAZ.1;

(BAR and BAZ are different)

idk what the stuff about referential transparency means.


#4

An expression is called referentially transparent if it can be replaced with its corresponding value without changing the program’s behavior.

For example, if you write f(x) = g(x) + g(x) you may replace this with f(x) = 2 * g(x) if and only if g(x) is referentially transparent; if however calling g should mutate x then the subsequent call to g(x) may no longer return the same results.

Usually referential transparency is about avoiding side-effects like IO, but mutation is also a side-effect. However, one could posit that mutation through &mut x is not a side-effect because it is stated explicitly in the type signature of functions operating on x: &mut T wherefore the side- part goes away. In any case, I think that &mut T combined with const fns in the context of Rust is mostly benign and useful.


#5

ah.

I think it’s an useful property to have mutating const fn. if you need it to be immutable you’ll just call it with f(&mut A_CONST) so the result is always the same.

with const arguments and stuff, I like to believe referential transparency is something we can live without. it’s also more ergonomic (the alternative is to add an output for the would-be-&mut input, and this is also slower at runtime).


#6

Tracking issue: https://github.com/rust-lang/rust/issues/57349