What are the limits of type inference in closures?

Let's have a look at two similar closures:

fn foo(v: &Vec<i32>) -> i32 {
    let x: usize = 0;
    let condition = |idx| v[idx] > 0;
    if condition(x) {0} else {1}
}

fn bar(v: &Vec<(i32, i32)>) -> i32 {
    let x: usize = 0;
    let condition = |idx| v[idx].0 > 0;
    if condition(x) {0} else {1}
}

The first one is successfully compiled, but the second one requires additional type annotations: playground

The problem can be fixed by adding type annotation to the second closures's argument: |idx: usize|. Could anyone explain what is the difference between the closures in foo and bar? Why do I have to help the compiler in the second case ?

UPD: it seems to be a compiler bug: issue

3 Likes

This isn't really a bug, just a limitation.

When your closure is typechecked, the type of idx is a type inference variable (the same sort of thing you end up with when you call .collect() without annotating the output type). This occurs because you assign the closure to an unannotated local; the compiler can only predict the type of closure arguments in certain scenarios (the closure must be passed in as a function argument with known Fn bounds, or it must be assigned directly to something annotated as fn(T) -> U or similar).

There's plenty of things you cannot do with type inference variables:

  • Access anything through field or method syntax (x.a, x.method())
    • Methods are forbidden because it's impossible to discover the signature of the method otherwise.
  • Dereference them (*x)
  • Index into them (x[index])
  • Call them (x())

(I'm not sure why those operators are forbidden; it might have to do with the fact that they all normally auto-deref, a behavior that would have to be suppressed on type inference variables)

Some things you are allowed to do with type inference variables are:

  • You can supply them as arguments to functions. (function(x))
  • You can use them as an index (vec[x]). The output is another type inference variable.
  • You can use ==, >, etc. on them.
  • You can match on them and use them as if conditions

Your first closure creates v[idx]—a type inference variable, and compares it using >. This is allowed. When the type checker checks this, it simply adds a constraint that the unknown type implements PartialEq<i32>[^1]. Later, once it reaches the call to condition(x), it can determine that idx is a usize, v[idx] is an i32 and it can verify that it implements the trait.

Your second closure creates v[idx]—a type inference variable, and tries to access the 0 field. This is forbidden.


[^1]: Technically the type of the i32 there is another type inference variable since 0 can be any integer type, but the mechanics of type inference for literals are complicated and not worth the effort to discuss.

3 Likes

Thank you for such detailed answer! If you are interested in further discussion about making limitations less strict, here is the issue.