The closure parameter type needs to be declared, can the compiler improve this?

The Rust code is as follows:

trait Test: Sized {
    fn test(self, f: impl FnOnce(Self)) {
        f(self)
    }
}
impl<T> Test for T {}

fn foo<T>(f: impl FnOnce(T)) {
    |x| x.test(f);
}

Compile Error:

error[E0282]: type annotations needed
 --> src/lib.rs:9:6
  |
9 |     |x| x.test(f);
  |      ^  - type must be known at this point
  |
help: consider giving this closure parameter an explicit type
  |
9 |     |x: _| x.test(f);
  |       +++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` due to previous error

x is passed into f and is treated as a parameter of the closure f, so the type should naturally be T.

Is this error impossible to infer type in principle, or can the Rust compiler improve?

By the way, changing foo function to this can be compiled:

fn foo<T>(f: impl FnOnce(T)) {
    |x| Test::test(x, f);
}

Or changing to this can be compiled too:

fn foo<T>(f: impl FnOnce(T)) -> impl FnOnce(T) {
    |x| x.test(f)
}

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.

4 Likes

AFAIK it's the same thing that lets x.clone() find Clone::clone -- it looks at the used traits currently in scope for something with a clone method, even though the type of x is not yet know. It just skips the inherent method part of resolution.

(That's why x.map(|y| y + 1) might give an error about Iterators even if you expected x to be an Option.)

3 Likes

I still don't know why it is known to refer to <T as Test> trait.

Is the same for the code below?

fn foo<T>(f: impl FnOnce(T)) {
    |x: T| x.test(f);
}

And how do they avoid the conflict about:

I mean why can the Rust compiler do <T as Test> automatically?

I think it wants to build the candidate list before trying to resolve things. That's just a guess though; once you use . more generally, rustc really wants to know what the thing on the left is already.

<_>::test and <_ as Trait>::test first look for traits with a test method, regardless of the arguments (including any receiver). I think that's the key difference: this step does not depend on the type of any arguments, while method resolution depends on the type of the receiver. If the trait search is ambiguous it will error, but here it just finds Test.

Once it knows you're calling a <_?0 as Test>::test (with arguments (_?0, impl FnOnce(_?0))), it can look at the arguments you pass in and decide they are test(_?0, impl FnOnce<T>), and infer that it's <T as Test>::test(T, impl FnOnce<T>) from there.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.