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 explicitly expressing the type of a -> impl Trait #1738

Closed
tomaka opened this issue Sep 5, 2016 · 26 comments
Closed

Allow explicitly expressing the type of a -> impl Trait #1738

tomaka opened this issue Sep 5, 2016 · 26 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@tomaka
Copy link

tomaka commented Sep 5, 2016

Right now, you can do this:

fn foo() -> impl Trait {
    ...   // the return value must implement Trait
}

In this issue I suggest that we add this syntax as well:

type FooRet = impl Trait;

fn foo() -> FooRet {
    ...   // the return value must implement Trait
}

This means that later we can use FooRet to designate the type that foo() returns.
This makes it possible to put it in a struct for example:

struct Bar {
    foo: FooRet,
}

impl Bar {
    fn new() -> Bar {
        Bar { foo: foo() }
    }
}

Unresolved questions

  • Do we allow multiple functions to return the same "type=impl type" if their effective return type is the same?
  • Handling template parameters looks straight-forward (type Foo<R> = impl Trait; fn foo<R>() -> Foo<R>), but I'm not totally sure that there's a not problem with that.

Alternatives

  • Add a syntax that resolves the return type of a function, similar to C++'s decltype.

Motivation

While the feature itself is -I think- mostly straight-forward, the biggest debate is probably the motivation.

Let's take this code:

struct MyIterator { ... }
impl Iterator for MyIterator { ... }

fn get_my_iterator() -> MyIterator { ... }

Right now you can wrap around it, if you want:

struct Wrapper(MyIterator);
fn get_wrapper() -> Wrapper { Wrapper(get_my_iterator() }

This is a zero-cost abstraction and a good usage of composition.

The problem begins when you want to use -> impl Trait:

fn get_my_iterator() -> impl Iterator { ... }

Suddenly, wrapping around it becomes hacky. The primary way to do so is this:

struct Wrapper<I>(I) where I: Iterator;
fn get_wrapper() -> Wrapper<impl Iterator> { Wrapper(get_my_iterator()) }

(The alternative way is to create a new trait named TraitForWrapper which contains the API of Wrapper, and make get_wrapper() return -> impl TraitForWrapper)

This not only complicates the documentation and makes the usage of get_wrapper() more confusing for beginners, but it is also leaky, as it makes wrapping around Wrapper more complicated:

struct WrapperWrapper<I>(Wrapper<I>) where I: Iterator;
fn get_wrapper_wrapper() -> WrapperWrapper<impl Iterator> { WrapperWrapper(get_wrapper()) }

(using the TraitForWrapper method is not better)

This looks like an hypothetical example, but for example if I were to use -> impl Trait in my project, some equivalents to WrapperWrapper would look nightmarish:

struct RenderSystem<Rp1, Rp2, Rp3, Pl1, Pl2, Pl3> {
    fog_of_war_pipeline: GraphicsPipeline<SimpleVertexDefinition, Pl1, Rp1>,
    background_pipeline: GraphicsPipeline<SimpleVertexDefinition, Pl2, Rp2>,
    lighting_pipeline: GraphicsPipeline<SimpleVertexDefinition, Pl3, Rp3>,
}

Basically, the problem of the -> impl Trait syntax is that it's poisonous. Once you use it in an API, all the codes that use this API will have to use -> impl Trait as well.

This proposition would fix this problem:

type MyIterator = impl Iterator;
fn get_my_iterator() -> MyIterator { ... }

struct Wrapper(MyIterator);
fn get_wrapper() -> Wrapper { Wrapper(get_my_iterator()) }

struct WrapperWrapper(Wrapper);
fn get_wrapper_wrapper() -> WrapperWrapper { WrapperWrapper(get_wrapper()) }
@tomaka
Copy link
Author

tomaka commented Sep 5, 2016

cc @eddyb

@eddyb
Copy link
Member

eddyb commented Sep 5, 2016

My prototype from last year supported impl Trait in associated types (not included in #1522), e.g.:

impl Iterator for Foo {
    type Item = impl Iterator<Item=String>;
    fn next(&mut self) -> Option<Self::Item> {...}
}

This feature is the minimal extension of #1522 to allow naming impl Trait (i.e. you can always use it to provide top-level type aliases) and it supports all trait usecases that don't involve HKT.

IMO it's a better first step because it's smaller in scope: I side-stepped the problem of choosing which functions to give the ability to determine the impl Trait, by allowing any method in the impl to specify the associated type in its return type, and they had to agree without seeing each-other's choices.

cc @rust-lang/lang

@eddyb eddyb added T-lang Relevant to the language team, which will review and decide on the RFC. I-nominated labels Sep 5, 2016
@durka
Copy link
Contributor

durka commented Sep 5, 2016

I still would like to name the type in the function signature, so it'd be foo::Output or fn foo()<R> -> R and foo::R or something.

@Ericson2314
Copy link
Contributor

I think this would be better served by abstract type Foo = _ and @eddyb's "independent agreement" approach rather than unification.

@eddyb
Copy link
Member

eddyb commented Sep 5, 2016

@durka We already have syntax for it: <typeof foo>::Output - we just need someone to RFC typeof.

@eddyb
Copy link
Member

eddyb commented Sep 5, 2016

@Ericson2314 I don't see how the proposal goes against my model, the difference with abstract type seems to be strictly syntactical, i.e. impl Trait is more palatable.
Even type Foo; or type Foo: Bar; is nicer and mimics the existing associated type syntax, while abstract doesn't say anything interesting or relevant (since associated types don't also use it).

@Ericson2314
Copy link
Contributor

Ericson2314 commented Sep 6, 2016

@eddyb going back to #105 (comment) I've wanted to separate the inference and abstraction aspects of impl trait. The only reason to keep them separate is avoiding global inference, but as you point out that's actually not necessary.

Because impls are similar to (parametertized) modules, it makes sense to put abstract type in an impl. The semantics are exactly what you say---items in the impl see the real definition but consumers see an abstract type. Similarly, for any item where is concrete, _ instructs that item to infer the type on its own, and only afterwards are the inferred types compared.

@cristicbz
Copy link

What are gotchas around <typeof foo>::Output?

Seems like a more general solution (since you can express original using it) and it applies to more cases eg. where a 3rd party library didn't think it was worth exposing the return type as a type alias. I find it very appealing.

@eddyb
Copy link
Member

eddyb commented Oct 3, 2016

@cristicbz It's not a silver bullet because it gives you the whole type, being able to name an impl Trait is more powerful in that you could have several of them at different levels.
However, I do agree that the ability to later name complex and/or anonymous types is good to have.

EDIT: As for gotchas, typeof $expr is not guaranteed to match another copy of typeof $expr.
That is, typeof || {} is different from any typeof || {} other than itself.
It also needs to be isolated wrt inference, so typeof 0 will be i32 (expect maybe in functions?).

The only new thing as far as the implementation is concerned is being able to type-check and infer that expression passed to typeof on-demand, potentially before the items it depends on even have types.
However, this is already needed for constants so work is underway to support that mechanism.

@tomaka
Copy link
Author

tomaka commented Oct 3, 2016

There are two possibilities for typeof: either typeof $ident or typeof $expr.
For the former, you'd write something like <typeof foo as Fn>::Output, and for the latter something like typeof foo().

@eddyb
Copy link
Member

eddyb commented Oct 3, 2016

@tomaka Well, it'd have to be a path not an identifier. But yeah, and it could've been implemented in 1.0.
Keep in mind however that even with the expression form you wouldn't want a call if you have arguments.

@nikomatsakis
Copy link
Contributor

More generally, you would probably write <typeof(foo::<X,Y,Z>) as Fn>::Output or something like that. (Presuming you had fn foo<A,B,C>() { ... }.)

@aturon aturon removed the I-nominated label Oct 6, 2016
@tomaka
Copy link
Author

tomaka commented Oct 14, 2016

@eddyb I was thinking of typeof $expr in order to mimic C++'s decltype.

@eddyb
Copy link
Member

eddyb commented Oct 14, 2016

@tomaka So you'd want arguments of the function to be in scope for typeof in the return type?
That could work, it's just more than typeof itself, which would be like an array length except not constant.

@burdges
Copy link

burdges commented Nov 2, 2016

I like <typeof foo>::Output or anything similar. And I dislike the original proposal here.

I've almost written :

type FooT = (Meow,Purr,Woof);
fn foo(..) -> FooT {
    debug_assert_eq!(mem::size_of::<FooT>(), ..)
    ...
}

It'd be far cleaner to write simply mem::size_of::<<typeof foo as Fn>::Output>()

Worse, I've needed to write u16::max_value() when I could not trust that the u16 might not change, so I've instead written :

type FooT = u16;
struct Foo(FooT);

And similar situations can arise around any method like T::new() that does not land in traits.

It'd be much cleaner to write Foo.0::max_value() or <typeof i.0>::max_value(). It's not that <typeof ..>:: is so easy to parse, but it'll work in macros. And this type alias trick makes reading the fn or struct signature hard, which seems worse.

@burdges
Copy link

burdges commented Nov 2, 2016

Just another idea : I wonder if it's worth adding a name or macro for the current fn. I've no example but a non-example is :

I've previously wanted a version of mem::transmute for pointers that ensured the referenced values had the same size, so roughly :

#[inline]
unsafe fn transmute_ref<A: Deref,B: Deref>(v: A) -> B {
    debug_assert_eq!(mem::size_of(A::Target),mem::size_of(B::Target));
    let r:B = core::mem::transmute::<A,B>(v);  
    r
}

I could situations where you wanted to write <current_fn as Fn>::Output in some macro though.

@tomaka
Copy link
Author

tomaka commented Nov 4, 2016

A tricky problem is this:

pub struct Foo {
    data: ???
}

impl Foo {
    pub fn new() -> Foo {
        Foo {
            data: (0 .. 3).map(|n| n * 2),
        }
    }
}

In this example you can't easily use typeof. In order to use typeof you would need to create a separate function just to initialize data, which can be very annoying.

@burdges
Copy link

burdges commented Nov 4, 2016

Yes, it'd be nice if Foo.data or Foo::data was simply the type of data as an associated type. Also Foo.0 or Foo::0 for tuples. Otherwise, one writes stuff like let foo: Foo; ... typeof(foo.data) which annoys the compiler because foo never gets used elsewhere. And it's worse at the top level with const declarations going unused.

@cristicbz
Copy link

@burdges but that would collide with method names, right?

struct Foo {
  data: u32,
}

impl Foo {
  fn data() { ... }
}

Foo::data refers to the method right now. Since types use a different namespaces I suppose you could use Foo::data to refer to the type of the data field rather than the data method when used in type position, but it might end up a little confusing...

@eddyb
Copy link
Member

eddyb commented Nov 4, 2016

@burdges TBH I wouldn't even expect outer declarations to be accessible, at least not in all cases. There is indeed a problem with accessing derived types that don't depend on inputs, and typeof { let x: Foo; x.data } is too ugly.

However, @tomaka's example wouldn't be fixable like that because you'd end up with the field type being equal to... itself. Which is a cyclic type dependency error.

In the complementary any/some model, that example would simply use data: some Iterator<Item=i32>.

@withoutboats
Copy link
Contributor

My intuition is that a type alias for impl Trait would not be a single type, but would be a distinct type in each instance its used.

For example, I might want to shorten impl Future<Item = T, Error = mycrate::Error> to type Future<T>. Perhaps that behaviour is better handled with trait aliases though.

However, this behavior might also be better handled by allowing users to abstract over for example the return type of some function fn function() -> impl Trait as an associated type, e.g. function::Return.

@tomaka
Copy link
Author

tomaka commented Jul 22, 2017

-> #2071

@scottmcm
Copy link
Member

scottmcm commented Jan 6, 2018

Seems like the "get the return type of an impl Trait" part of this is still needed despite abstract type, to handle places where you're calling something that didn't use abstract type?

https://users.rust-lang.org/t/how-do-you-store-the-result-of-an-impl-trait-return-type/14808/8

@matprec
Copy link

matprec commented Apr 27, 2018

However, this behavior might also be better handled by allowing users to abstract over for example the return type of some function fn function() -> impl Trait as an associated type, e.g. function::Return.

This would be a killer feature. I have a project where i generate an AST via the builder pattern on the type level. Obviously, the type of an AST shouldn't be named by the user; this would be madness for everything more elaborate than a FooBar example (Foo<Bar<Baz<Bez<Biz<Boz<Buz>>>>>>>).

Yet it is desirable to know the type of (and store) an AST. With impl Trait and associated types, we can already choose not the name it, yet know the type, or in this example the serializable AST.

When being able to access the return type of the function, we could actually name and store the AST and derivatives without having to write it out!

@dekellum
Copy link

dekellum commented Jun 2, 2018

Did you mean? Foo<Bar<Baz<Bez<Biz<Boz<Buz>>>>>>

@Centril
Copy link
Contributor

Centril commented Oct 7, 2018

Closing in favor of #2515.

@Centril Centril closed this as completed Oct 7, 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