Inferred function arguments

Taking this as an example:

fn test(a: &mut _) {
    *foo = 32_usize;
}

This currently does not compile.

error[E0121]: the placeholder `_` is not allowed within types on item signatures for functions
  --> src\main.rs:84:17
   |
84 | fn test(foo: &mut _) {
   |                 ^ not allowed in type signatures
   |
help: use type parameters instead
   |
84 | fn test<T>(foo: &mut T) {
   |        +++         ~

However, we don't necessarily want generics here. The type of the function argument foo can totally be inferred from its first assignment, 32_usize. And indeed the same thing works for closures today:

// This compiles
fn test_closure() {
    let closure = |foo: &mut _| {
        *foo = 32_usize;
    };
}

You might think this is trivial - why not just specify the function argument directly? There might be cases where the type of foo is hard to name, unnameable, or generated from a macro.

My use case specifically is that I have a macro that defines a struct with a run method implemented. The implementation of the macro is basically dark magic, and the type of the arguments of that run method depends on the content of the macro and can be extremely complicated or unstable. I would like to allow the users to define a bevy system for this macro and store that argument as a Resource, so it would be nice if the compiler can figure out the type of Res<_> for me.

fn run_once(state: Res<_>>) {
    let d = commands! {
        // stuff
    };
    d.run(state);
}

The diagnostic might should be able to infer the type to tell you what type to use, as is already done for the error for -> _, but this isn't likely to ever be allowed. The reason is that function signatures serve as abstraction boundaries, and to do so, the full type signature needs to be present as a contract between caller and callee.

If a closure infers the type correctly, then you should be able to just use that closure for the bevy system directly, rather than introducing a fn item. It's not quite as nice, but it's at least functional.

Alternatively, change the macro such that it allows naming the type involved. Either make a separate macro to be used in type position, or change the macro to be used at module scope and define a usable type alias in addition to a fn to get the value assigned to let d here.

12 Likes

The design decision Rust made here is that "all function signatures must be annotated, while variable types may be inferred". I'd have to dig up a link, but IIRC it is mentioned somewhere in the book. To phrase it more explicitly: eliding types in function signatures reduces the capability of type inference within the function.

Note that type inference on closures is not magic either, and it is not uncommon for the compiler to ask you to specify some type. The main difference between functions and closures is that closures are called or used within the context they were created, which provides type information. For functions, the equivalent would be to draw in type information of how they are called. As already pointed out, this is an obvious abstraction boundary violation. Of course you could build inference rules that only work within a function, but you'll notice its limits rather quickly.

3 Likes

Couldn't the compiler allow _ just to suggest types in diagnostic? Like typed holes in ghc.

That is, this code

fn test(a: &mut ()) {
    *a = 32_usize;
}

Gives this error:

error[E0308]: mismatched types
 --> src/lib.rs:2:10
  |
2 |     *a = 32_usize;
  |     --   ^^^^^^^^ expected `()`, found `usize`
  |     |
  |     expected due to the type of this binding

But if it were an _, it could suggest the changing the type of the signature to usize instead - inferring the function signature as if the function were a closure.