RFC: Add unboxed, abstract return types #105

Closed
wants to merge 1 commit into
from

Conversation

Projects
None yet
@aturon
Member

aturon commented Jun 3, 2014

No description provided.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

FWIW, I feel somewhat uneasy about the impl Trait syntax proposed here; bikeshedding welcome!

I am currently leaning toward the more conservative design detailed under "Alternatives".

Member

aturon commented Jun 3, 2014

FWIW, I feel somewhat uneasy about the impl Trait syntax proposed here; bikeshedding welcome!

I am currently leaning toward the more conservative design detailed under "Alternatives".

0000-abstract-return-types.md
+In today's Rust, you can write a function signature like
+````rust
+fn consume_iter_static<I: Iterator<u8>>(iter: I)
+fn consume_iter_dyanmic(iter: Box<Iterator<u8>>)

This comment has been minimized.

@huonw

huonw Jun 3, 2014

Member

s/an/na/

@huonw

huonw Jun 3, 2014

Member

s/an/na/

+ out, which can be very painful. Unboxed abstract types only require writing the
+ trait bound.
+
+* _Documentation_. In today's Rust, reading the documentation for the `Iterator`

This comment has been minimized.

@cmr

cmr Jun 3, 2014

Member

I looove this benefit.

@cmr

cmr Jun 3, 2014

Member

I looove this benefit.

This comment has been minimized.

@Valloric

Valloric Jun 3, 2014

This is very nice, agreed.

@Valloric

Valloric Jun 3, 2014

This is very nice, agreed.

This comment has been minimized.

@ticki

ticki Aug 23, 2015

Contributor

+1

@ticki

ticki Aug 23, 2015

Contributor

+1

+
+This code is roughly equivalent to
+````rust
+pub struct Result_produce_iter_static(

This comment has been minimized.

@huonw

huonw Jun 3, 2014

Member

Totally minor point, but I think this example would be clearer as just

struct Result_produce_iter_static {
    inner:  iter::Skip<...>
}
impl Iterator<int> for Result_produce_iter_static {
     fn next(&mut self) -> Option<int> { self.inner.next() }
}

(In particular, there's no runtime difference for using a so-called "newtype" vs a normal struct, unlike Haskell; and I've noticed that LLVM seems to optimise a plain struct better than tuple structs anyway.)

@huonw

huonw Jun 3, 2014

Member

Totally minor point, but I think this example would be clearer as just

struct Result_produce_iter_static {
    inner:  iter::Skip<...>
}
impl Iterator<int> for Result_produce_iter_static {
     fn next(&mut self) -> Option<int> { self.inner.next() }
}

(In particular, there's no runtime difference for using a so-called "newtype" vs a normal struct, unlike Haskell; and I've noticed that LLVM seems to optimise a plain struct better than tuple structs anyway.)

0000-abstract-return-types.md
+
+Just as is currently done for trait objects, the typechecker must ensure that
+lifetime parameters are not stripped when using an unboxed abstract type.
+For example (adapted from @glaebhoeri):

This comment has been minimized.

@huonw

huonw Jun 3, 2014

Member

@glaebhoerl (with an L)

@huonw

huonw Jun 3, 2014

Member

@glaebhoerl (with an L)

@nrc

This comment has been minimized.

Show comment
Hide comment
@nrc

nrc Jun 3, 2014

Member

Could you explain how this (e.g., fn foo() -> Tr in you proposal, where Tr is a trait) is different from, e.g., fn foo<X: T>()-> X in current Rust. As far as I can see, your proposal allows the callee to specify the concrete type for X rather than the caller - are there other differences? Is there a benefit over using a default type parameter for X (other than better encapsulation)?

Am I correct in assuming that the encapsulation here is only at the programmer level? That is, from the compiler's point of view, the caller does know the concrete type?

Member

nrc commented Jun 3, 2014

Could you explain how this (e.g., fn foo() -> Tr in you proposal, where Tr is a trait) is different from, e.g., fn foo<X: T>()-> X in current Rust. As far as I can see, your proposal allows the callee to specify the concrete type for X rather than the caller - are there other differences? Is there a benefit over using a default type parameter for X (other than better encapsulation)?

Am I correct in assuming that the encapsulation here is only at the programmer level? That is, from the compiler's point of view, the caller does know the concrete type?

+_implicit_ type argument.
+
+Using unboxed abstract types in arguments makes (simple) static and dynamic
+dispatch syntactically closer:

This comment has been minimized.

@nrc

nrc Jun 3, 2014

Member

I fear that this is a downside - the difference at the moment is relatively easy to explain. With this shorthand, I fear the syntax for static and dynamic dispatch is too similar. In other words, we break the principle of 'things which are different should look different'.

@nrc

nrc Jun 3, 2014

Member

I fear that this is a downside - the difference at the moment is relatively easy to explain. With this shorthand, I fear the syntax for static and dynamic dispatch is too similar. In other words, we break the principle of 'things which are different should look different'.

This comment has been minimized.

@chris-morgan

chris-morgan Jun 4, 2014

Member

At present, the static form is clumsy to read or to write, and so many people go in the direction of the less efficient dynamic dispatch. I view the increase in similarity as an improvement.

@chris-morgan

chris-morgan Jun 4, 2014

Member

At present, the static form is clumsy to read or to write, and so many people go in the direction of the less efficient dynamic dispatch. I view the increase in similarity as an improvement.

This comment has been minimized.

@bstrie

bstrie Jun 4, 2014

Contributor

I would like to know where you've observed people preferring dynamic dispatch just because of the syntax. The static dispatch syntax is more familiar to C++ programmers, and I'd expect them to reach for it first.

@bstrie

bstrie Jun 4, 2014

Contributor

I would like to know where you've observed people preferring dynamic dispatch just because of the syntax. The static dispatch syntax is more familiar to C++ programmers, and I'd expect them to reach for it first.

This comment has been minimized.

@sfackler

sfackler Jun 4, 2014

Member

I've seen people with functions returning Box<Iterator> in IRC decently often. The only alternative right now is to write a type signature that's probably too complex for anyone new to Rust to even figure out:

pub struct PhfMapEntries<'a, T> {
    priv iter: iter::FilterMap<'a,
                               &'a Option<(&'static str, T)>,
                               (&'static str, &'a T),
                               slice::Items<'a, Option<(&'static str, T)>>>,
}
@sfackler

sfackler Jun 4, 2014

Member

I've seen people with functions returning Box<Iterator> in IRC decently often. The only alternative right now is to write a type signature that's probably too complex for anyone new to Rust to even figure out:

pub struct PhfMapEntries<'a, T> {
    priv iter: iter::FilterMap<'a,
                               &'a Option<(&'static str, T)>,
                               (&'static str, &'a T),
                               slice::Items<'a, Option<(&'static str, T)>>>,
}
0000-abstract-return-types.md
+fn extend_dynamic(&mut self, iterator: &Iterator<T>)
+````
+
+It may be especially important for passing unboxed closures as arguments.

This comment has been minimized.

@nrc

nrc Jun 3, 2014

Member

Could you expand on this please? (and/or give an example)

@nrc

nrc Jun 3, 2014

Member

Could you expand on this please? (and/or give an example)

This comment has been minimized.

@aturon

aturon Jun 3, 2014

Member

@nick29581 Expanded with example.

@aturon

aturon Jun 3, 2014

Member

@nick29581 Expanded with example.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

@nick29581

fn foo<X: T>() -> X says "for any type X implementing T, I'll produce an X".

fn foo() -> impl T says "there's some hidden type X implementing T; I'll produce an X"

You cannot use generic types/default type parameters to get at the second meaning, because the point is that the function's code produces a single, concrete return type of its choosing.

From the compiler's point of view, what the caller knows depends on the stage of the compiler:

  • during typechecking, the caller knows only the trait bound, not the concrete type
  • during codegen, the caller knows the concrete type and generates statically-dispatched calls, etc.

This and other details are, I believe, covered in the RFC; let me know if it's not clear.

Member

aturon commented Jun 3, 2014

@nick29581

fn foo<X: T>() -> X says "for any type X implementing T, I'll produce an X".

fn foo() -> impl T says "there's some hidden type X implementing T; I'll produce an X"

You cannot use generic types/default type parameters to get at the second meaning, because the point is that the function's code produces a single, concrete return type of its choosing.

From the compiler's point of view, what the caller knows depends on the stage of the compiler:

  • during typechecking, the caller knows only the trait bound, not the concrete type
  • during codegen, the caller knows the concrete type and generates statically-dispatched calls, etc.

This and other details are, I believe, covered in the RFC; let me know if it's not clear.

+could provide different concrete iterator types for the first and second
+components of the tuple.
+
+### Structs and other compound types

This comment has been minimized.

@huonw

huonw Jun 3, 2014

Member

Is there a concrete use-case for this? It seems rather more complicated and adds an entirely implicit place of monomorphisation, that is, writing

fn use_foo(f: Foo) {}

is actually a generic function and will create multiple instantiations in the binary (am I interpreting this correctly?), but there is absolutely no indication of this from the signature. Is it crazy to restrict it to something like

struct Foo<T: Set<u8>> {
    s: T
}

fn use_foo(f: Foo<impl Set<u8>>)

(I guess this means not special-casing these types particularly.)

@huonw

huonw Jun 3, 2014

Member

Is there a concrete use-case for this? It seems rather more complicated and adds an entirely implicit place of monomorphisation, that is, writing

fn use_foo(f: Foo) {}

is actually a generic function and will create multiple instantiations in the binary (am I interpreting this correctly?), but there is absolutely no indication of this from the signature. Is it crazy to restrict it to something like

struct Foo<T: Set<u8>> {
    s: T
}

fn use_foo(f: Foo<impl Set<u8>>)

(I guess this means not special-casing these types particularly.)

This comment has been minimized.

@aturon

aturon Jun 3, 2014

Member

@huonw I agree; I am uneasy about putting these in structs, and I don't have a strong use-case for it.

The main reason for including it in the proposal was to treat impl Trait consistently as something you can write anywhere a type goes. But the more I think about it, the more I like the conservative alternative I outline in the end: restricting this RFC to function return types, and using the syntax "_ : Trait" instead.

@aturon

aturon Jun 3, 2014

Member

@huonw I agree; I am uneasy about putting these in structs, and I don't have a strong use-case for it.

The main reason for including it in the proposal was to treat impl Trait consistently as something you can write anywhere a type goes. But the more I think about it, the more I like the conservative alternative I outline in the end: restricting this RFC to function return types, and using the syntax "_ : Trait" instead.

This comment has been minimized.

@aturon

aturon Jun 3, 2014

Member

@nikomatsakis might want to jump in here -- he first suggested allowing impl Trait in structs, but I'm not sure if he had a concrete use-case in mind.

@aturon

aturon Jun 3, 2014

Member

@nikomatsakis might want to jump in here -- he first suggested allowing impl Trait in structs, but I'm not sure if he had a concrete use-case in mind.

This comment has been minimized.

@nikomatsakis

nikomatsakis Jun 4, 2014

Contributor

Not really. I think I was just pushing the idea to see how far it could go. The return value variation is interesting, though I think there is value in permitting it in argument position. We have precedent for having fn signatures have rich shorthands and I think it's served us fairly well.

@nikomatsakis

nikomatsakis Jun 4, 2014

Contributor

Not really. I think I was just pushing the idea to see how far it could go. The return value variation is interesting, though I think there is value in permitting it in argument position. We have precedent for having fn signatures have rich shorthands and I think it's served us fairly well.

@SiegeLord

This comment has been minimized.

Show comment
Hide comment
@SiegeLord

SiegeLord Jun 3, 2014

While I really like the idea of using impl Foo in places other than the return type (as removes the syntactic weight of the 'prefered' method of dispatch), the implicit parametrization in the struct example just rubs me the wrong way; is there no way to indicate to the reader what is happening there? In the case of function arguments, this also seems to preclude being able to specify type hints for these implicit type parameters. Or, would this work?

fn foo1(b: impl Foo) {}
foo1<Bar>()
fn foo2<T>(a: T, b: impl Foo) {}
foo2<Bar, Baz>() // T is set to Bar, implicit one is set to Baz

Also, just for complete clarity, does the & go before or after the impl?

While I really like the idea of using impl Foo in places other than the return type (as removes the syntactic weight of the 'prefered' method of dispatch), the implicit parametrization in the struct example just rubs me the wrong way; is there no way to indicate to the reader what is happening there? In the case of function arguments, this also seems to preclude being able to specify type hints for these implicit type parameters. Or, would this work?

fn foo1(b: impl Foo) {}
foo1<Bar>()
fn foo2<T>(a: T, b: impl Foo) {}
foo2<Bar, Baz>() // T is set to Bar, implicit one is set to Baz

Also, just for complete clarity, does the & go before or after the impl?

0000-abstract-return-types.md
+
+# Summary
+
+Allow functions to return types to return _unboxed abstract types_, written

This comment has been minimized.

@pczarn

pczarn Jun 3, 2014

remove to return types

@pczarn

pczarn Jun 3, 2014

remove to return types

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

Thanks for the quick feedback; I've updated the RFC to respond to most of the points made. I'll also respond in comments.

Member

aturon commented Jun 3, 2014

Thanks for the quick feedback; I've updated the RFC to respond to most of the points made. I'll also respond in comments.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

Added notes to the RFC on an additional choice: allowing impl Trait only in function signatures.

Member

aturon commented Jun 3, 2014

Added notes to the RFC on an additional choice: allowing impl Trait only in function signatures.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

@SiegeLord I very much agree with your concerns about allowing impl Trait to used everywhere, especially for the struct example. Honestly, I think allowing impl Trait in struct fields is probably a bad idea :-)

On the other hand, I've added an alternative design where impl Trait is only permitted in function signatures, which keeps the lightweight syntax but also means you can tell exactly where monomorphization is happening. Probably that design could allow explicitly supplying the concrete types for an impl Trait as well (as you're proposing).

Finally, regarding &, I think you'd want & impl Trait for "A reference to some T where T: Trait".

Member

aturon commented Jun 3, 2014

@SiegeLord I very much agree with your concerns about allowing impl Trait to used everywhere, especially for the struct example. Honestly, I think allowing impl Trait in struct fields is probably a bad idea :-)

On the other hand, I've added an alternative design where impl Trait is only permitted in function signatures, which keeps the lightweight syntax but also means you can tell exactly where monomorphization is happening. Probably that design could allow explicitly supplying the concrete types for an impl Trait as well (as you're proposing).

Finally, regarding &, I think you'd want & impl Trait for "A reference to some T where T: Trait".

@huonw

This comment has been minimized.

Show comment
Hide comment
@huonw

huonw Jun 3, 2014

Member

I don't see any mention of multiple traits, e.g.

fn foo() -> impl Iterator<int> + Clone

It's probably worth mentioning even if it's not explicitly part of this RFC.

Member

huonw commented Jun 3, 2014

I don't see any mention of multiple traits, e.g.

fn foo() -> impl Iterator<int> + Clone

It's probably worth mentioning even if it's not explicitly part of this RFC.

+fn collect_to_set<T, I: Iterator<T>>(iter: I) -> impl Set<T>
+````
+we could allow naming the concrete result type by a path like
+`collect_to_set::<T, I>::impl`. The only way to get a value of this type is by

This comment has been minimized.

@huonw

huonw Jun 3, 2014

Member

On first glance, I like this idea, especially since it makes the equality/self thing fall out automatically.

@huonw

huonw Jun 3, 2014

Member

On first glance, I like this idea, especially since it makes the equality/self thing fall out automatically.

This comment has been minimized.

@huonw

huonw Jun 4, 2014

Member

Although, there's a slight complication, what about something like

fn nested() -> Vec<impl Foo>

There is extra structure here, so presumably the nested::impl type would preferably point to the interior of the Vec rather than the whole return type (i.e. it's returning Vec<nested::impl>, meaning one might wish to write something like let x: &nested::impl = nested().get(0)), which then makes it hard to refer to values with multiple abstract generics, e.g.

fn tuple() -> (impl Iterator<int>, impl Iterator<u8>)

Also, what about abstract generics nested in others:

fn nested2() -> impl Iterator<impl Foo>
@huonw

huonw Jun 4, 2014

Member

Although, there's a slight complication, what about something like

fn nested() -> Vec<impl Foo>

There is extra structure here, so presumably the nested::impl type would preferably point to the interior of the Vec rather than the whole return type (i.e. it's returning Vec<nested::impl>, meaning one might wish to write something like let x: &nested::impl = nested().get(0)), which then makes it hard to refer to values with multiple abstract generics, e.g.

fn tuple() -> (impl Iterator<int>, impl Iterator<u8>)

Also, what about abstract generics nested in others:

fn nested2() -> impl Iterator<impl Foo>
@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

@huonw Just added a brief section in "Unresolved questions" on multiple bounds. I think it should definitely be part of the design, but I'm not sure about the syntax. I seem to recall some problems recently regarding + when not inside < > braces?

Anyway, if it can work, my preferred syntax would be impl Trait1 + Trait2 as you proposed.

Member

aturon commented Jun 3, 2014

@huonw Just added a brief section in "Unresolved questions" on multiple bounds. I think it should definitely be part of the design, but I'm not sure about the syntax. I seem to recall some problems recently regarding + when not inside < > braces?

Anyway, if it can work, my preferred syntax would be impl Trait1 + Trait2 as you proposed.

@huonw

This comment has been minimized.

Show comment
Hide comment
@huonw

huonw Jun 3, 2014

Member

I seem to recall some problems recently regarding + when not inside < > braces?

I think this was with as, something like

foo as X + Y

is ambiguous as (foo as X) + Y or foo as (X+Y).

Which brings us onto another thing, would/should/could explicit some_value as impl Iterator<int> casts be useful?

Member

huonw commented Jun 3, 2014

I seem to recall some problems recently regarding + when not inside < > braces?

I think this was with as, something like

foo as X + Y

is ambiguous as (foo as X) + Y or foo as (X+Y).

Which brings us onto another thing, would/should/could explicit some_value as impl Iterator<int> casts be useful?

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 3, 2014

Member

@huonw OK, so + for multiple bounds syntax would likely be a problem only if we allow these casts.

I can't offhand see why you'd need such a cast form. I suppose that the RFC implicitly assumes that T can be used for impl Trait whenever T: Trait, without any explicit casts. (Essentially the same behavior you get when instantiating generics today.)

Member

aturon commented Jun 3, 2014

@huonw OK, so + for multiple bounds syntax would likely be a problem only if we allow these casts.

I can't offhand see why you'd need such a cast form. I suppose that the RFC implicitly assumes that T can be used for impl Trait whenever T: Trait, without any explicit casts. (Essentially the same behavior you get when instantiating generics today.)

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Jun 4, 2014

Contributor

I think the meaning of foo as impl Bar would be pretty much the same as using impl in a local variable: essentially an assertion that the type does implement Bar. Not sure if there is much point, though, it could never help your program compile in particular, just make it fail.

Contributor

nikomatsakis commented Jun 4, 2014

I think the meaning of foo as impl Bar would be pretty much the same as using impl in a local variable: essentially an assertion that the type does implement Bar. Not sure if there is much point, though, it could never help your program compile in particular, just make it fail.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Jun 4, 2014

Contributor

(Also, the notation impl Foo + Bar is no more of a problem than the Foo + Bar types are today.)

Contributor

nikomatsakis commented Jun 4, 2014

(Also, the notation impl Foo + Bar is no more of a problem than the Foo + Bar types are today.)

@huonw

This comment has been minimized.

Show comment
Hide comment
@huonw

huonw Jun 4, 2014

Member

(Also, the notation impl Foo + Bar is no more of a problem than the Foo + Bar types are today.)

Which Foo + Bar types?

Member

huonw commented Jun 4, 2014

(Also, the notation impl Foo + Bar is no more of a problem than the Foo + Bar types are today.)

Which Foo + Bar types?

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Jun 4, 2014

Member

Is there any specific reason the impl keyword is required?
I don't think we'll have a way to return unsized types (because of the technical challenges of doing so, given existing calling conventions), so why not use just the trait name?

That would allow us to write:

fn add(x: int) -> |int| -> int {|y| x + y}
Member

eddyb commented Jun 4, 2014

Is there any specific reason the impl keyword is required?
I don't think we'll have a way to return unsized types (because of the technical challenges of doing so, given existing calling conventions), so why not use just the trait name?

That would allow us to write:

fn add(x: int) -> |int| -> int {|y| x + y}
@thestinger

This comment has been minimized.

Show comment
Hide comment
@thestinger

thestinger Jun 4, 2014

@eddyb: That's what I suggested in my unboxed closure proposal, and I don't see a problem with doing it like that.

@eddyb: That's what I suggested in my unboxed closure proposal, and I don't see a problem with doing it like that.

@huonw

This comment has been minimized.

Show comment
Hide comment
@huonw

huonw Jun 4, 2014

Member

Taking an anonymous generic as a parameter would be ambiguous with trait objects, e.g.

fn foo(x: &mut Trait) { ... }

could either be a trait object or equivalent to fn foo<T: Trait>(x: &mut T) { ... }.

Member

huonw commented Jun 4, 2014

Taking an anonymous generic as a parameter would be ambiguous with trait objects, e.g.

fn foo(x: &mut Trait) { ... }

could either be a trait object or equivalent to fn foo<T: Trait>(x: &mut T) { ... }.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Jun 4, 2014

Member

I was talking about the return types and maybe argument types that aren't behind a pointer, but I see your point.

Member

eddyb commented Jun 4, 2014

I was talking about the return types and maybe argument types that aren't behind a pointer, but I see your point.

@Kimundi

This comment has been minimized.

Show comment
Hide comment
@Kimundi

Kimundi Jun 4, 2014

Member

How would returning a annonymous type that might implement a trait work? For example, iter.map() returns a Map that only implements Iterator if T does, and only implements DoubleEndedIterator if T does.

So, depending on what iterator map() got called on, it would have to either return impl Iterator or impl Iterator+DoubleEndedIterator

Member

Kimundi commented Jun 4, 2014

How would returning a annonymous type that might implement a trait work? For example, iter.map() returns a Map that only implements Iterator if T does, and only implements DoubleEndedIterator if T does.

So, depending on what iterator map() got called on, it would have to either return impl Iterator or impl Iterator+DoubleEndedIterator

@thestinger

This comment has been minimized.

Show comment
Hide comment
@thestinger

thestinger Jun 4, 2014

@Kimundi: It would work exactly like bounded type parameters. If you specify it as returning Iterator, then the caller can only use the methods provided by Iterator.

So, depending on what iterator map() got called on, it would have to either return impl Iterator or impl Iterator+DoubleEndedIterator

This isn't really related to the proposal here. It's not possible to do this with a boxed trait object either. The purpose of this proposal is not to replace the existing generics system used for functions like map. It's an extension of trait objects to eliminate unnecessary boxing.

@Kimundi: It would work exactly like bounded type parameters. If you specify it as returning Iterator, then the caller can only use the methods provided by Iterator.

So, depending on what iterator map() got called on, it would have to either return impl Iterator or impl Iterator+DoubleEndedIterator

This isn't really related to the proposal here. It's not possible to do this with a boxed trait object either. The purpose of this proposal is not to replace the existing generics system used for functions like map. It's an extension of trait objects to eliminate unnecessary boxing.

@Kimundi

This comment has been minimized.

Show comment
Hide comment
@Kimundi

Kimundi Jun 4, 2014

Member

Right, its not directly relevant to the intention of this proposal.

But the proposal was talking about hiding complex iterator types behind a impl Iterator, which is restricting in many situations because you lose the double endedness in many generic cases, so I wondered if there was any though to that?

Member

Kimundi commented Jun 4, 2014

Right, its not directly relevant to the intention of this proposal.

But the proposal was talking about hiding complex iterator types behind a impl Iterator, which is restricting in many situations because you lose the double endedness in many generic cases, so I wondered if there was any though to that?

@thestinger

This comment has been minimized.

Show comment
Hide comment
@thestinger

thestinger Jun 4, 2014

If you to return an iterator, you need to specify the interface for the caller. In Rust's type system, that means a choice between it being an Iterator and DoubleEndedIterator because it's not possible to do type metaprogramming. Rust's type system does not allow the caller to make any assumptions based on the body of the callee either, and that's a desirable property.

If you to return an iterator, you need to specify the interface for the caller. In Rust's type system, that means a choice between it being an Iterator and DoubleEndedIterator because it's not possible to do type metaprogramming. Rust's type system does not allow the caller to make any assumptions based on the body of the callee either, and that's a desirable property.

@pnkfelix

This comment has been minimized.

Show comment
Hide comment
@pnkfelix

pnkfelix Jun 4, 2014

Member

I'm in favor of adding this in some form. (And I think I like the as-described fn foo(q: impl QuuxTrait) -> impl WidgetTrait syntax.)

@aturon I'm not sure if you addressed this question from @SiegeLord

In the case of function arguments, this also seems to preclude being able to specify type hints for
these implicit type parameters. Or, would this work?

fn foo1(b: impl Foo) {}
foo1<Bar>()
fn foo2<T>(a: T, b: impl Foo) {}
foo2::<Bar, Baz>() // T is set to Bar, implicit one is set to Baz

To elaborate: If we attempt to adopt the optional extension to allow these unboxed-abstract trait instances in function argument positions, in the RFC you give examples of how that would desugar to a type-parameterized function definition. But it is not clear to me whether that desugaring is meant to be interpreted literally, in that we would somehow combine the explicit type parameters with the implicitly-injected type parameters.

(The latter sounds easy on the surface, though I do worry about details like in what order multiple impl Trait injected parameters would be added.)

It would greatly benefit the RFC if you added an example of a function signature that had both an impl Trait as one of its arguments and also had explicit type parameters, and showed what it would look like to call such a function (with the ::<..>(...) syntax for explicitly instantiating the generic parameters).

Member

pnkfelix commented Jun 4, 2014

I'm in favor of adding this in some form. (And I think I like the as-described fn foo(q: impl QuuxTrait) -> impl WidgetTrait syntax.)

@aturon I'm not sure if you addressed this question from @SiegeLord

In the case of function arguments, this also seems to preclude being able to specify type hints for
these implicit type parameters. Or, would this work?

fn foo1(b: impl Foo) {}
foo1<Bar>()
fn foo2<T>(a: T, b: impl Foo) {}
foo2::<Bar, Baz>() // T is set to Bar, implicit one is set to Baz

To elaborate: If we attempt to adopt the optional extension to allow these unboxed-abstract trait instances in function argument positions, in the RFC you give examples of how that would desugar to a type-parameterized function definition. But it is not clear to me whether that desugaring is meant to be interpreted literally, in that we would somehow combine the explicit type parameters with the implicitly-injected type parameters.

(The latter sounds easy on the surface, though I do worry about details like in what order multiple impl Trait injected parameters would be added.)

It would greatly benefit the RFC if you added an example of a function signature that had both an impl Trait as one of its arguments and also had explicit type parameters, and showed what it would look like to call such a function (with the ::<..>(...) syntax for explicitly instantiating the generic parameters).

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Jun 4, 2014

Contributor

My opinion prior to reading this RFC was that it would be best to take the trait object analogy and run with it: make the analogy precise both (a) in syntax and (b) in semantics, with the exception of (c) some unavoidable semantic restrictions on the producer of an unboxed trait object relative to the producer of a boxed trait object.

(a) Syntax: Given that a boxed type is e.g. Box<Type>, an unboxed type is Type, and a boxed trait object is Box<Trait>, an unboxed trait object should be Trait.

(b) Semantics: Box<Trait> behaves like exists T: Trait. Box<T>, so Trait should behave like exists T: Trait. T. In particular, this means that we should not do the "type equality and Self" part of this proposal, because that would break the analogy: it would behave like a hidden compiler-generated newtype rather than like a true existential type, unlike boxed trait objects. Just as a boxed trait object in argument position fn foo(obj: &Trait) semantically behaves like fn foo<T: Trait>(obj: &T) (incidentally, this is also similar to what happens with implicit lifetime parameters in current Rust), fn foo(obj: Trait) should behave like fn foo<T: Trait>(obj: T). Similarly in the return type position, fn bar() -> Trait should behave from the caller's perspective as-if it were a true existential type, and might be a different type each time the function is called.

(c) Restrictions: The additional restriction relative to boxed trait objects is that any expression which is used as an unboxed trait object must evaluate to the same actual type in all branches. So in argument position, from the caller's side, given fn print_unboxed(arg: Show), print_unboxed(if x { 9i } else { "nine" }) is illegal, even though both types impl Show. Likewise, in return type position, from the callee's perspective, fn some_show(x: bool) -> Show { if x { 9i } else { "nine" } } is illegal, even though the equivalent with a boxed trait object would be legal.

I would allow this only in function signatures, at least in the first round. We can always think about expanding it later on.

Benefits. These decisions are not independent. We are "allowed" to use the same syntax for boxed and unboxed trait objects because we have made the analogy between them precise. This means fewer distinct concepts need to be learned: if you know what "unboxed" and "trait object" mean, you also know what "unboxed trait object" means. We avoid running afoul of "different things should look different" because from a semantic perspective, they are the same thing. This is also helpful for refactoring: for the most part, you can switch between boxed and unboxed trait objects just as you would between boxed and unboxed versions of any concrete type, because the same relationships hold. (I would have said "add or remove sigils", but we don't have many of those any more.)

After reading the RFC, the fact that Box<Trait> and Box<impl Trait> are both meaningful has caused me to waver. This is also consistent, just in a different way. Instead of the natural extension of the boxed trait object concept to unboxed ones, we have a new thing, with new syntax, which itself has internally consistent behavior when used in various different contexts.

What I've outlined above could perhaps be thought of as the minimalist approach, with fewer concepts and less expressiveness, and this RFC as the maximalist one, with more concepts and greater expressiveness. Under the above you can't easily express Box<impl Trait>, and you "know less" about equality between the underlying types of unboxed trait objects than you theoretically could. That said, with respect to Box<impl Trait>, if you really want it, you could still do the newtype thing manually. And if/when we gain support for explicit existential quantification syntax, you could also use that: exists T: Trait. Box<T> corresponds to Box<Trait> and Box<exists T: Trait. T> corresponds to Box<impl Trait>.

Contributor

glaebhoerl commented Jun 4, 2014

My opinion prior to reading this RFC was that it would be best to take the trait object analogy and run with it: make the analogy precise both (a) in syntax and (b) in semantics, with the exception of (c) some unavoidable semantic restrictions on the producer of an unboxed trait object relative to the producer of a boxed trait object.

(a) Syntax: Given that a boxed type is e.g. Box<Type>, an unboxed type is Type, and a boxed trait object is Box<Trait>, an unboxed trait object should be Trait.

(b) Semantics: Box<Trait> behaves like exists T: Trait. Box<T>, so Trait should behave like exists T: Trait. T. In particular, this means that we should not do the "type equality and Self" part of this proposal, because that would break the analogy: it would behave like a hidden compiler-generated newtype rather than like a true existential type, unlike boxed trait objects. Just as a boxed trait object in argument position fn foo(obj: &Trait) semantically behaves like fn foo<T: Trait>(obj: &T) (incidentally, this is also similar to what happens with implicit lifetime parameters in current Rust), fn foo(obj: Trait) should behave like fn foo<T: Trait>(obj: T). Similarly in the return type position, fn bar() -> Trait should behave from the caller's perspective as-if it were a true existential type, and might be a different type each time the function is called.

(c) Restrictions: The additional restriction relative to boxed trait objects is that any expression which is used as an unboxed trait object must evaluate to the same actual type in all branches. So in argument position, from the caller's side, given fn print_unboxed(arg: Show), print_unboxed(if x { 9i } else { "nine" }) is illegal, even though both types impl Show. Likewise, in return type position, from the callee's perspective, fn some_show(x: bool) -> Show { if x { 9i } else { "nine" } } is illegal, even though the equivalent with a boxed trait object would be legal.

I would allow this only in function signatures, at least in the first round. We can always think about expanding it later on.

Benefits. These decisions are not independent. We are "allowed" to use the same syntax for boxed and unboxed trait objects because we have made the analogy between them precise. This means fewer distinct concepts need to be learned: if you know what "unboxed" and "trait object" mean, you also know what "unboxed trait object" means. We avoid running afoul of "different things should look different" because from a semantic perspective, they are the same thing. This is also helpful for refactoring: for the most part, you can switch between boxed and unboxed trait objects just as you would between boxed and unboxed versions of any concrete type, because the same relationships hold. (I would have said "add or remove sigils", but we don't have many of those any more.)

After reading the RFC, the fact that Box<Trait> and Box<impl Trait> are both meaningful has caused me to waver. This is also consistent, just in a different way. Instead of the natural extension of the boxed trait object concept to unboxed ones, we have a new thing, with new syntax, which itself has internally consistent behavior when used in various different contexts.

What I've outlined above could perhaps be thought of as the minimalist approach, with fewer concepts and less expressiveness, and this RFC as the maximalist one, with more concepts and greater expressiveness. Under the above you can't easily express Box<impl Trait>, and you "know less" about equality between the underlying types of unboxed trait objects than you theoretically could. That said, with respect to Box<impl Trait>, if you really want it, you could still do the newtype thing manually. And if/when we gain support for explicit existential quantification syntax, you could also use that: exists T: Trait. Box<T> corresponds to Box<Trait> and Box<exists T: Trait. T> corresponds to Box<impl Trait>.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 4, 2014

Member

@eddyb @thestinger The main reason for using some marker like impl is to distinguish Trait (an unsized type under DST) from impl Trait (a sized type in this proposal).

If we restrict the proposal to return types only, I would favor fn A -> _ : Trait as being more consistent with generics.

Member

aturon commented Jun 4, 2014

@eddyb @thestinger The main reason for using some marker like impl is to distinguish Trait (an unsized type under DST) from impl Trait (a sized type in this proposal).

If we restrict the proposal to return types only, I would favor fn A -> _ : Trait as being more consistent with generics.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 4, 2014

Member

@pnkfelix In the current proposal, there is no way to provide the concrete types explicitly, which could be problematic.

If we allow impl Trait in argument positions, but not in struct fields, I think it would be sensible to treat them as extra, unnamed type parameters that are positioned after the explicit type parameters in the signature, e.g.,

fn foo<T>(iter: impl Iterator<T>) -> impl Iterator T;
foo::<int, Range<int>>(range(0, 10))

But this approach doesn't work so well if impl Trait is allowed in struct fields, because then the parameters depend on the internals of the structs being used in the arguments, which might not even be public! I take this as evidence that allowing impl Trait in struct fields is probably a bad idea.

Member

aturon commented Jun 4, 2014

@pnkfelix In the current proposal, there is no way to provide the concrete types explicitly, which could be problematic.

If we allow impl Trait in argument positions, but not in struct fields, I think it would be sensible to treat them as extra, unnamed type parameters that are positioned after the explicit type parameters in the signature, e.g.,

fn foo<T>(iter: impl Iterator<T>) -> impl Iterator T;
foo::<int, Range<int>>(range(0, 10))

But this approach doesn't work so well if impl Trait is allowed in struct fields, because then the parameters depend on the internals of the structs being used in the arguments, which might not even be public! I take this as evidence that allowing impl Trait in struct fields is probably a bad idea.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Jun 4, 2014

Member

@glaebhoerl That's a very interesting perspective, but I have some questions.

  1. The use of Trait as a type is planned to have a meaning under DST: it will be an unsized type. This is important for the consistency of the type system in other respects, but I think it's incompatible with "unboxed trait objects", which would crucially be sized (since the whole point is that the underlying concrete type is known at compile time.) So I think some kind of marker like impl is probably needed. Or do you see a way to make the two interpretations compatible?

  2. If I understand correctly, your proposal is equivalent to:

    • the core RFC without the impl marker (which means that Box<impl Trait> is impossible,
    • the function parameters add-on
    • none of the other add-ons

    Is that right, or are there other differences I'm missing?

Personally, I am favoring something like this more minimal approach (the "Somewhat conservative alternative" I give).

Member

aturon commented Jun 4, 2014

@glaebhoerl That's a very interesting perspective, but I have some questions.

  1. The use of Trait as a type is planned to have a meaning under DST: it will be an unsized type. This is important for the consistency of the type system in other respects, but I think it's incompatible with "unboxed trait objects", which would crucially be sized (since the whole point is that the underlying concrete type is known at compile time.) So I think some kind of marker like impl is probably needed. Or do you see a way to make the two interpretations compatible?

  2. If I understand correctly, your proposal is equivalent to:

    • the core RFC without the impl marker (which means that Box<impl Trait> is impossible,
    • the function parameters add-on
    • none of the other add-ons

    Is that right, or are there other differences I'm missing?

Personally, I am favoring something like this more minimal approach (the "Somewhat conservative alternative" I give).

@blaenk

This comment has been minimized.

Show comment
Hide comment
@blaenk

blaenk Aug 23, 2015

Contributor

Yeah tilde is what we had suggested long ago and I think it'd look very nice and it's concise, and I think its association with "approximation" is useful as a mnemonic for its use.

Contributor

blaenk commented Aug 23, 2015

Yeah tilde is what we had suggested long ago and I think it'd look very nice and it's concise, and I think its association with "approximation" is useful as a mnemonic for its use.

@SimonSapin

This comment has been minimized.

Show comment
Hide comment
@SimonSapin

SimonSapin Aug 23, 2015

Contributor

-1 for sigils. https://xkcd.com/1306/

Contributor

SimonSapin commented Aug 23, 2015

-1 for sigils. https://xkcd.com/1306/

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Aug 23, 2015

Contributor

cmr:

One of my major ergonomic concerns is that the "fast thing" (static dispatch) is more syntax than the slow thing. We should make the fast thing easy and more natural.

👍 x 100

Contributor

ticki commented Aug 23, 2015

cmr:

One of my major ergonomic concerns is that the "fast thing" (static dispatch) is more syntax than the slow thing. We should make the fast thing easy and more natural.

👍 x 100

@Ericson2314

This comment has been minimized.

Show comment
Hide comment
@Ericson2314

Ericson2314 Aug 23, 2015

Contributor

The way I proposed to handle the contextual dependency is to replacing values of that impl Trait type with the inference variable that is used to "collect" the concrete type of the impl Trait.

I'm not exactly sure what you are proposing, but this can only be done within the scope where the the type is transparent: there exists a concrete types that will unify with the inference variable.

Arguably, you could expand on this, and be context-dependent inside a function by deanonymizing impl Traits assigned by the function. Or, in your suggestion, abstract types of the same module.
At least that wouldn't affect unification, but instead work similarly to projections (::Assoc before it's resolved).
But most importantly, you couldn't "witness" the equivalence between the abstract type and its concrete counterpart outside of a function in that module. Would that be good enough?

I'm not sure you mean---this sounds like what you would do outside the scope where the type is transparent / where the type is opaque. Only there does one need to make sure that that equivalence is unobservable. I don't see how to do that without making sure the abstract type and it's concrete definition don't unify.

Multi-function inference can become less painful (read: "less impossible") after the HIR/MIR work, but I wouldn't count on it.

Again, we can always introduce a fresh inference variable for each function in the scope where the type is transparent, and afterwords ensure that that their inferred definitions are the same.

I also fail to see the practical value of such abstract types (emulating parts of a module system we don't have, just for the sake of doing so, doesn't count), whereas returning anonymized types has had applications and a demand for years now.

This is needed for one has multiple functions that return the same abstract type, and at least two construct the type using it's concrete definition.

What can you do with non-inferred abstract types that newtypes don't already handle?
It might be better just to improve on newtypes, if there are rough corners, rather than papering over their issues with a non-orthogonal system borrowed from a world where it is used in a different way (AFAICT, most of its original usecases are handled by traits).

Granted, ML often uses existentials/abstract types where Haskell and Rust use universals:

open Queue;
foo: ...Queue.t...
fn foo<T>(....) -> ... where T: Queue;

But newtypes are different story. Clearly traits don't help alleviate the need for newtypes---in fact new types are often needed for traits (due to coherence)!

There definitely an overlap betwen "one off" abstract types ("one off" in that since they are part of a module's signature and not a trait, they don't form a "reusable" abstraction) and new types. The most major differences are:

  • abstract types are more ergonomic
  • newtypes impose less coherence restrictions on downstream crates
  • newtypes expose their size (for e.g. transmute) and thus slightly leak

I don't think you can blame my proposal for the overlap, however. Allowing struct MyIter(_); (i.e. the type of the single field is inferred) addresses all the needs this RFC mentions.

On the matter of syntax...
If we're not afraid of recycling sigils...

To be clear, I was only proposing abs / abstract for named abstract types.

Contributor

Ericson2314 commented Aug 23, 2015

The way I proposed to handle the contextual dependency is to replacing values of that impl Trait type with the inference variable that is used to "collect" the concrete type of the impl Trait.

I'm not exactly sure what you are proposing, but this can only be done within the scope where the the type is transparent: there exists a concrete types that will unify with the inference variable.

Arguably, you could expand on this, and be context-dependent inside a function by deanonymizing impl Traits assigned by the function. Or, in your suggestion, abstract types of the same module.
At least that wouldn't affect unification, but instead work similarly to projections (::Assoc before it's resolved).
But most importantly, you couldn't "witness" the equivalence between the abstract type and its concrete counterpart outside of a function in that module. Would that be good enough?

I'm not sure you mean---this sounds like what you would do outside the scope where the type is transparent / where the type is opaque. Only there does one need to make sure that that equivalence is unobservable. I don't see how to do that without making sure the abstract type and it's concrete definition don't unify.

Multi-function inference can become less painful (read: "less impossible") after the HIR/MIR work, but I wouldn't count on it.

Again, we can always introduce a fresh inference variable for each function in the scope where the type is transparent, and afterwords ensure that that their inferred definitions are the same.

I also fail to see the practical value of such abstract types (emulating parts of a module system we don't have, just for the sake of doing so, doesn't count), whereas returning anonymized types has had applications and a demand for years now.

This is needed for one has multiple functions that return the same abstract type, and at least two construct the type using it's concrete definition.

What can you do with non-inferred abstract types that newtypes don't already handle?
It might be better just to improve on newtypes, if there are rough corners, rather than papering over their issues with a non-orthogonal system borrowed from a world where it is used in a different way (AFAICT, most of its original usecases are handled by traits).

Granted, ML often uses existentials/abstract types where Haskell and Rust use universals:

open Queue;
foo: ...Queue.t...
fn foo<T>(....) -> ... where T: Queue;

But newtypes are different story. Clearly traits don't help alleviate the need for newtypes---in fact new types are often needed for traits (due to coherence)!

There definitely an overlap betwen "one off" abstract types ("one off" in that since they are part of a module's signature and not a trait, they don't form a "reusable" abstraction) and new types. The most major differences are:

  • abstract types are more ergonomic
  • newtypes impose less coherence restrictions on downstream crates
  • newtypes expose their size (for e.g. transmute) and thus slightly leak

I don't think you can blame my proposal for the overlap, however. Allowing struct MyIter(_); (i.e. the type of the single field is inferred) addresses all the needs this RFC mentions.

On the matter of syntax...
If we're not afraid of recycling sigils...

To be clear, I was only proposing abs / abstract for named abstract types.

@gnzlbg

This comment has been minimized.

Show comment
Hide comment
@gnzlbg

gnzlbg Sep 8, 2015

Contributor

+1 For ~.

Contributor

gnzlbg commented Sep 8, 2015

+1 For ~.

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Sep 8, 2015

Contributor

I'm afraid that ~ may create confusion as these were sugar for Box before. This might create problems with older material on Rust (docs, tutorials and so on).

Contributor

ticki commented Sep 8, 2015

I'm afraid that ~ may create confusion as these were sugar for Box before. This might create problems with older material on Rust (docs, tutorials and so on).

@mitchmindtree

This comment has been minimized.

Show comment
Hide comment
@mitchmindtree

mitchmindtree Sep 8, 2015

Another syntax option is -> @Trait where the @ kind of represents the a in @nonymous/@bstract return type.

However to be honest -> impl Trait has grown on me too - it also reuses existing syntax, is fairly clear and saves fancy sigils for other use cases.

Another syntax option is -> @Trait where the @ kind of represents the a in @nonymous/@bstract return type.

However to be honest -> impl Trait has grown on me too - it also reuses existing syntax, is fairly clear and saves fancy sigils for other use cases.

@gnzlbg

This comment has been minimized.

Show comment
Hide comment
@gnzlbg

gnzlbg Sep 8, 2015

Contributor

Honestly I wish we could just -> Trait and be done with it (no sigil, no keyword, no nothing). The chaining could be just: -> TraitA + TraitB + ....

My second best option would be something like -> R: Trait where R is a generic parameter whose constrain appears on the return type -> R: TraitA + TraitB + ... (I really would prefer the constraints to appear explicitly in the return type instead of with the other generic arguments but I could live with that too).

For reasons I don't understand (yet) we can't have any of these and as @eddyb argues above, impl is too much typing/noise for something that should be common and easy.

Since I read the sigil ~ as approx it makes sense to me in this context. But it is not my first option, and I dislike sigils in general since they are a barrier of entry for newcomers to the language who don't know them (just give a newby a Haskell programs that uses <$> and related operators everywhere). Maybe a better alternative could be to reuse as, like this: -> as TraitA + TraitB + ....

Contributor

gnzlbg commented Sep 8, 2015

Honestly I wish we could just -> Trait and be done with it (no sigil, no keyword, no nothing). The chaining could be just: -> TraitA + TraitB + ....

My second best option would be something like -> R: Trait where R is a generic parameter whose constrain appears on the return type -> R: TraitA + TraitB + ... (I really would prefer the constraints to appear explicitly in the return type instead of with the other generic arguments but I could live with that too).

For reasons I don't understand (yet) we can't have any of these and as @eddyb argues above, impl is too much typing/noise for something that should be common and easy.

Since I read the sigil ~ as approx it makes sense to me in this context. But it is not my first option, and I dislike sigils in general since they are a barrier of entry for newcomers to the language who don't know them (just give a newby a Haskell programs that uses <$> and related operators everywhere). Maybe a better alternative could be to reuse as, like this: -> as TraitA + TraitB + ....

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 8, 2015

Member

Using -> Trait might work, but that prevents nesting the anonymized type in other types (e.g. Box<impl Trait>) and can't be used in associated types at all, as it already has a meaning in those cases.

Generics can't ever be abused for this because the caller can use any type that implements Trait for R, and R has to be provided either directly or via inference.

The closest syntax that could be used is _: Trait, but AFAICT nobody likes that.

Member

eddyb commented Sep 8, 2015

Using -> Trait might work, but that prevents nesting the anonymized type in other types (e.g. Box<impl Trait>) and can't be used in associated types at all, as it already has a meaning in those cases.

Generics can't ever be abused for this because the caller can use any type that implements Trait for R, and R has to be provided either directly or via inference.

The closest syntax that could be used is _: Trait, but AFAICT nobody likes that.

@m4rw3r

This comment has been minimized.

Show comment
Hide comment
@m4rw3r

m4rw3r Sep 8, 2015

After using impl Trait for a while, it is not so bad at all; here is some example code which uses it: https://github.com/m4rw3r/rust_parser_experiments/blob/ebedd36f2f7e19171c65e38fdee3822d5daa4090/src/main.rs#L235

I agree with you @eddyb regarding generics, not only because it is abuse but also because it creates more noise for the caller. The caller might have to annotate a function, and then the caller either needs to be able to write the type for the generic return (impossible in the case of a closure) or leave a _ in its place which is just unnecessary noise and also something rustc might forbid in some contexts.

_: Trait: to me this looks like you are defining an anonymous generic which implements Trait, since all other uses of _ are anonymous placeholders of some kind.

m4rw3r commented Sep 8, 2015

After using impl Trait for a while, it is not so bad at all; here is some example code which uses it: https://github.com/m4rw3r/rust_parser_experiments/blob/ebedd36f2f7e19171c65e38fdee3822d5daa4090/src/main.rs#L235

I agree with you @eddyb regarding generics, not only because it is abuse but also because it creates more noise for the caller. The caller might have to annotate a function, and then the caller either needs to be able to write the type for the generic return (impossible in the case of a closure) or leave a _ in its place which is just unnecessary noise and also something rustc might forbid in some contexts.

_: Trait: to me this looks like you are defining an anonymous generic which implements Trait, since all other uses of _ are anonymous placeholders of some kind.

@bluss

This comment has been minimized.

Show comment
Hide comment
@bluss

bluss Sep 8, 2015

An interesting point that I think came up in discussion was that traits lose the main utility of associated types when you use abstract return types, since there is no concrete impl to attach the associated types to. Some kind of aliasing functionality may help with that.

bluss commented Sep 8, 2015

An interesting point that I think came up in discussion was that traits lose the main utility of associated types when you use abstract return types, since there is no concrete impl to attach the associated types to. Some kind of aliasing functionality may help with that.

@nagisa

This comment has been minimized.

Show comment
Hide comment
@nagisa

nagisa Sep 8, 2015

Contributor

fn abcd() ⇝ Trait

Here you go: a sigil, not intrusive, hard to spot and most importantly… extremely hard to type.


What I’m trying to say: this RFC was closed more than a year ago and nobody’s getting this feature in without a new RFC. As (my) previous experience shows, people tend to to go all over the same debate on a follow-up/replacement/new RFCs (see the placement-in + <-), therefore discussing syntax for a feature that’s not landing anytime soon looks like mostly a waste of time (people ignore discussions on previous RFCs most of the time).

Contributor

nagisa commented Sep 8, 2015

fn abcd() ⇝ Trait

Here you go: a sigil, not intrusive, hard to spot and most importantly… extremely hard to type.


What I’m trying to say: this RFC was closed more than a year ago and nobody’s getting this feature in without a new RFC. As (my) previous experience shows, people tend to to go all over the same debate on a follow-up/replacement/new RFCs (see the placement-in + <-), therefore discussing syntax for a feature that’s not landing anytime soon looks like mostly a waste of time (people ignore discussions on previous RFCs most of the time).

@mitchmindtree

This comment has been minimized.

Show comment
Hide comment
@mitchmindtree

mitchmindtree Sep 8, 2015

@nagisa I think most people are aware a new RFC is needed but are just getting some bikeshedding done before the time comes.

@nagisa I think most people are aware a new RFC is needed but are just getting some bikeshedding done before the time comes.

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 8, 2015

Member

@nagisa I've waited more than a month for @aturon and then we had a meeting last week and a new RFC might be coming this month, if that gives you any hope.
The syntax problem is one that will create bikeshed before and after a new RFC, and I'd be glad if we had something optimal before then.

Member

eddyb commented Sep 8, 2015

@nagisa I've waited more than a month for @aturon and then we had a meeting last week and a new RFC might be coming this month, if that gives you any hope.
The syntax problem is one that will create bikeshed before and after a new RFC, and I'd be glad if we had something optimal before then.

@ticki

This comment has been minimized.

Show comment
Hide comment
@ticki

ticki Sep 8, 2015

Contributor

There is a point in making it short, simple, and easy, though, to encourage the programmer to use this over Box (when possible). Naming it AnAbstractUnboxedReturnType does not encourage this. A sigil, on the other side, can also create confusions.

Contributor

ticki commented Sep 8, 2015

There is a point in making it short, simple, and easy, though, to encourage the programmer to use this over Box (when possible). Naming it AnAbstractUnboxedReturnType does not encourage this. A sigil, on the other side, can also create confusions.

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Sep 8, 2015

Member

@eddyb

I've waited more than a month for @aturon and then we had a meeting last week and a new RFC might be coming this month, if that gives you any hope.

Sorry! :(

I have a couple other RFCs in my queue, but hope to push them out this week, and then will focus on reviving this one. Thanks again, @eddyb, for your work on this topic.

Member

aturon commented Sep 8, 2015

@eddyb

I've waited more than a month for @aturon and then we had a meeting last week and a new RFC might be coming this month, if that gives you any hope.

Sorry! :(

I have a couple other RFCs in my queue, but hope to push them out this week, and then will focus on reviving this one. Thanks again, @eddyb, for your work on this topic.

@mitchmindtree

This comment has been minimized.

Show comment
Hide comment
@mitchmindtree

mitchmindtree Sep 8, 2015

Here here! Thanks @eddyb and @aturon (and everyone else involved). Personally, this has been one of my most anticipated features - looking forward to seeing what unfolds 👍

Here here! Thanks @eddyb and @aturon (and everyone else involved). Personally, this has been one of my most anticipated features - looking forward to seeing what unfolds 👍

@critiqjo

This comment has been minimized.

Show comment
Hide comment
@critiqjo

critiqjo Sep 20, 2015

The syntax problem is one that will create bikeshed before and after a new RFC, and I'd be glad if we had something optimal before then.

So here's my two cents: I liked the impl Trait syntax, but I feel uneasy that it doesn't play well with the where clause, and things may get really long. So I propose an alternative, inspired from the pattern matching syntax:

fn factory(num: i32) -> T @ _
    where T : Fn(i32) -> i32
{
    move |x| x + num
}

I also liked what @Stebalien proposed here, and here's an alternative:

type X = T @ Arc<_> where T : Send;

during compilation X should be resolvable to a single concrete type.

The downside is that you have to use where clause now!

Update: another downside is that it is not obvious from the syntax that T is not a trait... So I guess using impl is the better choice...

The syntax problem is one that will create bikeshed before and after a new RFC, and I'd be glad if we had something optimal before then.

So here's my two cents: I liked the impl Trait syntax, but I feel uneasy that it doesn't play well with the where clause, and things may get really long. So I propose an alternative, inspired from the pattern matching syntax:

fn factory(num: i32) -> T @ _
    where T : Fn(i32) -> i32
{
    move |x| x + num
}

I also liked what @Stebalien proposed here, and here's an alternative:

type X = T @ Arc<_> where T : Send;

during compilation X should be resolvable to a single concrete type.

The downside is that you have to use where clause now!

Update: another downside is that it is not obvious from the syntax that T is not a trait... So I guess using impl is the better choice...

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 20, 2015

Member

@critiqjo But that has the wrong semantics: you're requiring that the type implement certain traits but not exposing it.
This is similar to the confusion with generics, and your syntax examples are less ergonomic than even the -> _: Trait syntax, which is the opposite direction of where I'd like this to go.

Member

eddyb commented Sep 20, 2015

@critiqjo But that has the wrong semantics: you're requiring that the type implement certain traits but not exposing it.
This is similar to the confusion with generics, and your syntax examples are less ergonomic than even the -> _: Trait syntax, which is the opposite direction of where I'd like this to go.

@critiqjo

This comment has been minimized.

Show comment
Hide comment
@critiqjo

critiqjo Sep 20, 2015

That's true... 😞 But my original concern was to have an option to specify it using where...

That's true... 😞 But my original concern was to have an option to specify it using where...

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Sep 20, 2015

Member

@critiqjo I honestly don't see the point, what would where bring?
If it was actual existential syntax, it might be interesting, but it's pretty hard to use existentials for fn declarations because the whole fn is existential, not the return type (which would be a plain -> Trait).

Member

eddyb commented Sep 20, 2015

@critiqjo I honestly don't see the point, what would where bring?
If it was actual existential syntax, it might be interesting, but it's pretty hard to use existentials for fn declarations because the whole fn is existential, not the return type (which would be a plain -> Trait).

@critiqjo

This comment has been minimized.

Show comment
Hide comment
@critiqjo

critiqjo Sep 20, 2015

Wow! I see!! (I thought where was just to improve readability... Sorry for the noise...)

Wow! I see!! (I thought where was just to improve readability... Sorry for the noise...)

@aturon

This comment has been minimized.

Show comment
Hide comment
@aturon

aturon Sep 29, 2015

Member

Thanks @glaebhoerl @Ericson2314 @eddyb and others for the insightful discussion since this RFC was closed. I've been thinking about this a fair amount, and after digesting your various comments, wrote up a blog post outlining a couple possible directions.

Member

aturon commented Sep 29, 2015

Thanks @glaebhoerl @Ericson2314 @eddyb and others for the insightful discussion since this RFC was closed. I've been thinking about this a fair amount, and after digesting your various comments, wrote up a blog post outlining a couple possible directions.

@Stebalien

This comment has been minimized.

Show comment
Hide comment
@Stebalien

Stebalien Sep 29, 2015

Contributor

Nice! Syntax nit. I'd prefer the following over the arrow syntax:

trait IterAdapter: Iterator
    where Self: Clone if Self::Inner: Clone,
          Self: DoubleEndedIterator if Self::Inner: DoubleEndedIterator
{
    type Inner: Iterator;
}

To keep APIs sane, I wouldn't allow the inline version.

Also, this alone probably deserves its own RFC (it seems like it would be useful by itself).

Contributor

Stebalien commented Sep 29, 2015

Nice! Syntax nit. I'd prefer the following over the arrow syntax:

trait IterAdapter: Iterator
    where Self: Clone if Self::Inner: Clone,
          Self: DoubleEndedIterator if Self::Inner: DoubleEndedIterator
{
    type Inner: Iterator;
}

To keep APIs sane, I wouldn't allow the inline version.

Also, this alone probably deserves its own RFC (it seems like it would be useful by itself).

@mitchmindtree

This comment has been minimized.

Show comment
Hide comment
@mitchmindtree

mitchmindtree Sep 29, 2015

Just thought I'd mention there's further discussion of @aturon 's latest blogpost on reddit also.

Just thought I'd mention there's further discussion of @aturon 's latest blogpost on reddit also.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Oct 3, 2015

Contributor

@aturon (Going to respond here, because this is where most of the technical discussion has been, and the reddit discussion has fallen off the front pages by now.)

Here are some things which occurred to me while re-reading your post:

If I have my druthers, this feature would also be usable in argument position:

fn map<U>(self, f: ~FnOnce(T) -> U) -> Option<U>

However we end up solving the "abstract return type" use case, I agree it would be nice if it could extend to abstract arguments as well: it bothers me that we currently have to perform the same kind of fn hof<F: FnOnce(...)>(f: F) dance as C++. We should be able to do better.

That said, given the "leaky" semantics of the proposed ~Trait syntax, it seems to me that its analogous behavior in argument position would be much closer to C++ template expansion than to our existing generics:

fn my_print(thing: ~Display) {
    println!("{}", thing + 1)
}
my_print(21); // OK, prints 22
my_print("hi"); // compile error

Here, the fact that my_print can depend on the actual type that the caller chose to call it with, rather than just the specified Display interface, is simply dual to how given fn my_printable() -> ~Display, the caller could depend on the type that my_printable chose -- in either case meaning that a change in the implementation can break clients even while the signatures stay the same.

(Personally, this bothers me quite a bit: this is a question of priorities, but explicit interfaces and non-leaky abstractions would be much closer to hard requirements on my list, along with a clean, orthogonal design.)

  • It behaves exactly like associated types today.

[...]

  • This kind of “leakage” is already prevalent – and important! – in Rust today. For example, when you define an abstract type, you give a trait bound which must be fulfilled. But when a client has narrowed to a particular impl, everything about the associated type is revealed:

Could you spell this analogy out in greater detail? I don't quite have the intuition behind it. (One difference I notice is that with associated types, you do write out the actual type in at least one place, unlike with ~Trait - but it's not obvious to me what it corresponds to in the broader analogy.)

  • The type leakage is, in general, very unlikely to be relied upon. For example, to observe the particulars of an iterator adapter type, you’d have to do something like assign it to a suitably-typed mutable variable:

    let iter: Chain<Map<'a, (int, u8), u16, Enumerate<Filter<'a, u8, vec::MoveItems<u8>>>>, SkipWhile<'a, u16, Map<'a, &u16, u16, slice::Items<u16>>>>;
    iter = some_function();
    

I don't understand this example... why couldn't you rely on type inference? Why is mutability relevant? Either way, I don't think I agree with the broader point. On the one hand, maybe this is the case to some extent for iterator adapters, simply because these are special-purpose types whose only purpose in life is to adapt iterators, and there's inherently not much else you can do with them. But in general, most types have much broader interfaces. And on the other hand, I thought leakage for things like conditional impls was the whole point!

The basic idea is to introduce a “type abstraction operator” @ that is used to “seal” a concrete type to a particular interface:

pub type File = FileDesc@(Read + Write + Seek + Debug);

This is an intriguing approach, but you don't quite spell it out in the post -- what's the motivation for formulating things this way, rather than e.g. abstract type File: Read + Write + Seek + Debug = FileDesc?

  • How should these type definitions interact with coherence? Can you implement traits for File? Inherent methods? What if they conflict with traits/methods on FileDesc?

The answer feels like it should be "no", or at least, the rules should be akin to the ones for normal type aliases. You definitely shouldn't be able to give conflicting impls for File and FileDesc -- the owning module, at least, should see these as the same type. I guess it's an interesting question that if you do impl Foo for FileDesc and impl Bar for File, and if both File and FileDesc are exported, then outside the module you should be able to know that FileDesc: Foo and File: Bar, but not FileDesc: Bar or File: Foo (which you'd know inside the module, given you know File = FileDesc). That seems logical enough at least for this simple example, but it's kind of subtle and weird, so it might be a better idea to just forbid trait impls directly on abstract types. (Inherent impls seem more desirable... of course you'd like to provide an external API of things you can do with the abstract type, that's kind of the point. While from the owning module's perspective, it should still behave the same as if you were impling on a type alias.)

  • How do you deal with bounds where the type isn’t in Self position? For example, there is also an impl of Read and Write for &File that should be exported.

With the abstract type formulation, at least, it seems natural to use a where clause -- pub abstract type File = FileDesc where File: Read+Write+Seek+Debug, &File: Read+Write;, or somesuch.

Contributor

glaebhoerl commented Oct 3, 2015

@aturon (Going to respond here, because this is where most of the technical discussion has been, and the reddit discussion has fallen off the front pages by now.)

Here are some things which occurred to me while re-reading your post:

If I have my druthers, this feature would also be usable in argument position:

fn map<U>(self, f: ~FnOnce(T) -> U) -> Option<U>

However we end up solving the "abstract return type" use case, I agree it would be nice if it could extend to abstract arguments as well: it bothers me that we currently have to perform the same kind of fn hof<F: FnOnce(...)>(f: F) dance as C++. We should be able to do better.

That said, given the "leaky" semantics of the proposed ~Trait syntax, it seems to me that its analogous behavior in argument position would be much closer to C++ template expansion than to our existing generics:

fn my_print(thing: ~Display) {
    println!("{}", thing + 1)
}
my_print(21); // OK, prints 22
my_print("hi"); // compile error

Here, the fact that my_print can depend on the actual type that the caller chose to call it with, rather than just the specified Display interface, is simply dual to how given fn my_printable() -> ~Display, the caller could depend on the type that my_printable chose -- in either case meaning that a change in the implementation can break clients even while the signatures stay the same.

(Personally, this bothers me quite a bit: this is a question of priorities, but explicit interfaces and non-leaky abstractions would be much closer to hard requirements on my list, along with a clean, orthogonal design.)

  • It behaves exactly like associated types today.

[...]

  • This kind of “leakage” is already prevalent – and important! – in Rust today. For example, when you define an abstract type, you give a trait bound which must be fulfilled. But when a client has narrowed to a particular impl, everything about the associated type is revealed:

Could you spell this analogy out in greater detail? I don't quite have the intuition behind it. (One difference I notice is that with associated types, you do write out the actual type in at least one place, unlike with ~Trait - but it's not obvious to me what it corresponds to in the broader analogy.)

  • The type leakage is, in general, very unlikely to be relied upon. For example, to observe the particulars of an iterator adapter type, you’d have to do something like assign it to a suitably-typed mutable variable:

    let iter: Chain<Map<'a, (int, u8), u16, Enumerate<Filter<'a, u8, vec::MoveItems<u8>>>>, SkipWhile<'a, u16, Map<'a, &u16, u16, slice::Items<u16>>>>;
    iter = some_function();
    

I don't understand this example... why couldn't you rely on type inference? Why is mutability relevant? Either way, I don't think I agree with the broader point. On the one hand, maybe this is the case to some extent for iterator adapters, simply because these are special-purpose types whose only purpose in life is to adapt iterators, and there's inherently not much else you can do with them. But in general, most types have much broader interfaces. And on the other hand, I thought leakage for things like conditional impls was the whole point!

The basic idea is to introduce a “type abstraction operator” @ that is used to “seal” a concrete type to a particular interface:

pub type File = FileDesc@(Read + Write + Seek + Debug);

This is an intriguing approach, but you don't quite spell it out in the post -- what's the motivation for formulating things this way, rather than e.g. abstract type File: Read + Write + Seek + Debug = FileDesc?

  • How should these type definitions interact with coherence? Can you implement traits for File? Inherent methods? What if they conflict with traits/methods on FileDesc?

The answer feels like it should be "no", or at least, the rules should be akin to the ones for normal type aliases. You definitely shouldn't be able to give conflicting impls for File and FileDesc -- the owning module, at least, should see these as the same type. I guess it's an interesting question that if you do impl Foo for FileDesc and impl Bar for File, and if both File and FileDesc are exported, then outside the module you should be able to know that FileDesc: Foo and File: Bar, but not FileDesc: Bar or File: Foo (which you'd know inside the module, given you know File = FileDesc). That seems logical enough at least for this simple example, but it's kind of subtle and weird, so it might be a better idea to just forbid trait impls directly on abstract types. (Inherent impls seem more desirable... of course you'd like to provide an external API of things you can do with the abstract type, that's kind of the point. While from the owning module's perspective, it should still behave the same as if you were impling on a type alias.)

  • How do you deal with bounds where the type isn’t in Self position? For example, there is also an impl of Read and Write for &File that should be exported.

With the abstract type formulation, at least, it seems natural to use a where clause -- pub abstract type File = FileDesc where File: Read+Write+Seek+Debug, &File: Read+Write;, or somesuch.

@mitchmindtree

This comment has been minimized.

Show comment
Hide comment
@mitchmindtree

mitchmindtree Oct 4, 2015

🔔 Just a notice to any thread followers, a possible alternative is being discussed in kimundi's latest RFC.

Edit: here's the reddit discussion.

🔔 Just a notice to any thread followers, a possible alternative is being discussed in kimundi's latest RFC.

Edit: here's the reddit discussion.

@nrc

This comment has been minimized.

Show comment
Hide comment
@nrc

nrc Jan 12, 2016

Member

Some thoughts on impl trait here: http://ncameron.org/blog/abstract-return-types-aka-%60impl-trait%60/

One thing I don't address there, but think will work is allowing impls to use a concrete type where the trait uses impl Trait. Then allowing callers which know they have exactly that impl to use the concrete return type

Member

nrc commented Jan 12, 2016

Some thoughts on impl trait here: http://ncameron.org/blog/abstract-return-types-aka-%60impl-trait%60/

One thing I don't address there, but think will work is allowing impls to use a concrete type where the trait uses impl Trait. Then allowing callers which know they have exactly that impl to use the concrete return type

@comex

This comment has been minimized.

Show comment
Hide comment
@comex

comex Jan 13, 2016

Wait, you want OIBITs to leak from the function body? That seems like an odd abstraction violation. Why would Sync or Send be different from any other trait in that respect? Isn't part of the motivation to allow stabilizing APIs where the concrete type may change in the future?

comex commented Jan 13, 2016

Wait, you want OIBITs to leak from the function body? That seems like an odd abstraction violation. Why would Sync or Send be different from any other trait in that respect? Isn't part of the motivation to allow stabilizing APIs where the concrete type may change in the future?

@eddyb

This comment has been minimized.

Show comment
Hide comment
@eddyb

eddyb Jan 13, 2016

Member

@comex The reasoning is the same as with private fields: they are not exposed in the public API but they affect OIBITs.

If we don't reflect OIBITs through impl Trait, they would have to be explicitly specified, which apart from being an annotation nightmare, it would also require support for conditional bounds (e.g. (Send if T: Sync) or (Sync if 'a: 'static)).
And it wouldn't compose, at all. If a new OIBIT is added, existing impl Trait uses wouldn't have it.

My only concerns were about it requiring global inference to implement, but I believe we can create "global obligations" that are checked after all the types are known.

Member

eddyb commented Jan 13, 2016

@comex The reasoning is the same as with private fields: they are not exposed in the public API but they affect OIBITs.

If we don't reflect OIBITs through impl Trait, they would have to be explicitly specified, which apart from being an annotation nightmare, it would also require support for conditional bounds (e.g. (Send if T: Sync) or (Sync if 'a: 'static)).
And it wouldn't compose, at all. If a new OIBIT is added, existing impl Trait uses wouldn't have it.

My only concerns were about it requiring global inference to implement, but I believe we can create "global obligations" that are checked after all the types are known.

@mitchmindtree mitchmindtree referenced this pull request in PistonDevelopers/conrod Feb 4, 2016

Merged

Switch to event-based input handling #684

2 of 12 tasks complete

@eternaleye eternaleye referenced this pull request Apr 27, 2016

Merged

Minimal `impl Trait` #1522

+
+The basic idea is to allow code like the following:
+````rust
+pub fn produce_iter_static() -> impl Iterator<int> {

This comment has been minimized.

@gdox

gdox Sep 11, 2016

Maybe not the best place to write this, but the impl syntax sounds a bit confusing for me given the impl Trait for Struct syntax we currently have. What about (something like) the following?

pub fn produce_iter_static<I>() -> I guarantees I : Iterator<int> {
    range(0, 10).rev().map(|x| x * 2).skip(2)
}

This way, the usual syntax of static dispatch (the where-clause) is kept.
As a bonus, it allows:

pub fn produce_iter_static<I>() -> I guarantees I : Iterator<int> + Clone {...}
@gdox

gdox Sep 11, 2016

Maybe not the best place to write this, but the impl syntax sounds a bit confusing for me given the impl Trait for Struct syntax we currently have. What about (something like) the following?

pub fn produce_iter_static<I>() -> I guarantees I : Iterator<int> {
    range(0, 10).rev().map(|x| x * 2).skip(2)
}

This way, the usual syntax of static dispatch (the where-clause) is kept.
As a bonus, it allows:

pub fn produce_iter_static<I>() -> I guarantees I : Iterator<int> + Clone {...}

This comment has been minimized.

@Mark-Simulacrum

Mark-Simulacrum Sep 11, 2016

Contributor

@gdox This should be discussed in the tracking issue.

@Mark-Simulacrum

Mark-Simulacrum Sep 11, 2016

Contributor

@gdox This should be discussed in the tracking issue.

@cramertj cramertj referenced this pull request in cramertj/impl-trait-goals Jun 5, 2017

Open

Conditional Bounds #5

@Thomasdezeeuw Thomasdezeeuw referenced this pull request in rust-lang-nursery/futures-rs Dec 26, 2017

Closed

Cannot ergonomically match between diverging futures #683

@nikomatsakis nikomatsakis referenced this pull request in rust-lang/rust Mar 23, 2018

Open

Tracking issue for `impl Trait` (RFC 1522, RFC 1951, RFC 2071) #34511

14 of 27 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment