Why not infer type

What are the rationales for why Rust does not infer the types of const and static variables (requires you to specify them), but does infer the types of other variables?

Compiler splits the notions slightly differently here: it's not constant variables vs other variables, rather, it is local variables vs items.

Constants and statics behave like items (functions, structs, etc):

fn main() {
  println!("{}", X);
  const X: i32 = 92;
}

So, rust runs type inference for locals, but not for items.

This is a design decision -- it totally is possible to have rust which infers types of items withing a single crate.

There are two motivations here:

  • improving readability: in the average case, omitting item types prioritizes code writer over code reader. In the select cases omitting the types can improve readability, but on the whole Rust's solution of always requiring them leads to easier understand code without extra discipline on the part of the programmer.
  • making tooling more powerful: having function, constant & static types specified explicitly makes it possible to compile code in parallel and it helps IDEs to be more resilient to errors.
7 Likes

It seems like if an argument can be made for the benefits of requiring types to be specified for const and static variables, the same argument would apply to all variables.

Not exactly, there's important difference in locality. Local variables are always scoped to a single block of code -- what happens inside a function stays inside the function. Scope of constants & statics can extend to the whole crate. This changes the weight of the argument.

But, to be clear, this is not a no-brainier. Requiring types is also more language-level work. For example, we need the whole impl Trait machinery for that, while C++ gets away with instantiation-time errors. Requiring types also makes expressing certain things impossible: const C: ??? = || X;.

2 Likes

You seem to be asking the same question over and over again, just in slightly different contexts:

By now you may as well be almost sure what the answer is. It's the same every time. Some of these just can't be done, and the rest of them are "only" undesirable, because they add more ambiguity for the reader, and more opportunity for bugs, than they add convenience for the writer.

1 Like

I think this is exactly the point to separate "can't be done / could be done in theory", and to figure out why exactly this can't be done :slight_smile: Imo, each of those questions warrants a language-lawyer deep-dive.

I also specifically don't agree with a polymorphic "explicit is better than implicit" answer. Rust has a ton of implicit/inferred stuff (autoref, autoderef, coercions, hellishly complicated name lookup, coercions, constant promotions, const/binding inference in patters). Why Rust doesn't coerce between i32 and i64, but does implicitly coerce between &i32 and *const i32?

Looking at some of those responses, I am not entirely satisfied with them, to be honest :slight_smile: We can do better, there's an important knowledge hidden there.

This is something I don't know, for example! My guess would be "this'll make type-inference ambigious in some cases, or it changes inference and breaks backwards compat". Instead, looking at the great answer by @quinedot I've learned that this is mostly stalled rut to the lack of manpower. Following the links, I've also found a new recent development I didn't know about before: https://github.com/rust-lang/lang-team/pull/62.

Still, I wish someone has added comparison with C++. C++ has implicit reference, they were added to support operator overloading (with comparison to Rust's eye of Sauron pattern), and google style-guide suggest using pointers to pass out-parameters, because &mut (but not &!) at the call-site is useful.

The core unobvious point of "adding new trait impl should be semver compatible (not breaking inference downstream)" is mentioned. It would be nice to add that this also has perf implications for the compiler, which can use index by method name for type-inference, but needs to consider the list of visible traits for code completion. This answer could also mention that API design pattern where accepting trait-bound argument is more ergonotic, as that doesn't require a trait import. Ie, serde_json::to_string() can be used with : Serialize types without importing the trait.

It would also be interesting to compare traits, which allow one to have genuine method ambiguities, with Java-style interfaces, which result in syntactic method clashes with Go-style duck-typed interfaces, which define ambiguity away.

An interesting unmentioned point here (and the one conspicuously doesn't come up when discussing lifetime inference in C++) is that it's impossible to infere lifetimes for structs (short of global inference).

These two are difference structs, and compiler doesn't have the data to infer which one would be correct.

struct<'a, 'b>S1(&'a str, &'b str);
struct<'a>    S2(&'a str, &'a str);

OTOH, inferring (as opposed to eliding) lifetimes for functions would be possible, but might be a bad tradeoff due to the issues discussed in this question.

3 Likes

I'm afraid I don't follow. To the question "Why not X?", both "X is impossible" and "X is possible but bad" are potentially valid answers, although mutually exclusive. I didn't assert that all of the points having been brought up are impossible to implement, in fact I explicitly mentioned that some of them are, the rest being excluded by-design.

Exactly which of these are impossible is largely irrelevant in this thread, if only for the reason that I did not want to repeat all the previous, correct, and thorough explanations others have provided in the threads I linked to.

You are implying here that mine was such an answer, which is not the case. Explicit over implicit is a safe (as in "less surprise, fewer bugs") default, but there are notable exceptions. Two case studies are local type inference and RAII:

  • Local type inference is a totally general, global feature intentionally restricted to function boundaries, but still included in its restricted form.
  • RAII is also an automatic, implicit mechanism, which is however desirable, because it's kind of special. Usually, running general, arbitrary code invisibly can be confusing; however, memory deallocation and relinquishing ownership of acquired resources in general is something very specific that people forget or overdo (leaking and UaF) all the time. Therefore, it's worth automating, and it is an empirical fact that it prevents way more bugs than the invisibly-inserted drop() function calls themselves cause.

While this is true, the context again implies that the assertion was "it's not possible". I have to disagree with that – when I saw this brought up for the first time, I immediately felt distaste for such a feature, even though I assumed it was something quite easy to do.

In general I do agree that elaborating more on more nuanced advantages and downsides of certain features is useful, however I disagree that not doing so is somehow disingenuous or that it renders answers useless. The general pattern is discoverable in OP's posts, and so is the general tendency of features not being implemented for reasons more subtle than straight-up theoretical infeasibility – since that is the very point of language design.

1 Like

I wonder if those discussions would be more at home on the internals forum. When giving answers here, I usually take the current state of the language as axiomatic because most people aren't willing to fork the language to get their project done.

2 Likes

I think I might be misreading your comment. I've read it as "all these questions have the same answer", not as "the answers to these questions all have the same theme". I agree with the latter, but not with the former.

For me personally, this really depends on the kind of the question. For questions "how do I do X" I feel giving direct, short answer (with maybe some extra pointers footnotes) is most valuable. For more general "how the language works/why the language works this way" I feel it is valuable to go as deep as you can (provided that one has time&desire to do so).

  • understanding the why helps with just using the thing
  • forum posts are read repeatedly by many people, so adding more info here has high leverage

Those are, to my mind, two separate classes of question. Detailed answers about how the language works are absolutely on topic here, as are explanations of what the general design principles for the language are, so that people can predict how other, unfamiliar systems are likely to behave.

At some point, though, those principles need to be taken as given (for backwards compatibility reasons, if nothing else). Discussions about how those might change, or what Rust might be like if a different decision was made years ago, or whether a new bit of syntax might fit in the language aren't really in scope for this forum.

"Why" questions like the ones under discussion sit right on the boundary between these two categories, and can easily stray from one to the other. They can also get somewhat heated if the asker doesn't like the answers they receive (Thankfully not the case here).

3 Likes

Just pointing out, these aren't necessarily mutually exclusive – maybe the reason for the lack of manpower is because the people who could be working on it realize that it would break type inference, and don't find it otherwise useful enough to pursue.

It can be hard to distinguish between "nobody's working on this due to external constraints, despite being a widely desired feature" and "nobody's working on this because nobody really cares for it".

2 Likes