Inference or not?

I was sure that if i write something

let x = 3;

compiler assigned i32 type to x.

Now, if i try:

println!("{}", x.Pow(3));

compiler complains:

can’t call method pow on ambiguous numeric type {integer}

suggesting:

help: you must specify a type for this binding, like i32

Ok, thx for the help, but why x is “ambiguous”? Does not the compiler assign a “sure” type to x?

thx for any hint.

Let’s check

Ok but my code was
this
why it does not work?

The assert eq caused it to infer what the type had to be, since both parameters are required to be the same type. It’s basically a very indirect way to declare the type.

Not entirely true, assert_eq!(t, u) requires T: PartialEq<U>, and that is all (not T: PartialEq<T>). where t: T, and u: U.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9824083eabbab878e578361a55614d71

Type inference knows that there is only 1 implementation of ParitalEq for integers, so it uses this to determine your types when you supply one type. But this falls over when you use a custom PartialEq parameterized on multiple integer types. Then Rust then defaults to i32, even if that is not an option.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3ac33be94c15da2b02b2fce09029be6f

Interesting… but
when i write
let x = 3;

what is x? The question rises cause i’d like to know how infernce works under the hood…

thx again.

In most cases when Rust can’t infer the types for an integer, it defaults to i32, which is what it does here. Sometimes it errors out because of ambiguity, as you saw.

using assert to compare with i32 tells compiler that its i32. Without that it doesn’t know.

I see! now your question is more clear. First of all, I love these kind of small little things that will hopefully lead to better understanding of the whole system, so thanks for asking this question :slight_smile:

Rust’s type inference is based on Hindley-Milner algorithm, which (based on my understanding) matches the intuition of how inference should be made given usage and type assumptions that already exist (I cannot claim to understanding it deep enough though now).

Here’s my intuitive justification;

Given

fn main() {
    let mut v = vec![]; // <-- no other type comes before it
    v.push("hi"); // <-- this resolves v to be of type `Vec<&str>`
}

Back to your example

fn main() {
    let x = 3; // <-- fresh new variable x and literal 3 of type i32 with let binding
    x.pow(3); // <-- hmmm ?
}

Here pow has been defined for many integral types i8, i16, i32 etc. and since 3 can belong to more than one of those types (opposite to earlier vec example), then the type inference is confused so the compilation error.

It seems that type inference can take a hint though from assert_eq!(x, 3i32) and exclude other type possibilities. I don’t know at what extent ones can makes these hints. For example, I cannot explicitly exclude other type possibilities using assert_ne!(x, 3i8), assert_ne!(x, 3i16), ... (which kind of makes sense! :neutral_face:)

@notriddle @Fiedzia please let me know if the following argument is valid or not.

When I output MIR of

fn main() {
    let x = 3;
}

it shows

fn main() -> () {
    let mut _0: ();                      // return place
    scope 1 {
    }
    scope 2 {
        let _1: i32;                     // "x" in scope 2 at src/main.rs:2:9: 2:10
    }

    bb0: {                              
        StorageLive(_1);                 // bb0[0]: scope 0 at src/main.rs:2:9: 2:10
        _1 = const 3i32;                 // bb0[1]: scope 0 at src/main.rs:2:13: 2:14
                                         // ty::Const
                                         // + ty: i32
                                         // + val: Scalar(Bits { size: 4, bits: 3 })
                                         // mir::Constant
                                         // + span: src/main.rs:2:13: 2:14
                                         // + ty: i32
                                         // + literal: Evaluated(Const { ty: i32, val: Scalar(Bits { size: 4, bits: 3 }) })
        StorageDead(_1);                 // bb0[2]: scope 0 at src/main.rs:3:1: 3:2
        return;                          // bb0[3]: scope 0 at src/main.rs:3:2: 3:2
    }
}

So it seems that x will be of type i32 (in _1 = const 3i32;), because right before the scope ends, the type inference doesn’t have any other options and this is in contrast with having x.pow(3).But this is not a contradiction with saying that the default type of x is i32. It is so if and only if there’s no other possibilities.

3 Likes

I don’t think assert_eq! is acting as a type hint directly, it is doing it through the impl of Eq. Thus assert_ne! would also do the same thing – assert_ne!(x, 3i8) would not tell the compiler that x is not an i8, it would tell the compiler that x is an i8 because that is what would be required to make the != compare work.

4 Likes

Right! makes sense, only if we had Neq trait and Eq was defined based on Neq (which doesn’t match our intuitive for equality).

Neq wpuld add a new trait for almost no additional value, so that would have been a bad way to design the api. Remember, Eq means the values of the types are equal it says nothing of the types themselves. It just happens to be the case that integers are special cased to allow for better type inference.

Really interesting. Rust is amazing…

That wouldn’t solve the problem either, because a Neq trait would still require the types to be specifically related for the ne operation to be defined. What you’re really thinking of is type inequality, not a trait for value inequality. And to do that, you could use something analogous to a function which requires two of the same type fn test<T>(a: T, b: T), which will produce a compile error if you pass it two unequal types. Rust doesn’t currently have a way to express inequality this way fn uneq<T, U>(a: T, b: U) where T != U. If rust had such a function, you could call it to tell the compiler that a value is not of some type.

If we had specialization, we could do

#![feature(specialization)]

trait TypeEq<T: ?Sized> {
    fn get() -> bool;
}

impl<T: ?Sized, U: ?Sized> TypeEq<U> for T {
   default fn get() -> bool { false }
}

impl<T: ?Sized,> TypeEq<T> for T {
    fn get() -> bool { true }
}

fn eq<T: ?Sized, U: ?Sized>() -> bool { <T as TypeEq<U>>::get() }