Why can't Rust do more complex type inference?

  • A+
Category:Languages

In Chapter 3 of The Rust Programming Language, the following code is used as an example for a kind of type inference that Rust cannot manage:

fn main() {     let condition = true;      let number = if condition { 5 } else { "six" };      println!("The value of number is: {}", number); } 

with the explanation that:

Rust needs to know at compile time what type the number variable is, definitively, so it can verify at compile time that its type is valid everywhere we use number. Rust wouldn’t be able to do that if the type of number was only determined at runtime; the compiler would be more complex and would make fewer guarantees about the code if it had to keep track of multiple hypothetical types for any variable.

I'm not certain I understand the rationale, because the example does seem like something where a simple compiler could infer the type.

What exactly makes this kind of type inference so difficult? In this case, the value of condition can be clearly inferred at compile time (it's true), and so thus the type of number can be too (it's i32?).

I can see how things could become a lot more complicated, if you were trying to infer types across multiple compilation units for instance, but is there something about this specific example that would add a lot of complexity to the compiler?

 


There are three main reasons I can think of:

1. Action at a distance effects

Let's suppose the language worked that way. Since we're extending type inference, we might as well make the language even smarter and have it infer return types as well. This allows me to write something like:

pub fn get_flux_capacitor() {   let is_prod = true;    if is_prod { FluxCapacitor::new() } else { MovieProp::new() } } 

And elsewhere in my project, I can get a FluxCapacitor by calling that function. However, one day, I change is_prod to false. Now, instead of getting an error that my function is returning the wrong type, I will get errors at every callsite. A small change inside one function has lead to errors in entirely unchanged files! That's pretty weird.

(If we don't want to add inferered return types, just imagine it's a very long function instead.)

2. Compiler internals exposed

What happens in the case where it's not so simple? Surely this should be the same as the above example:

pub fn get_flux_capacitor() {   let is_prod = (1 + 1) == 2;    ... } 

But how far does that extend? The compiler's constant propagation is mostly an implementation detail. You don't want the types in your program to depend on how smart this version of the compiler is.

3. What did you actually mean?

As a human looking at this code, it looks like something is missing. Why are you branching on true at all? Why not just write FluxCapacitor::new()? Perhaps there's logic missing to check and see if a env=DEV environment variable is missing. Perhaps a trait object should actually be used so that you can take advantage of runtime polymorphism.

In this kind of situation where you're asking the computer to do something that doesn't seem quite right, Rust often chooses to throw its hands up and ask you to fix the code.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: