Why is this type annotation needed?


#1
fn main() {
    let x = 100i32;

    let mut op: Option<&i32> = None;      // will not compile without this type annotation
    *op.unwrap();       // Yes, this will panic, but let's just care about the compilation
    op = Some(&x);
}

It’s also confusing that the annotation is only needed if I dereference the unwrapped value, if that asterisk is removed, it compiles successfully (and panics) even without that type annotation. So is there some issue related to the reference mechanism?

Thanks.


#2

The reason is that *, the deref operator, is user-implementable and desugars to a method call like x.deref(...). And in general, Rust will not accept methods being called on not-yet-known types.


#3

The reason it doesn’t compile, because None could belong to infinitely many Option types. So if you say let x = None; it needs to know what’s the T in Option<T> you are referring to.


#4

No it doesn’t, not at that point.


#5

Thanks for your helpful replies, though I still have some confusions about Rust’s type inference mechanism. I’ve thought that, in general, it’s able to “look ahead” in the code to find a proper result, so in a compile-time view, I wonder why the compiler treats that type as not-yet-known. Like this example we may be more familiar to:

fn main() {
    let mut v = Vec::new();  // here, is the type parameter T in Vec<T> not-yet-known?
    let r = &mut v;          // and here a reference is introduced
    println!("{}", r.len()); // here we call a method through the reference
    r.push(123);             // and finally, the type T is determined 
}

And this compiles and runs normally. So what’s the difference?


#6

You’re not calling a method on a completely unknown type here; the type is known to be Vec<_>. (Having a reference here or not is unrelated to the type inference btw.)

Try println!("{}", r[0].foo()) or something like that.


#7

I’ll take a wild aim at this. The thing is Vec::new() or vec![] doesn’t allocate any memory, so it doesn’t need to know the size of T. push() will allocate, and it knows the size of T then, so everything works fine.

Which is a reason why this code doesn’t work:

fn main() {
    let x = Vec::with_capacity(10);   
}

with_capacity will “reserve space” and needs to know the size of T for it to do so.


#8

No, this has nothing to do with allocation, it happens long before, during typechecking. Of course Rust will not compile if T is never determined. But this works:

let mut v = Vec::with_capacity(10);
// later
v.push("blah");

#9

Ah okay. Thanks


#10

OK, I’ve got what you mean, and my Vec example seems indeed an improper one. The conclusion may be: yes, the type inference mechanism can “look ahead” in some way, but this ability seems still limited, as it can only find the “missing part” of a “partial-known” type, but cannot determine something totally absent, though the information is already complete somewhere in the further code. If so, does the compiler has the potential to be improved one day so it can deal with such situations? Can we say so? Or is this actually theoretically impossible?


#11

It is not theoretically impossible, but it would mean more effort required from the compiler, and more processing time. Especially in situations with a lot of generics, this can easily blow up without the programmer realizing why. I think that was the reason to keep inference during typechecking to (essentially) a single pass over the code.


#12

Thanks very much!


#13

#14

Type inference never looks ahead. It simply progresses through the body, using information known at a given point to resolve methods (throwing an inference error if it cannot), and performing unification to update the information known about types of things that appear earlier in the body.

The reason it often feels like it’s looking ahead is because, for many things, the compiler doesn’t need to know anything more than the fact that a type satisfies a trait bound. For instance, calling Clone::clone(&x) is totally fine even if the type of x is currently unknown; the compiler will basically just make a note that it needs to verify later that the type of x does indeed implement Clone. Or something like that.

Well… okay.. There is a notable exception involving closure parameters. In all of the following cases, the compiler eagerly unifies the type of slice with the &[T] that appears in Fn(&[T]) (or at least, the compiler does something vaguely like that) before type-checking the closure body, allowing the call to slice.len() in the closure to compile.

// A closure in return position for a return type that implements a Fn trait.
fn foo() -> impl Fn(&[()]) -> usize {
    |slice| slice.len()
}

fn bar<T>(slice: &[T], func: impl Fn(&[T]) -> usize) {
    func(slice);
}

fn main() {
    // A closure supplied as an argument to something that has
    // a Fn trait bound
    bar(&[()], |slice| slice.len());
    
    // A closure that is immediately saved to a type-annotated local.
    // (if we broke this up into two lines, type inference would fail
    //  at slice.len())
    let foo: fn(&[()]) -> usize = |slice| slice.len();
}