Parameterisation over mutability

A recent post on the /r/rust subreddit brought to light a percieved problem with the current mutability system: should mutability be able to be parameterised? Right now the standard library is full with methods with immutable and mutable versions with the same functionality, so this could easily be seen as a useful change.

The syntax could be something like this:

impl<T> Vec<T> {
    pub fn get<<m>, 'a>(&'a <m> self, index: uint) -> &'a <m> T {
        &<m> self.as_slice::<<m>>()[index] // the ::<<m>> would probably be inferred
    }
}

(Yes, this syntax could do with a lot of improvement.) Traits like Deref/DerefMut could also probably be merged into a single trait with a mutability parameter. There is a large number of such traits in the standard library, so this could be quite good for reducing the number of functions, methods, and traits.

5 Likes

I believe it is possible if you restrict the method to treating the mutability-parameterized reference as if it were unique but not mutable (never usable directly as &mut). I also remember finding a problem with this approach, but I can’t remember precisely what it was… possibly nothing!

Such methods would have to be written in a very restricted manner due to the aliasing constraints. It may just be easier to re-introduce &const, instead of a weird parameterization system (where both & and &mut can be coerced to &const), but then one needs to summon the mutpocalypse again…

The &const Rust previously had doesn’t solve the problem, since you can’t use fn find(&const self, key: uint) -> &const X to get either a & out (if called on an & receiver) or a &mut (for a &mut receiver), you always get a &const out.

This was discussed on reddit in some depth previously.

After thinking about this some more, I came up with an alternative design that doesn’t even need the language to be extended (except maybe adding a lang item). All you really need is a Reference<'a, T> trait that is implemented only by &'a T and &'a mut T. It requires one method, fn from_ref(r: &'a mut T) -> Self. I tried setting this up here, but unfortunately the UFCS explicit self syntax doesn’t seem to support generic types. (I’ve no idea if that’s intentional—a quick naïve search on Github didn’t reveal an issue about it.)

1 Like

Isn’t this clearly something that can be put on the stack for things to do after 1.0?

If mut parameterization is introduced, we can deprecate .as_mut_slice() in favour of .as_slice() and so on.

I don’t think changing the behaviour of as_slice is fully backwards compatible: currently, (&mut [T])::as_slice returns a &[T], but would return a &mut [T] with parameterised mutability, which could change which trait impls are selected if a trait is implemented separately for both & and &mut (so, somewhat of an edge case).

That is the only way I can think of that will cause to stop compiling/change behaviour; the largest impact would be on programs that currently don’t compile (like, foo.as_slice()[0] += 1 becoming valid), which is fine from a BC viewpoint.

I’m hugely in favour of parameterized mutability.

Some pros:

  • Reduced amount of physical code to maintain, less duplication of logic
  • Smaller number of methods in APIs, less combinatoric explosions
  • Reduced binary size (in contrast to generics in general, mutability is simply a compile time notion)
  • More natural chaining of mut/immut method calls; this is harder to do with just macros

Some cons:

  • more syntax and/or sigils; general increased language complexity
  • potential increase in compile time from need to determine if a valid combination of mutabilities exists

Some other syntaxes to consider:

fn get <'a, "m> (foo: &'a"m Foo) -> &'a"m Bar;

Concise, matches lifetimes, sigil heavy, possibly precludes generic mutability of values, mutability is named so that it cascades in a predictable manner.

fn get <'a> (foo: &'a mut? Foo) -> &'a mut? Bar;

More verbose, matches mut’s use everywhere else, a bit sigily, permits generic mutability everywhere, mutability of one ref/value holds independent of others, potentially tricky.

Regardless of the precise syntax and implementation, I think the compiler should always try to infer immutable first, and then mutable if not possible. If neither is possible it should of course be a compilation error. Unnecessary generic mutability should also probably be a warning, if possible.

1 Like

Is this something that could be done somehow with HKT later … generalised reference types? imagine something like…

fn foo<R<>, T>( x:R<T>) {.....}
fn foo<R:Ref, T>( x:R<T>) {.....}   // some R<T> can be a type wrapping &T or &mut T

I’m not sure how that would play out, and people don’t already use template-template parameters to do it in C++ (that i know of)

would it help or hinder to use abstracted pointer types to manage lifetimes & mutability together… you’d probably need a lot of control over their coercions… maybe it is a case where more inbuilt syntax would be much saner

This actually would possibly be better if it were a three-way choice—by reference, by mutable reference, or by move. There are just so many traits, methods, and functions that have immutable, mutable, and move variants, and even ones that should have move variants but don’t (e.g. Deref*, Index* &c.). Using a far-too-sigil’d syntax:

pub trait Deref<?m, Result> {
    fn deref<'a>(?m 'a self) -> ?m 'a Result;
}

// ?m can be const, mut, or move
impl<T> Deref<const, T> for Rc<T> { ... }
impl<?m, T> Deref<?m, T> for Box<T> { ... }

This could pose some problems for anything that wants to, for example, implement Deref<const> and Deref<move> but not Deref<mut>.

1 Like

Related thoughts, similar to some of the ones above.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.