As far as I can tell, it’s basically impossible to infer in principle. There is no indication whatsoever that the Test
trait is involved in
fn foo<T>(f: impl FnOnce(T)) {
|x| x.test(f); // error
}
There might as well exist a second trait with a test
method, too, or a struct with an inherent method called “test
”. That’s why for method resolution, the rust compiler wants the receiver type to be known.
In
fn foo<T>(f: impl FnOnce(T)) {
|x| x.test(f); // error
}
the method .test
can not be resolved until the type of x
is known. There is nothing else indicating the type in question, the whole closure is just left unused, and the closure body does only that method call. E.g. if we added another usage, e.g. passing x
to f
conditionally, it compiles
fn foo<T>(f: impl FnOnce(T)) {
|x| if false { f(x) } else { x.test(f) };
}
as a side-note, the other way does apparently not compile, though IMO that case doesn’t look like a fundamental limitation to me, but something that might be possible to be improved.
fn foo<T>(f: impl FnOnce(T)) {
|x| if true { x.test(f) } else { f(x) }; // error
}
Your other code example
fn foo<T>(f: impl FnOnce(T)) -> impl FnOnce(T) {
|x| x.test(f)
}
uses the whole |x| x.test(f)
in a way that can help type inference. The impl FnOnce(T)
return types informs the compiler that x
is of type T
, then method resolution can succeed, and .test
is finally known to refer to <T as Test>::test
.
With the other code example
fn foo<T>(f: impl FnOnce(T)) {
|x| Test::test(x, f);
}
there is no longer any method resolution necessary, so the function call can be used to help type inference. The exact meaning of Test::test
still requires some inference to choose the right trait implementation, but at least the trait is known, so the signature is known, too, and then by passing f
as the second argument, the compiler can connect the dots and infer the type of x
.
There is an argument to be had about whether the compiler could have reasoned in the original code example that there are no other methods called “test
” on any type we have around… well what types do we have to consider though? The thing about Rust’s importing story is that everything is always usable via qualified paths without any explicit “importing” statements (such as use
), and similarly (non-trait) methods can always be called on values of any type even if the type isn’t explicitly imported in any way. So reasoning about the entire set of all methods on all types of our crate all dependencies of our crate sounds like an extremely brittle analysis, prone to breaking once dependencies get updated and define some new APIs in (what ought to be) non-breaking changes.
There’s the <_>::test(…)
syntax, for which I’m - by the way - not entirely sure how and why it works, but it’s like <SomeType>::method
, just with the type left to be inferred I guess. Anyways, its behavior is apparently, than with <_>::method_name
you can only call methods called “method_name” from any traits that are in scope, and it turns out
fn foo<T>(f: impl FnOnce(T)) {
|x| <_>::test(x, f);
}
does compile. But it’s not as bad as the picture painted above: Only trait methods of in-scope traits being considered means that (mostly) only methods of traits we defined locally or imported via use
are considered, so changes to completely unrelated types, or to traits we didn’t import won’t affect out code.