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

Created on 28 Jan 2018  路  9Comments  路  Source: rust-lang/rust

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

C-feature-request T-lang

Most helpful comment

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.

All 9 comments

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?

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.

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(_, _, _) -> _

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.

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;
}

@petrochenkov The desugaring change I'm proposing is only valid for trait bounds, not trait objects.
I'll edit the description.

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

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

Triage: no changes I'm aware of

Was this page helpful?
0 / 5 - 0 ratings