Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow impl Trait in more contexts #1879

Closed
spinda opened this issue Feb 1, 2017 · 23 comments
Closed

Allow impl Trait in more contexts #1879

spinda opened this issue Feb 1, 2017 · 23 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@spinda
Copy link

spinda commented Feb 1, 2017

Currently impl Trait is only allowed as the return type of a free-standing functions and inherent methods. I would like to see it opened to a few more positions.

Local variable signatures

let x: impl Foo = bar();

can already be simulated by:

fn wrapper<T: Foo>(x: T) -> impl Foo { x }
let x = wrapper(bar());

Also,

let x: Option<impl Foo> = bar();

If bar returns Option<Baz>, Baz would be checked for an implementation of Foo.

Parameters of free-standing functions and methods

fn bar(x: impl Foo) { }

can be equivalent to:

fn bar<T: Foo>(x: T) { }

Furthermore,

fn bar(x: Option<impl Foo>) { }

can be equivalent to:

fn bar<T: Foo>(x: Option<T>) { }

This cuts down a bit on boilerplate.

Other positions in return types of free-standing functions and methods

fn bar() -> Option<impl Foo> { }

@withoutboats withoutboats added the T-lang Relevant to the language team, which will review and decide on the RFC. label Feb 1, 2017
@eddyb
Copy link
Member

eddyb commented Feb 1, 2017

@archshift
Copy link
Contributor

For local variables, is there a use-case for using impl Trait versus a (re)borrow?

let x: &Foo = &bar();

@spinda
Copy link
Author

spinda commented Feb 1, 2017 via email

@KalitaAlexey
Copy link

@spinda,
You can just omit the type.
Why can't you?

@spinda
Copy link
Author

spinda commented Feb 1, 2017

I may want to provide an explicit type annotation to clear up some ambiguity, for example, a type parameter that can't be inferred.

Really, I would like to remove as much special-casing of where impl Trait can be used as possible, excluding the ban on using it in trait definitions to avoid unintentionally introducing HKT.

@eddyb
Copy link
Member

eddyb commented Feb 1, 2017

@spinda Keep in mind the semantics can differ a lot between those positions. And this always worked:

fn bar() -> Option<impl Foo> { }

@spinda
Copy link
Author

spinda commented Feb 1, 2017

@eddyb So it does, thanks. I was under the impression that only fn bar() -> impl Foo was allowed.

@oli-obk
Copy link
Contributor

oli-obk commented Feb 1, 2017

Couldn't all locations that so far are compilation failures due to Sized bounds simply act like impl Trait. So impl would only be required for the given semantics in ?Sized situations. This would allow fn bar() -> Foo and let x: Foo = ... and so on, but be backwards compatible, since Box<Foo> would still be dynamic, and Box<impl Foo> could be used to differentiate.

@burdges
Copy link

burdges commented Feb 1, 2017

As an alternative, one could use an associated output type for functions, ala f::Output, although certainly that's less ergonomic in some cases.

I'm nervous about taking fn bar(x: Option<impl Foo>) { } to be equivalent to fn bar<T: Foo>(x: Option<T>) { }. Would you want to do that for closures too? In other contexts, I read impl Trait as being more _::Output where the compiler must infer the fn name _ so that _::Output matches the trait bounds.

That said, there are certainly situations where you can simplify presentation using that interpretation. In particular, you can make users view your tiers of type parameters in a two tiered way, as happens with several HashMap methods, ala

fn meow<K: Hash+Eq>(q: impl Borrow<K>) -> ...

@Ixrec
Copy link
Contributor

Ixrec commented Feb 1, 2017

My understanding of impl Trait was that it has fundamentally different semantics from type parameters, namely: the concrete type that a type parameter represents is determined by the function's call site, while the concrete type that an impl Trait represents is decided by the function's implementation. Given that, it seems nonsensical to put "impl Trait" in all of these other parts of the grammar as if it was merely syntactic sugar for ordinary type parameters.

For instance, under this RFC, would the desugaring of fn foo(x: Option<impl Foo>) -> impl Foo { } be a function with one type parameter or two? And which of those concrete types would be determined by the call site versus determined by the implementation? (it doesn't make sense for the concrete type of a function's arguments to be unknowable at any of its call sites, right?)

@burdges
Copy link

burdges commented Feb 1, 2017

I suppose your let x: impl Foo = y.bar(); actually comes into play if we have y satisfying Bar<u64>+Bar<usize> along with

trait Foo { .. }

trait Bar<R> {
    type Res;
    fn bar(self) -> Res;
}

impl Bar<u64> for SomeType {
    type Res = RealType;
    fn bar(self) -> RealType { .. }
}

impl Bar<usize> for SomeType {
    type Res = impl Foo; 
    fn bar(self) -> impl Foo { .. }
}

because now the impl Foo disambiguates which impl's bar method.

I imagine this type Res = impl Foo; is "concrete" enough not to be equivalent to HTKs in the sense that you only know its an impl Foo after specifying that you're using SomeType, but maybe this makes type inference tricky somehow. Yup see rust-lang/rust#34511 (comment)

@torkleyy
Copy link

torkleyy commented Feb 7, 2017

impl Trait for local variable

I think there definitely are some use cases and I also think this should be implemented.

impl Trait for function parameters

I agree, it removes some boiler plate; however, I don't think this should be implemented.

I think that having

fn foo<T: Bar>(value: T) {
}

makes it pretty obvious that this function will have a separate implementation for every type T it is called with, whereas in

fn foo(value: impl Bar) {
}

this isn't really obvious. Additionally, it has many limitations:

  • You can not have a where (like where Option<T>: Debug)
  • This also means your function parameters are getting pretty long and not as clear (I see that it is somehow also easier to read because you do not have to look up T in the where clause to see what it is bound on)
  • It may be harder for the compiler to talk about certain types when giving you an error message
  • You cannot use this type
    • For a return type
    • For multiple parameters: Are the types of x and y the same in fn foo(x: impl Bar, y: impl Bar) or are can they be different?

Also, this usage is very different from impl Trait as return type:

fn foo() -> impl Foo {}

means that foo will return an implementation of the trait Foo and it also means that it will always be the same concrete type but the implementer didn't want to or can't write out the exact type (because a closure was used for example).

But using impl Trait as a parameter type now is completely different from that. It means that any type that matches Trait is allowed, so that the type can be different for the next call of that function.

So in the end impl Trait as a parameter type and impl Trait as return type have different semantics which would be pretty confusing. Plus it isn't really much shorter than writing out the type parameter and it also means that you don't have the type named.

That's why I only agree with implementing it for local variables but not for function parameters.

@burdges
Copy link

burdges commented Feb 16, 2017

I think fn foo(value: impl Bar) being sugar for fn foo<T: Bar>(value: T) sounds premature because :

If I understand, impl Trait is currently an "existentially quantified" type, meaning the details are hidden but the type compiler find the real type that works, while T is a universally quantified type in fn foo<T: Bar>(value: T). Any argument for this sugar approach must claim that existentially quantified do not make sense as function arguments. That sounds quite strong.

In particular, anything you'd do with impl Trait in argument position is likely to correspond with what you'd do in struct/enum member position. At minimum I could imagine using impl Trait as a form of cheap associated type for struct/enum or something.

@burdges
Copy link

burdges commented Feb 16, 2017

We could use type .. = impl Trait as a lightweight type inference powered wrapper type, like say:

pub type HasherUsedHere = impl Hasher;
fn foo(..) -> HasherUsedHere, ..) { ... }
fn bar(&mut HasherUsedHere, ..) { ... }

@burdges
Copy link

burdges commented Feb 24, 2017

It's unclear if impl Trait for closure arguments would necessarily be the same as a generic closure.

pub fn invoker<..,F>(.., f: F) where f: Fn(impl Iterator) { .. }

vs

pub fn invoker<..,F>(.., f: F) where for<I: Iterator> F: Fn(I) { .. }

@porky11
Copy link

porky11 commented Apr 22, 2017

Why impl Trait for local variables? It's far more important for global variables or struct fields, where types are required, but cannot be written.
What I'd like to write is this:

struct Struct {
    actions: Vec<impl Fn()>
}

@torkleyy
Copy link

@porky11

I don't think it should be done for struct fields, because you cannot say the type from the declaration anymore. Additionally, you can easily make your struct generic over T, which isn't possible for local variables.

In the above example it does not make much sense to store the same closure multiple times. What you want instead is a heterogeneous Vector using Box<Fn()>.

@burdges
Copy link

burdges commented Apr 23, 2017

You could store the same closure with different captures using struct Fun<F: Fn()>(pub F). I've no idea if struct Foo(pub Vec<impl Bar>) should mean that or struct Foo(pub Vec<T>) for some opaque but fixed T.

@porky11
Copy link

porky11 commented Apr 24, 2017

@torkleyy

how would I do following without impl Trait for struct fields?

impl Struct {
    fn add_action(&mut self, c: i32, inc: i32) {
        self.push(move || {c+=inc; println!("{}", c)})
    }
}

If I had a local vector, it also would not have to contain boxes of traits,

@torkleyy
Copy link

@porky11

how would I do following without impl Trait for struct fields?

By having a struct Action:

struct Action {
    c: i32,
    inc: i32,
}

impl Action {
    fn execute(&mut self) {
        self.c += inc;
        println!("{}", self.c);
    }
}

It's of course more code, but if you have a

Vec<impl Trait>

you do not know what type it is by looking at the declaration (it's the same as for constants; using it different locally should not change the signature of a public item). Furthermore, you can never use the concrete type, which means you will have an impl Trait everywhere.

If I had a local vector, it also would not have to contain boxes of traits

I'm not sure if I understand you correctly. Is the following what you want to do?

let mut v = Vec::new();
v.push(|| do_whatever_you_want());

@porky11
Copy link

porky11 commented Apr 25, 2017

@torkleyy

Yes, your example is what I mean. This is only possivle with local variables, not when I need to specify a type.
I think, since the type can be inferred, it should be possible to use it also for global variables and struct fields to use it.

And yes, it's possile for every closure to be imlemented as structs with some call method, but don't closures exist to allow skipping to write such code?

@torkleyy
Copy link

@porky11 We could theoretically infer much more than we do: function return types, lifetimes, globals' types... However, if we do that, it becomes very difficult to argue about the code.

don't closures exist to allow skipping to write such code?

I guess they exist for some ergonomic handling like using iterator adapters where it would not make sense to explicitly declare a function or a struct for them.

I somehow agree that this limits the flexibility of a closure, but I really prefer to have the full type information given (just imagine you have a function pop_action: if it has a concrete return type, you can easily inspect that; with impl Trait you first have to look up where the actions are added). Additionally, in such a case you would not be able to add a closure to the Vec from another function.

@Centril
Copy link
Contributor

Centril commented Apr 26, 2018

Closing as we've already accepted RFCs for this (well... Option<impl Foo> maybe not, or maybe yes..).

@Centril Centril closed this as completed Apr 26, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests