-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Add unboxed, abstract return types #105
Conversation
FWIW, I feel somewhat uneasy about the I am currently leaning toward the more conservative design detailed under "Alternatives". |
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>>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/an/na/
Could you explain how this (e.g., 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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)>>>,
}
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:
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nikomatsakis might want to jump in here -- he first suggested allowing impl Trait
in struct
s, but I'm not sure if he had a concrete use-case in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
While I really like the idea of using 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 |
|
||
# Summary | ||
|
||
Allow functions to return types to return _unboxed abstract types_, written |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove to return types
Thanks for the quick feedback; I've updated the RFC to respond to most of the points made. I'll also respond in comments. |
Added notes to the RFC on an additional choice: allowing |
@SiegeLord I very much agree with your concerns about allowing On the other hand, I've added an alternative design where Finally, regarding |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On first glance, I like this idea, especially since it makes the equality/self thing fall out automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 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 Anyway, if it can work, my preferred syntax would be |
I think this was with foo as X + Y is ambiguous as Which brings us onto another thing, would/should/could explicit |
@huonw OK, so I can't offhand see why you'd need such a cast form. I suppose that the RFC implicitly assumes that |
I think the meaning of |
(Also, the notation |
Which |
Is there any specific reason the That would allow us to write: fn add(x: int) -> |int| -> int {|y| x + y} |
@eddyb: That's what I suggested in my unboxed closure proposal, and I don't see a problem with doing it like that. |
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 |
@nagisa I think most people are aware a new RFC is needed but are just getting some bikeshedding done before the time comes. |
There is a point in making it short, simple, and easy, though, to encourage the programmer to use this over |
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. |
So here's my two cents: I liked the 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 The downside is that you have to use Update: another downside is that it is not obvious from the syntax that |
@critiqjo But that has the wrong semantics: you're requiring that the type implement certain traits but not exposing it. |
That's true... 😞 But my original concern was to have an option to specify it using |
@critiqjo I honestly don't see the point, what would |
Wow! I see!! (I thought |
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. |
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). |
Just thought I'd mention there's further discussion of @aturon 's latest blogpost on reddit also. |
@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:
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 That said, given the "leaky" semantics of the proposed
Here, the fact that (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.)
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
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
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.
The answer feels like it should be "no", or at least, the rules should be akin to the ones for normal
With the |
🔔 Just a notice to any thread followers, a possible alternative is being discussed in kimundi's latest RFC. Edit: here's the reddit discussion. |
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 |
Wait, you want OIBITs to leak from the function body? That seems like an odd abstraction violation. Why would |
@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 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. |
|
||
The basic idea is to allow code like the following: | ||
````rust | ||
pub fn produce_iter_static() -> impl Iterator<int> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 {...}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gdox This should be discussed in the tracking issue.
ember-cli should look up for addons in optionalDependencies as well.
No description provided.