RFA-573 · Case file with fixtures · Case 545 of 694 · Compiler evidence
Rust Closure Arity Must Match the Required Fn Contract
A closure's parameter list is part of its callable type. Match the callback protocol instead of reaching around inputs through captured state.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The closure body was treated as enough to define compatibility, while its parameter tuple is also part of the required callable protocol.
- First discriminating check
- Read the exact Fn bound and iterator item shape, then accept every supplied argument explicitly even when one is deliberately ignored.
A callback contract includes how many arguments each call supplies. If a function requires F: Fn(u32) -> u32, a closure written as || 42 is not close enough because it accepts no input. Rust reports E0593 at the boundary.
The failing fixture passes a constant-producing closure to apply, which invokes its operation with one u32. The closure body could ignore the number logically, but the parameter list must still receive it.
Arity is part of the callable protocol
I read Fn(u32) -> u32 as a small interface: the caller may invoke the value repeatedly, each invocation provides one unsigned integer, and the invocation returns one unsigned integer. The closure must implement that whole interface.
The standard Fn trait represents arguments as a tuple internally. Source syntax makes a one-argument call look ordinary, but zero arguments and one argument are distinct tuple shapes. E0593 is therefore a type mismatch, not a complaint about whether the body happens to use a value.
If the correct operation ignores the input, I write |_| 42. The underscore makes acceptance explicit. If the result depends on the input, I give it a domain name such as |attempt|, |status|, or |byte|.
The repair follows data through the callback
The repaired fixture accepts value and adds one. The fixture asserts the result, recording both the callback shape and the intended transformation.
This is safer than capturing another value with the same meaning. Suppose apply passes the latest retry count, while the closure captures an earlier count from its environment. A zero-argument design may compile in a different API but silently operate on stale state. Passing the value through the contract makes data flow visible.
The closure reference separates parameter and capture behaviour. Parameters arrive on every call. Captures become fields in the closure's generated anonymous type. I decide between them based on ownership and meaning, not only on which version satisfies the compiler.
Arity errors commonly appear through iterator adapters
Iterator callbacks differ in what they pass. map receives one item, filter commonly receives a reference to an item, and some methods over key-value pairs still pass one tuple item rather than two independent arguments. Errors can look surprising when a closure writes |key, value| but the adapter supplies |(key, value)|, or the reverse for another API.
I inspect the method's actual trait bound and the iterator's Item type. IDE hints can help, but writing a temporary named function with an explicit signature is often the fastest way to expose the contract.
Framework callbacks can add extractors, request state, or context arguments across releases. I check the versioned primary documentation rather than copying a closure from a similar-looking API. Type inference then verifies the exact local version.
Arity is separate from Fn, FnMut, and FnOnce
After the argument count matches, Rust may report that a closure only implements FnMut or FnOnce. That is another dimension. Arity describes inputs; the callable trait describes what invoking the closure does to its captured environment.
A closure that only observes captures can implement Fn. One that mutates captured state may require FnMut. One that moves a captured value out may only support FnOnce. I solve these diagnostics independently so I do not hide an ownership decision while fixing a parameter list.
For public callback APIs, I use the weakest callable requirement that the implementation truly needs. If I call once, FnOnce accepts the broadest useful set of closures. If I call repeatedly without exclusive access, Fn is a real promise and constraint.
My E0593 checklist
- What exact
Fn(...) -> ...bound does the caller require? - Is a pair supplied as two arguments or as one tuple argument?
- Should an unused supplied value be written explicitly as
_? - Did captured state accidentally replace an intended parameter?
- What is the iterator or stream's precise item type?
- Does the callback need
Fn,FnMut, or onlyFnOnce? - Did a dependency version change the callback signature?
- Does a small typed function make the intended protocol easier to see and test?
The core principle is that a callback is a typed protocol, not only a block of executable code. Its argument count and shapes define how the caller delivers data. I fix E0593 by aligning that protocol and then review capture ownership as a separate design choice.