-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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: Limited return type inference constrained to a trait #11455
Comments
I like this proposal but I think I would want more syntax for the return type. It's impossible to tell a trait apart from any other type just by looking at it, so it might get confusing. I don't have any particularly good ideas around this. The first that comes to mind is |
Yeah, that occurred to me too. There's no other legal meaning a trait in that position could possibly have, so it's not ambiguous in that sense, and it is very similar in terms of behaviour to a trait object (even if it's not quite the same thing), where the same concern applies and we don't seem to mind so much, so I'm not sure it's that bad. I could also see having different syntax though. My usual tactic in these cases is to take a look at the list of Rust keywords: in this case the two that look like they might have any kind of potential are So the options (so far) are:
I still like 1., but I'm not overly attached to it. Edit: actually, assuming of course that we implement this at all, maybe we should do 5., which is more precise and explicit, and reserve the option to introduce 1. as sugar for it at some later point. Edit again: yeah, I guess I'm basically in favor of 5. now. Conveying the meaning accurately is more important than appealing syntax. |
(It's not ambiguous, but if we have separate typing rules for the return On Sat, Jan 11, 2014 at 11:45 AM, Gábor Lehel [email protected]:
|
I would personally not be in favor of this. I find it incredibly important that all typechecking and borrowchecking related problems are all completely local to a function. I as a programmer can very easily know exactly what's going in because I have a very small world that I need to analyze (just this function). If you don't explicitly name the return type, what if it has lifetime parameters that I need to know about? What if I then need to pass it to another function? I agree that it's really annoying trying to type out all the iterator nested traits when returning one, but I really don't want to break the mental model of completely local inference. |
That's a good point, I hadn't considered lifetime parameters. On Sat, Jan 11, 2014 at 1:11 PM, Alex Crichton [email protected]:
|
@alexcrichton I think you're partly misunderstanding how it would work (but the issue you raise wrt lifetimes is still very valid). The caller of a function with an inferred return type would not know anything about the type that is returned (and hence would not be able to depend on it), except that it impls the trait. They would only see it as "some anonymous type". Therefore
you would only be able to pass it to another function if it's a generic one that accepts any
This one is hurting my brain. If I'm thinking straight, substituting a dummy/placeholder/anonymous type for a type that has lifetime parameters would break type safety. Consider for example:
Just as with trait objects, we would have to forbid erasing lifetime information in this way. I'm not sure how significantly this would impact the usefulness of the feature. @alexcrichton Does this address your concerns? Do you see any other problems? |
This is still very different from returning a trait. For example if I return a I'm still very concerned about this and its effects on a programmer's mental model. I can come up with a few cases here and there about where I personally would be confused, but this will always come back to the basic problem that reasoning about a function is no longer local to the function itself. I don't think that a feature like this is impossible, but we would need to very carefully approach it. Another idea off the top of my head is |
With regards to the lifetimes issue, trying out my previous example, except using a trait object:
I get the error:
No.
Yes. Any information you want to convey to the outside world has to be in the signature. If you want the rest of the program to know that it's a PeekIterator, then you need to write
Not to me. The same thing happens with trait objects. Why is this more surprising?
It shouldn't have any more effect than trait objects do. Just like with trait objects, you get an unknown type that you can call trait methods on. The only differences from a type system perspective are that (a) the caller can assume that the same type is returned every time (while not knowing anything about the type) (whereas with trait objects, the type "inside" can be different each time), and correspondingly (b) all returns from the function have to infer to the same type.
You can write that today, but the meaning is very different. That type says that you have to be able to return any iterator type of the caller's choice. In other words, you won't be able to implement it. What we want is a return type chosen by the callee, with the details of what type that is (apart from what is present in the signature, i.e. the trait) being hidden from the caller. EDIT: Basically these concerns about non-local reasoning are exactly what this proposal was designed to address. |
Cool, so from what you said this sounds more palatable to me. I remain concerned about the idea, however, because this sounds exactly like a trait object, except that you're able to return one by value. From a compiler standpoint I guess this will infer what the return value actually is, but why won't we start inferring these arguments everywhere else? If you can do this for return values, why not for argument values or struct values? I fear that this is addressing a very specific problem without addressing the problem at large. I don't think that DST will fix this, but perhaps it will? I'm sorry if I sound so negative here, I don't want to deter an awesome feature while it's brewing! I'm just concerned about adding a very specific feature to the language instead of "getting our bang for our buck" from some other feature. |
Well, let's try looking at it case by case. For functions we can break it down on two axes: argument vs. return, in other words caller provides value vs. callee provides value, and caller chooses type vs. callee chooses type.
Struct values: in other words, this would be
What would you say is "the problem at large"?
When I first started thinking about this, I thought it might be closely related to DST as well (after all it's sort of like an unboxed existential, while DST deals with boxed existentials), but after thinking about it more, I reached the conclusion that the two don't actually have much to do with each other, and that this is better thought of as (restricted) return type inference (for instance, because unboxed existentials are impossible, and return type inference is possible).
No problem at all, or rather: thank you. You've already pointed out one very important issue (lifetimes), and made me consider another possibility I hadn't before (the analogous case for function arguments, from above), even if I don't think there's much of a use case for it at present. And "generalize, generalize, generalize" is basically my mantra as well, so we're in agreement there. I don't see any general thing at the moment that this feature could be a specific case of. The only axis I can think of to generalize it on would be the potential to return something where an inner type is inferred, e.g. I think we might be on the same page in the end? That trait-restricted return type inference is preferable to having to use trait objects (because performance), and preferable to unrestricted return type inference (because local reasoning), and preferable to always having to write out the type (because unboxed closures won't have one). And that if there's some more general mechanism, then that would be even more preferable: I just don't happen to know of any. (And apologies if I might be a little too comprehensive in my responses!) |
So as I read this it's getting more and more palatable to me. I'd certainly want to discuss this in at least a meeting if not a larger forum. It doesn't make sense to me to store a value like this in structs or things like that, but your |
Sure, there's no rush. It's a backwards compatible feature and we don't even have unboxed closures yet (and it's not like I have a PR ready to go). |
Maybe we should merge (or at least link) #11196 and this issue, they both deal with using traits as types. And I found an example where you can't just use the trait name as the return type, but maybe it's a bit convoluted: impl<A, B, C, F: |A| -> B, G: |B| -> C, FG: |A| -> C> Mul<G, FG> for F {
fn mul(&self, g: &G) -> FG {
|a| g(self(a))
}
} |
@eddyb where would you want to use a trait as return type in that example? |
@glaebhoerl sorry if it wasn't obvious, fn compose<A, B, C>(f: |A| -> B, g: |B| -> C) -> |A| -> C {
|a| g(f(a))
} |
@eddyb I realized that it's function composition :), what I don't see is what you would want to write. One of the main motivations for this feature is that you can't write the type of an unboxed closure. But in that example you don't have to -- it's already generic. |
It's generic over a constant inner anonymous type - maybe I should try to make this clearer. Suppose we have an impl Iterable<int, ???> for int { // Always returns the same number.
fn iter(&self) -> Iterator<int> {
// Pretend this is an anonymous type that can't be constructed
// outside of the iter method.
struct Foo {x: int};
impl Iterator<int> for Foo {
fn next(&self) -> Option<int> {
Some(self.x)
}
}
Foo {x: *self}
}
} |
Ah, I see what you're saying finally. Well, I don't have any ideas. Do you? |
@glaebhoerl well, the implementation looks complicated enough to warrant using an explicit type and impl |
That's pretty much what I was going to suggest as well. |
I've sketched out the details of implementing this, if anyone wants to try it. There could be some issues with variance, but nothing we can't solve. |
@eddyb yup, it should. @glaebhoerl Are you planning to write one? or @eddyb ? If so, pls link it here too. |
I definitely think there's a gap in the language here, and something like this proposal could be a good starting point. For the moment, though, I want to mention that there's a reasonable workaround using newtypes. Where you'd like to write something like
in your proposal, you can instead introduce a newtype with a private field:
By following this pattern, you hide the concrete type That said, I do think in the long run we'll want a more convenient way to hide concrete return types without using trait objects. |
@aturon sure, but this proposal is mainly useful for returning types unspeakable outside the function body (or not even within the function body). I should mention that the |
@aturon This proposal would also serve to insulate the function from the details of the various iterators it's employing. It's extremely awkward to try to return a |
How would you know the size of such a type? Or would you monomorphise the caller or something? |
@nick29581 As far as I understand the proposal, the idea is to monomorphize the callers, as you say. @glaebhoerl's comment above makes clear that return types are missing the abilities we have with argument types. We can take trait-bounded arguments via generics (static dispatch) or as objects (dynamic dispatch), in either case keeping the actual type abstract. But there's no similar choice for return types: you either return a concrete type or a trait object (forcing the client into dynamic dispatch). This is a real problem for things like iterators, where static dispatch is essential for performance, but you'd rather not reveal exactly how the iterator is built. As things currently stand, you can always use newtypes to return a "concrete" type while hiding everything but its trait bounds, as I proposed above. This is a pain. But I think it will work even for at least some proposed versions of unboxed closures, provided that you can implement the proposed closure trait directly -- at the cost of not being able to use closure sugar when you want to return a closure. |
To my understanding, it requires no monomorphization. The caller doesn't On Mon, May 19, 2014 at 8:49 PM, Aaron Turon [email protected]:
|
@cmr Agreed, the function chooses the type -- it's the caller that's monomorphized. The caller expects some type |
I've now written a formal RFC on something like this proposal: rust-lang/rfcs#105 |
Keeping around an issue here isn't necessary because this isn't backwards incompatible or actionable. A proposal needs to be accepted through the RFC process. |
skip `todo!()` in `never_loop` As promised in rust-lang#11450, here is an implementation which skips occurrences of the `todo!()` macro. changelog: [`never_loop`]: skip loops containing `todo!()`
Problem
Right now if you want to write a function that transforms an iterator you have to specify the type explicitly:
(1.)
If you want to hide the type of the iterator, you can use a trait object:
(2.)
but this has a performance cost: heap allocation and indirection.
One solution is return type inference:
(3.)
This is what C++14 adopts. It has several drawbacks:
Rust's policy of no type inference at the API level is a sound one, in my opinion.
The same problem will recur when we add unboxed closures.
Proposed solution
Allow writing:
(4.)
This syntax, a trait as the return type of a function, indicates a limited form of return type inference. Clients of the function may only use the return type according to the trait. The return type of the function must infer to a type that implements the trait.
This is sort of like an existential, but not quite, which is why I think thinking of it as return type inference is more accurate. If it were a true existential, the function could have several branches and return a different type, each implementing
Iterator
, from each branch. This is OK with trait objects, but clearly not representable in an unboxed form. Similarly, from a caller's perspective, if it were a true existential, two calls to the function could not be assumed to return the same type. But here it's quite reasonable to assume that they do.In the interest of least surprise, the return type of such a function with an inferred return type should be assumed to be equal only to itself:
In other words, when typechecking the rest of the program, for each function with such an inferred return type, the compiler would conjure a new anonymous type which it would pretend is the return type of the function, knowing only that it implements the trait. The true identity of this type (and the impl on it) would be determined when typechecking the function itself. In this way it should be possible to typecheck the body of the function and the rest of the program separately. (Edit: I think we would have to ban using this feature on trait methods though.)
This solution seems superior to the other three (explicit return type, trait object, or unlimited return type inference), especially if you also consider wanting to return a lambda when we have unboxed closures.
The text was updated successfully, but these errors were encountered: