1

Consider the following code:

fn main() {
    let x = Overload::overload();
    let y = Overload::overload();
    magic(x);
    more_magic(y);
}

trait OverloadTrait<T> {
    fn overload() -> T;
}

struct Overload;

impl OverloadTrait<i32> for Overload {
    fn overload() -> i32 {
        42
    }
}

impl OverloadTrait<u32> for Overload {
    fn overload() -> u32 {
        69
    }
}

fn magic(i: u32) {
    println!("magic {}", i);
}

fn more_magic(i: i32) {
    print!("more magic {}", i);
}

How can rust compiler determine which function to use based on the return value type? What's the process the compiler goes through to distinguish these 2 functions and calls the appropriate one based on later use of a variable?

Quest
  • 2,764
  • 1
  • 22
  • 44
  • 1
    well, that is why the types are there, magic and more_magic need an specified type, so the compiler just matches the instances of the Overload struct to what it need to fulfill those types... – Netwave Nov 24 '21 at 11:11
  • @Netwave so in case someone would write if Overload::overload() == 4 that would be implicitly determined to be 4i32 thus the i32 overload would be used, is this correct? Is there any possible way where this could lead to potential flaws in the code? – Quest Nov 24 '21 at 11:13
  • yes exactly like that. There shouldn't be any flaw, or at least the compiler will complain about it when he cannot know the type. In that case you would have to disambiguate the calls. – Netwave Nov 24 '21 at 11:15
  • Seems about right.. With println!("x: {}, y: {}", x, y) the compiler complains that it cannot infer type. How does it work anyway? When the compiler sees a variable x, it delays the decision process of its type until it is actually used. Does it use any kind of a placeholder to know it knows variable with that name but not yet its type? – Quest Nov 24 '21 at 11:19
  • I do not really know about the resolution rules of the compiler per se. – Netwave Nov 24 '21 at 11:21
  • The exact algorithm used for type inference is the implementation detail of the compiler. There are many cases where it's obvious to a human which type should be used, but the compiler doesn't guess it - in which case you can provide explicit types using [type ascrpition](https://stackoverflow.com/questions/36389974/what-is-type-ascription) or the [turbofish](https://turbo.fish/). The only guarantee provided by the compiler is that future versions of the compiler won't reject currently valid code - i.e. inference is only allowed to become more powerful, not less. – user4815162342 Nov 24 '21 at 11:26
  • 2
    Does this answer your question? [How does Vec::new() know what is the requested element type?](https://stackoverflow.com/q/69542319) – Sven Marnach Nov 24 '21 at 12:07
  • 1
    @user4815162342 There actually isn't even the guarantee that type inference will only ever become more powerful. Sometimes requiring new type annotations is [an explicit exemption in Rust's stability guarantee](https://blog.rust-lang.org/2014/10/30/Stability.html#what-are-the-stability-caveats). – Sven Marnach Nov 24 '21 at 12:11
  • There is documentation about trait resolution in the rustc guide [(see docs)](https://rustc-dev-guide.rust-lang.org/traits/resolution.html) – Jerboas86 Nov 24 '21 at 12:20
  • @SvenMarnach I guess you're technically correct, but as far as I'm aware that's done **extemely rarely**, and even then only to fix safety holes or outright bugs that allowed something that should never have been allowed. The compiler team has the "crater" tool at their disposal with the express purpose of preventing accidentally backward-incompatible changes by testing the new compiler with a huge number of open-source crates. If the compiler were truly free to break existing code, Rust would simply not be a backward compatible language. – user4815162342 Nov 24 '21 at 12:38
  • @user4815162342 There have been some library changes in the past that required new type annotations. There also have been changes that were rejected because crater runs indicated it would require too many changes. But in the end, Rust probably isn't a backwards compatible language in the strictest sense – it's just rare that old code needs any changes, and if it does, the changes are easy to apply. – Sven Marnach Nov 24 '21 at 14:09
  • 1
    @SvenMarnach What you say is true, or I have no reason to doubt it. I'd still point out that someone reading your comment without prior Rust experience might get very wrong ideas regarding Rust's backward compatibility. In Rust it is quite normal to encounter crates that haven't been updated in years, and they compile just fine. The backward-incompatible changes you mention are sufficiently rare or apply to rare situations, that my original comment was correct for all practical intents and purposes. (Except the word "guarantee" is prehaps too strong, it's more of a promise I guess...) – user4815162342 Nov 24 '21 at 14:31

1 Answers1

4

In a very general sense, rustc can look at uses of a particular value to "constrain" the types it can be. If, at the end of this process, there isn't a concrete type known, you get an error:

fn foo() {
  let x = vec![];  // error, type is Vec<_>
}

However, you could add usage to the function to "provide information to the compiler":

fn foo() {
  let mut x = vec![];  // must be a Vec<&str>
  x.push("hello");  
}

It happens that, in Rust, functions are considered "boundaries" for type inference, so you can't write something like:

fn accepts_any_vec(mut v: Vec<_>) {
  v.push("hello");
}

even though there's enough info here to work out the concrete type. This is a design decision, more than anything, and helps prevent details about the implementation of a function "leaking" unintentionally.

In your case, because you pass x: Overload::<_>::overload() into a function that expects an i32, that adds the bound:

i32: Overload::<_>::overload()

This case is pretty trivial, but it gets more complicated when you add things like associated types and other Rust goodies.

For more information on how this works, I'd recommend looking at the chalk project, which is an attempt to describe trait resolution in terms of logic rules (like the one above). It's not currently used in rustc but there are plans to switch over to it, though I'm not sure the status of that.

For more info on the style of type inference Rust does more generally, have a look at "Hindley-Milner type inference", it's not exactly what Rust uses, but it's pretty close.

cameron1024
  • 9,083
  • 2
  • 16
  • 36