RFA-519 · Case file with fixtures · Case 491 of 694 · Compiler evidence
Manual Implementations of Rust Fn Traits Remain Unstable
Stable Rust lets closures implement Fn traits through compiler-generated machinery, but user-written implementations require unstable features. Prefer a closure or an ordinary named call trait in stable APIs.
- 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
- Stable Rust exposes compiler-generated Fn implementations for closure types but does not stabilise the manual rust-call ABI as a user implementation surface.
- First discriminating check
- Use a closure, impl Fn return, boxed callable, or ordinary named trait unless a pinned nightly experiment is an explicit compatibility choice.
I manually implemented FnOnce<(u32,)> for Increment. Stable Rust emitted E0183 together with E0658 diagnostics for the experimental call ABI and function-trait machinery.
The failing fixture shows that the traits are visible in std::ops, yet their implementation syntax is not a stable user extension point.
Closure callability uses compiler-generated implementation
Closure expressions create anonymous types. The compiler determines whether each closure implements Fn, FnMut, or only FnOnce based on how it captures and uses values.
The closure Reference explains this call-trait behaviour. Stable users create closures and use the resulting implementations; they do not normally write the impl blocks themselves.
Visibility of a trait does not guarantee stable manual implementation.
The repaired fixture uses a closure
The repaired fixture captures an increment and creates move |value| increment + value. It calls the closure and verifies the result.
Because the captured u32 is copied and not mutated or consumed, this closure supports a reusable call capability. More complex captures may narrow the implemented call traits.
The compiler manages the private representation and call ABI.
Ordinary traits work for named callable objects
If I need a public named type with custom call-like behaviour, I define a stable trait such as Transform<Input> { type Output; fn apply(&mut self, input: Input) -> Self::Output; }.
Implementors then use ordinary stable Rust. Call sites write .apply(value) instead of parentheses, but the API can express errors, configuration, and ownership clearly.
Syntax convenience is rarely worth coupling stable production code to an experimental ABI.
The three Fn traits express capture use
FnOnce permits consuming captured state and can be called at least once. FnMut permits mutation through exclusive access. Fn supports calls through shared access.
Bounds should request the weakest capability the caller needs. A function that calls once can accept FnOnce; requiring Fn unnecessarily rejects stateful or consuming closures.
The FnOnce documentation describes the relationship among these traits.
Nightly features are a deliberate product choice
The compiler suggests fn_traits and unboxed_closures, and the extern "rust-call" ABI also participates. Enabling them requires a nightly toolchain and accepts instability.
For experiments or compiler-adjacent work this may be appropriate. For a library promised on stable Rust, it changes the compatibility contract and can break on toolchain updates.
I record the toolchain channel and test it explicitly rather than smuggling feature use through environment tricks.
Multiple diagnostics describe one unstable surface
The fixture's first diagnostic is E0658 for the ABI, followed by another unstable-feature message and E0183 for the manual implementation. Recording only the first line can hide the user's actual intent.
I keep E0183 and the related feature errors together as one case. The repair removes the whole unstable mechanism, not one keyword at a time.
This is why executable evidence is stronger than a copied historical example.
Returning impl Fn hides the generated closure type
A function can return impl Fn(u32) -> u32 when it creates one concrete closure type. This gives callers normal call syntax without naming the anonymous representation. Different branches must still converge on one hidden type, so boxed trait objects may be needed for runtime-varying closures.
I choose impl Fn for one statically known callable and Box<dyn Fn...> for heterogeneous runtime selection. Capture lifetimes and Send or Sync bounds follow how the callable is stored and moved.
Performance claims need measurement
Static closure calls can often be inlined, while trait-object calls use dynamic dispatch. Allocation depends on the chosen container, not on Fn alone. I benchmark the real hot path before replacing a clear boxed callback with complex generics.
The stable repair should remain driven by API and workload evidence, not a belief that manual Fn implementations unlock automatic speed.
My E0183 checklist
- Am I manually implementing
Fn,FnMut, orFnOnce? - Does the build promise stable Rust support?
- Can a closure express the callable behaviour directly?
- Would an ordinary named trait provide a clearer public API?
- Which call capability does the consumer truly require?
- Are captures read, mutated, or consumed?
- Have I accounted for related E0658 and rust-call ABI diagnostics?
- If nightly is intentional, is the toolchain pinned and tested?
The core principle is that stable closure call syntax is compiler-provided machinery. I use closures for anonymous callable values and ordinary traits for named stable abstractions unless experimental compiler work is the explicit goal.