Allow using Fn(A, B, C) -> _ for unconstrained FnOnce::Output.

9c55464
Opened by Eduard-Mihai Burtescu at 2024-03-18 00:00:11

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

  1. 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

  2. that would also set precedent for using _ as "whatever type goes" in other contexts

    It 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

  3. 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

  4. Personally, I view this as arising from the Fn(T) -> U sugar 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 spelled Fn(T) or Fn<T> (if we had variadic generics), and Fn(T) -> U would use where F::Output = U.

    eternaleye at 2018-01-28 09:26:18

  5. I'd rather support the more general transformation S<T, AssocTy = _> -> S<T> in signatures than a special case for the S(T) -> U sugar.

    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

  6. @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

  7. 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 make fn foo() { } be short for fn foo() -> () { }, which essentially entails that F: Fn() be short for F: 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 spelled Fn(T) or Fn<T> (if we had variadic generics), and Fn(T) -> U would use where F::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 of F: Fn<&u8> and F: Fn(&u8) would vary in a kind of subtle way (the latter is more like F: 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 of impl with 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

  8. 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 here
    

    So 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

  9. Triage: no changes I'm aware of

    Steve Klabnik at 2019-10-18 13:20:28

  10. 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 of impl with 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 forall form to impl Fn(impl Trait)?)

    If we consider it somewhat fundamental that we have this exists/forall dichotomy, 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>
    

    (exists inside forall being significant wrt universes, i.e. Y can depend on 'a and/or X)

    Now that's beautiful and (ideally) consistent with impl Trait!

    EDIT: if we wanted to make it clearer, we should've picked any/some instead of impl+context-varied-semantics for the impl Trait syntax, e.g. the example above would be:

    where F: Fn(&'_ [any _]) -> Vec<some _>
    

    Eduard-Mihai Burtescu at 2022-07-24 17:24:14

  11. Note that the exists formulation 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 exists? (which is also how impl Trait works when used inside a body, e.g. as the type of a let variable)

    <hr/>

    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

  12. 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