Allow using Fn(A, B, C) -> _ for unconstrained FnOnce::Output.
F: Fn(A, B, C) -> R desugars to F: Fn<(A, B, C), Output = R> currently.
F: Fn(A, B, C) -> _ could easily desugar to F: Fn<(A, B, C)>.
More generally, we can allow T: Trait<AssocTy = _>, meaning the same as T: Trait.
This form would make it easier to be generic over the return type without having to specify it as another generic parameter (which is worse in type definitions than impls, as it leaks to users).
cc @eternaleye (who suggested it) @nikomatsakis @withoutboats @Centril
Interesting idea =) Is this a problem in practice that needs solving? If we commit to this form, that would also set precedent for using
_as "whatever type goes" in other contexts. I worry that it can make for some unintelligible code. So.. don't we want it to leak to users?That said, I'm cautiously positively inclined towards this idea.
PS: RFC this idea if we like it for greater transparency?
Mazdak Farrokhzad at 2018-01-28 07:46:20
that would also set precedent for using
_as "whatever type goes" in other contextsIt already means that and you can already do this in a function body, just not in signatures, type definitions, etc.
Eduard-Mihai Burtescu at 2018-01-28 08:48:25
It already means that and you can already do this in a function body, just not in signatures, type definitions, etc.
True =)
Do we want to set that precedent in signatures and type definitions?
Would this be valid eventually?
fn identity(x: _) -> _ { x } // Probably not? the two _'s needn't be the same types? // so you would get // :: forall a b. a -> b // instead of // :: forall a. a -> a // and thus it would not compile?What about:
Fn(_, _, _) -> _Mazdak Farrokhzad at 2018-01-28 08:59:51
Personally, I view this as arising from the
Fn(T) -> Usugar having put an 'output' type in the unfortunate position of appearing to be an input type.(The historical reasons for this, AIUI, come down to closure traits predating associated items, and the sugar not getting revisited in that context when the traits themselves were, due to stability issues.)
I'd say that a bright line could be drawn between
_here and the cases @Centril raises based on the following points:- This case does not require global inference, which Rust strenuously avoids
- This case only allows eliding a fully-determined type, and cannot introduce ambiguity
- The elided type can still be named, as
F::Output
Frankly, if I had my druthers,
Fn(T) -> _would be spelledFn(T)orFn<T>(if we had variadic generics), andFn(T) -> Uwould usewhere F::Output = U.eternaleye at 2018-01-28 09:26:18
I'd rather support the more general transformation
S<T, AssocTy = _>->S<T>in signatures than a special case for theS(T) -> Usugar.Note that in bodies
Fn(A, B, C) -> _already has well-defined behavior (_is a new inference variable) and changing it is a breaking change.#![feature(unboxed_closures)] fn f(_: u8) -> u8 { 0 } fn main() { let x: &Fn(u8) -> _ = &f; // Current desugaring, `_` is an inference variable // OK let x: &Fn<(u8,), Output = _> = &f; // Proposed desugaring for signatures // ERROR: the value of the associated type `Output` must be specified let x: &Fn<(u8,)> = &f; }Vadim Petrochenkov at 2018-01-28 10:03:57
@petrochenkov The desugaring change I'm proposing is only valid for trait bounds, not trait objects. I'll edit the description.
Eduard-Mihai Burtescu at 2018-01-28 10:05:57
I do agree there is a problem that needs solving. I'm not sure how I feel about overloading
_with it. I sort of agree with @eternaleye's analysis, though I think the roots of the problem lie in the decision to makefn foo() { }be short forfn foo() -> () { }, which essentially entails thatF: Fn()be short forF: Fn() -> ()for consistency.Specifically, the problem I see is in type definitions; I generally just leave
T: Fn()bounds out of structs because I don't want to define a type parameter for them (e.g., in Rayon).Frankly, if I had my druthers,
Fn(T) -> _would be spelledFn(T)orFn<T>(if we had variadic generics), andFn(T) -> Uwould use whereF::Output = U.I've always assumed we would wind up with
F: Fn<T>, but -- besides being subtle -- it is also perhaps a suboptimal thing because the behavior ofF: Fn<&u8>andF: Fn(&u8)would vary in a kind of subtle way (the latter is more likeF: for<'a> Fn<&'a u8>).As a related aside, I've something thought about
_being a kind of shorthand for introducing a fresh type parameter whose identity doesn't matter (sort ofimplwith no bound). For example,where T: Foo<_>. This kind of fits with that? But not quite. Anyway, I've been worried about people getting confused by the many meanings of_, which has held me back (although I think that it fits with how regions behave; elided regions in signatures obey fixed rules, in bodies they yield inference).Niko Matsakis at 2018-01-29 17:44:44
Is this a problem in practice that needs solving?
Deep in the nightly hole, writing the option lift struct was made extra difficult by the extremely confusing error about the output parameter when I tried to use normal
Fn()syntax:error[E0229]: associated type bindings are not allowed here --> src/main.rs:11:50 | 11 | impl<F:FnOnce(T0)->R, T0, R> FnOnce(Option<T0>)->R for OptionLift<F> { | ^ associated type not allowed hereSo if we ever want to allow implementing function traits directly, some change here would be nice. (Of course, one such change would just be having variadic generics so that
FnOnce<T0>is a normal, stable thing to see and think about, like @eternaleye mentioned.)scottmcm at 2018-01-31 04:44:34
Triage: no changes I'm aware of
Steve Klabnik at 2019-10-18 13:20:28
As a related aside, I've something thought about
_being a kind of shorthand for introducing a fresh type parameter whose identity doesn't matter (sort ofimplwith no bound). For example,where T: Foo<_>. This kind of fits with that? But not quite. Anyway, I've been worried about people getting confused by the many meanings of_, which has held me back (although I think that it fits with how regions behave; elided regions in signatures obey fixed rules, in bodies they yield inference).@nikomatsakis So I think there's a nice framing of this, esp. given
impl Trait:Out of these 2, we only allow the first (
assoc):fn assoc(_: impl Iterator<Item = impl Clone>) {} fn param(_: impl AsRef<impl Clone>) {}Why is that? Well, the argument goes something like this:
fn assoc(_: impl exists<X: Clone> Iterator<Item = X>) {} fn param(_: impl forall<X: Clone> AsRef<X>) {} // unimplementable w/o `for<X>`(though IIUC we may choose to only apply the
forallform toimpl Fn(impl Trait)?)If we consider it somewhat fundamental that we have this
exists/foralldichotomy, then we can apply it to_as well (and pretend it's e.g.impl AnyTypeEver).So if we had a bound like this:
where F: Fn(&[_]) -> Vec<_>We could theoretically interpret it to be:
where F: forall<'a, X> exists<Y> Fn(&'a [X]) -> Vec<Y>(
existsinsideforallbeing significant wrt universes, i.e.Ycan depend on'aand/orX)Now that's beautiful and (ideally) consistent with
impl Trait!EDIT: if we wanted to make it clearer, we should've picked
any/someinstead ofimpl+context-varied-semantics for theimpl Traitsyntax, e.g. the example above would be:where F: Fn(&'_ [any _]) -> Vec<some _>Eduard-Mihai Burtescu at 2022-07-24 17:24:14
Note that the
existsformulation also helps with code like this that works on stable:let _f: &dyn Fn() -> _ = &|| {};Inside a body, every
_is an inference variable, which is why this works today.But what is an "inference variable" other than a body-scoped
<hr/>exists? (which is also howimpl Traitworks when used inside a body, e.g. as the type of aletvariable)We'll probably have to be careful around closures, to not clash with (future) generic closures.
Eduard-Mihai Burtescu at 2022-07-24 17:34:07
FWIW, this is actually a problem when using UFCS with FnOnce. You can't bind associated types in UFCS, but you can't talk about FnOnce without doing so unless you enable the unstable feature.
JP Sugarbroad at 2024-03-18 00:00:11