Mandatory inlined functions for unsized types and 'super lifetime?

The more I think about this, the more I feel like "return references" are the right answer here.

We currently have the following matrix:

read-only read-write write-only
owned let foo = bar; let mut foo = bar; let foo;
reference &foo &mut foo ???

It feels like what we're trying to do in this thread is fill in the last cell in that matrix. I'll strawman this as &write (or perhaps &uninit).

Concretely, this code works fine because the compiler can do enough analysis to verify that v is initialised before any possible reads:

fn main() {
    let mut r = "blah";

    let v;
    if r.len() % 2 == 1 {
        v = r.to_uppercase();
        r = &v;
    }
    println!("{}", r);
}

But there's currently no formalisation for saying that a reference can be uninitialised, and performing this analysis.

MaybeUninit is working along the same lines, but feels like a it's catering towards a very different kind of analysis. MaybeUninit is saying "Here's some memory, the caller will manage its initialisation out of band" (hence its unsafety).

&write is saying "The compiler can already do initialisation analysis, I'd like it to expand the scope of where it does that analysis".

You can currently write:

fn main() {
    let mut v = String::new();
    println!("{}", upper_if_odd("blah", &mut v));
}

fn upper_if_odd<'a>(s: &'a str, backing_store: &'a mut String) -> &'a str {
    if s.len() % 2 == 1 { 
        *backing_store = s.to_uppercase();
        backing_store
    } else {
        s   
    }   
}

&write would enable writing something like:

fn main() {
    let v;
    println!("{}", upper_if_odd("blah", &write v));
}

fn upper_if_odd<'a>(s: &'a str, backing_store: &'a write String) -> &'a str {
    if s.len() % 2 == 1 {
        *backing_store = s.to_uppercase();
        backing_store
    } else {
        s
    }
}

The caller declares the binding, so has control over scope and dropping, and the &write effectively instructs the compiler to treat upper_if_odd as inlined for the purposes of its initialisation analysis.

By framing this as a kind of borrow rather than a new kind of returned lifetime, and by treating it as an instruction to existing analysis, this feels like a much smaller and more consistent addition the the language.

Its concrete benefits over &mut are:

  • Avoids the boilerplate and overhead of unnecessary Options.
  • Avoids adding extra unsafe with MaybeUninits.

I feel like this is low enough boilerplate to avoid needing extra sugar as @dhm is suggesting around with_slots...

It opens up the possibility for &write impl Trait, and if we end up with auto-generated sum types (a la RFC 2414, and some IRLO threads) these could extend fairly naturally to &write to drive the ergonomics for multiply-sized values in a consistent way, similar to how @petertodd was describing with automatic enum derivation.

I'm not sure I see a clear path to unsized support, though...

1 Like