-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Tracking issue for impl Trait
(RFC 1522, RFC 1951, RFC 2071)
#34511
Comments
@aturon Can we actually put the RFC in the repository? (@mbrubeck commented there that this was a problem.) |
Done. |
First attempt at implementation is #35091 (second, if you count my branch from last year). One problem I ran into is with lifetimes. Type inference likes to put region variables everywhere and without any region-checking changes, those variables don't infer to anything other than local scopes. One thing I thought of, that would have 0 impact on region-checking itself, is to erase lifetimes:
That last point about auto trait leakage is my only worry, everything else seems straight-forward. cc @rust-lang/lang |
But lifetimes are important with fn get_debug_str(s: &str) -> impl fmt::Debug {
s
}
fn get_debug_string(s: &str) -> impl fmt::Debug {
s.to_string()
}
fn good(s: &str) -> Box<fmt::Debug+'static> {
// if this does not compile, that would be quite annoying
Box::new(get_debug_string())
}
fn bad(s: &str) -> Box<fmt::Debug+'static> {
// if this *does* compile, we have a problem
Box::new(get_debug_str())
} I mentioned that several times in the RFC threads |
trait-object-less version: fn as_debug(s: &str) -> impl fmt::Debug;
fn example() {
let mut s = String::new("hello");
let debug = as_debug(&s);
s.truncate(0);
println!("{:?}", debug);
} This is either UB or not depending on the definition of |
@arielb1 Ah, right, I forgot that one of the reasons I did what I did was to only capture lifetime parameters, not anonymous late-bound ones, except it doesn't really work. |
@arielb1 Do we have a strict outlives relation we can put between lifetimes found in the concrete type pre-erasure and late-bound lifetimes in the signature? Otherwise, it might not be a bad idea to just look at lifetime relationships and insta-fail any direct or indirect |
Sorry for taking a while to write back here. So I've been thinking This all relates to my desire to make the set of region constraints Anyway, let me just lay out a bit of my thinking here. Let's work with this pub fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {...} I think the most accurate desugaring for a pub struct FooReturn<'a, 'b> {
field: XXX // for some suitable type XXX
}
impl<'a,'b> Iterator for FooReturn<'a,'b> {
type Item = <XXX as Iterator>::Item;
} Now the In some ways a better match is to consider a kind of synthetic trait: trait FooReturn<'a,'b> {
type Type: Iterator<Item=u32>;
}
impl<'a,'b> FooReturn<'a,'b> for () {
type Type = XXX;
} Now we could consider the trait FooReturn<'a,'b> {
type Type: Iterator<Item=u32>;
}
impl<'a,'b> FooReturn<'a,'b> for () {
default type Type = XXX; // can't really be specialized, but wev
} In this case, Anyway, my point in exploring these potential desugarings is not to One place that this projection desugaring is a really useful guide is At trans time, and for auto-traits, we will have to know what If you look both at the struct desugaring or the impl, there is an This is where the Let's look at an actual example: fn foo<'a,'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
if condition { x.iter().cloned() } else { y.iter().cloned() }
} Here, the type if
If we then instantiate the variable 0 to
So what value should we use for Note that if we just run the normal algorithm, we would wind up with OK, that's as far as I've gotten. =) |
On the PR #35091, @arielb1 wrote:
I thought it would make more sense to discuss here. @arielb1, can you elaborate more on what you have in mind? In terms of the analogies I made above, I guess you are fundamentally talking about "pruning" the set of lifetimes that would appear either as parameters on the newtype or in the projection (i.e., I don't think that the lifetime elision rules as they exist would be a good guide in this respect: if we just picked the lifetime of Anyway, it'd be great to see some examples that illustrate the rules you have in mind, and perhaps any advantages thereof. :) (Also, I guess we would need some syntax to override the choice.) All other things being equal, if we can avoid having to pick from N lifetimes, I'd prefer that. |
Implement `impl Trait` in return type position by anonymization. This is the first step towards implementing `impl Trait` (cc #34511). `impl Trait` types are only allowed in function and inherent method return types, and capture all named lifetime and type parameters, being invariant over them. No lifetimes that are not explicitly named lifetime parameters are allowed to escape from the function body. The exposed traits are only those listed explicitly, i.e. `Foo` and `Clone` in `impl Foo + Clone`, with the exception of "auto traits" (like `Send` or `Sync`) which "leak" the actual contents. The implementation strategy is anonymization, i.e.: ```rust fn foo<T>(xs: Vec<T>) -> impl Iterator<Item=impl FnOnce() -> T> { xs.into_iter().map(|x| || x) } // is represented as: type A</*invariant over*/ T> where A<T>: Iterator<Item=B<T>>; type B</*invariant over*/ T> where B<T>: FnOnce() -> T; fn foo<T>(xs: Vec<T>) -> A<T> { xs.into_iter().map(|x| || x): $0 where $0: Iterator<Item=$1>, $1: FnOnce() -> T } ``` `$0` and `$1` are resolved (to `iter::Map<vec::Iter<T>, closure>` and the closure, respectively) and assigned to `A` and `B`, after checking the body of `foo`. `A` and `B` are *never* resolved for user-facing type equality (typeck), but always for the low-level representation and specialization (trans). The "auto traits" exception is implemented by collecting bounds like `impl Trait: Send` that have failed for the obscure `impl Trait` type (i.e. `A` or `B` above), pretending they succeeded within the function and trying them again after type-checking the whole crate, by replacing `impl Trait` with the real type. While passing around values which have explicit lifetime parameters (of the function with `-> impl Trait`) in their type *should* work, regionck appears to assign inference variables in *way* too many cases, and never properly resolving them to either explicit lifetime parameters, or `'static`. We might not be able to handle lifetime parameters in `impl Trait` without changes to lifetime inference, but type parameters can have arbitrary lifetimes in them from the caller, so most type-generic usecases (or not generic at all) should not run into this problem. cc @rust-lang/lang
I haven't seen interactions of |
The "explicit" way to write fn foo<'a: 'c,'b: 'c,'c>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> + 'c {
if condition { x.iter().cloned() } else { y.iter().cloned() }
} Here there is no question about the lifetime bound. Obviously, having to write the lifetime bound each time would be quite repetitive. However, the way we deal with that kind of repetition is generally through lifetime elision. In the case of I am opposed to adding explicitness-sensitive lifetime elision as @eddyb did only in the specific case of |
@arielb1 hmm, I'm not 100% sure how to think about this proposed syntax in terms of the "desugarings" that I discussed. It allows you to specify what appears to be a lifetime bound, but the thing we are trying to infer is mostly what lifetimes appear in the hidden type. Does this suggest that at most one lifetime could be "hidden" (and that it would have to be specified exactly?) It seems like it's not always the case that a "single lifetime parameter" suffices: fn foo<'a, 'b>(x: &'a [u32], y: &'b [u32]) -> impl Iterator<Item=u32> {
x.iter().chain(y).cloned()
} In this case, the hidden iterator type refers to both |
So @aturon and I discussed this issue somewhat and I wanted to share. There are really a couple of orthogonal questions here and I want to separate them out. The first question is "what type/lifetime parameters can potentially be used in the hidden type?" In terms of the (quasi-)desugaring into a fn foo<'a, 'b, T>() -> impl Trait { ... } would get desugared to something like: fn foo<'a, 'b, T>() -> <() as Foo<...>>::Type { ... }
trait Foo<...> {
type Type: Trait;
}
impl<...> Foo<...> for () {
default type Type = /* inferred */;
} then this question comes down to "what type parameters appear on the trait The default answer we've been using is "all of them", so here This has an effect on the outlives relation, since, in order to determine that This is why I say it is not enough to consider lifetime parameters: imagine that we have some call to So I imagine that what we need is:
@aturon spitballed something like fn foo<'a, 'b, T>(...) -> impl<T> Trait { } to indicate that the hidden type does not in fact refer to As for the defaults, it seems like having more data would be pretty useful -- but the general logic of elision suggests that we would do well to capture all the parameters named in the type of Another related note is what the meaning of a lifetime bound is. I think that sound lifetime bounds have an existing meaning that should not be changed: if we write Here it just behaves exactly as the desugaring would behave: fn foo<'a, 'b, T>() -> <() as Foo<'a, 'b, 'T>>::Type { ... }
trait Foo<'a, 'b, T> {
type Type: Trait + 'static; // <-- note the `'static` bound appears here
}
impl<'a, 'b, T> Foo<...> for () {
default type Type = /* something that doesn't reference `'a`, `'b`, or `T` */;
} All of this is orthogonal from inference. We still want (I think) to add the notion of a "choose from" constraint and modify inference with some heuristics and, possibly, exhaustive search (the experience from RFC 1214 suggests that heuristics with a conservative fallback can actually get us very far; I'm not aware of people running into limitations in this respect, though there is probably an issue somewhere). Certainly, adding lifetime bounds like |
Possible options: Explicit lifetime bound with output parameter elisionLike trait objects today, Disadvantage: unergonomic Explicit lifetime bounds with "all generic" elisionLike trait objects today, However, elision creates a new early-bound parameters that outlives all explicit parameters: fn foo<T>(&T) -> impl Foo
-->
fn foo<'total, T: 'total>(&T) -> impl Foo + 'total Disadvantage: adds an early-bound parameter more. |
I ran into this issue with impl Trait +'a and borrowing: #37790 |
I think I found a bug in the current Codetrait Collection {
type Element;
}
impl<T> Collection for Vec<T> {
type Element = T;
}
existential type Existential<T>: Collection<Element = T>;
fn return_existential<I>(iter: I) -> Existential<I::Item>
where
I: IntoIterator,
I::Item: Collection,
{
let item = iter.into_iter().next().unwrap();
vec![item]
} Error
You can find this on stackoverflow, too. |
I'm not 100% sure we can support this case out of the box, but what you can do is rewrite the function to have two generic parameters: fn return_existential<I, J>(iter: I) -> Existential<J>
where
I: IntoIterator<Item = J>,
{
let item = iter.into_iter().next().unwrap();
vec![item]
} |
Thanks! fn return_existential<I, T>(iter: I) -> Existential<T>
where
I: IntoIterator<Item = T>,
I::Item: Collection,
{
let item = iter.into_iter().next().unwrap();
vec![item]
} |
Are there plans for |
impl trait in traits is a separate feature to the ones being tracked here, and does not presently have an RFC. There is a fairly long history of designs in this space, and further iteration is being held off until the implementation of 2071 (existential type) is stabilized, which is blocked on implementation issues as well as unresolved syntax (which has a separate RFC). |
@cramertj The syntax is nearly resolved. I believe the main blocker is GAT now. |
@alexreg: rust-lang/rfcs#2515 is still waiting on @withoutboats. |
@varkor Yeah, I'm just being optimistic they'll see the light with that RFC soon. ;-) |
Will something like the following be possible? #![feature(existential_type)]
trait MyTrait {}
existential type Interface: MyTrait;
struct MyStruct {}
impl MyTrait for MyStruct {}
fn with<F, U>(cb: F) -> U
where
F: FnOnce(&mut Interface) -> U
{
let mut s = MyStruct {};
cb(&mut s)
} |
You can do this now, although only with a #![feature(existential_type)]
trait MyTrait {}
existential type Interface: MyTrait;
struct MyStruct {}
impl MyTrait for MyStruct {}
fn with<F, U>(cb: F) -> U
where
F: FnOnce(&mut Interface) -> U
{
fn hint(x: &mut MyStruct) -> &mut Interface { x }
let mut s = MyStruct {};
cb(hint(&mut s))
} |
How would you write it if the callback would be able to choose its argument type? Actually nvm, I guess you could solve that one via a normal generic. |
@CryZe What you're looking for is unrelated to It would potentially look something like that : trait MyTrait {}
struct MyStruct {}
impl MyTrait for MyStruct {}
fn with<F, U>(cb: F) -> U
where
F: for<I: Interface> FnOnce(&mut I) -> U
{
let mut s = MyStruct {};
cb(hint(&mut s))
} |
@KrishnaSannasi Ah, interesting. Thanks! |
Is this supposed to work? #![feature(existential_type)]
trait MyTrait {
type AssocType: Send;
fn ret(&self) -> Self::AssocType;
}
impl MyTrait for () {
existential type AssocType: Send;
fn ret(&self) -> Self::AssocType {
()
}
}
impl<'a> MyTrait for &'a () {
existential type AssocType: Send;
fn ret(&self) -> Self::AssocType {
()
}
}
trait MyLifetimeTrait<'a> {
type AssocType: Send + 'a;
fn ret(&self) -> Self::AssocType;
}
impl<'a> MyLifetimeTrait<'a> for &'a () {
existential type AssocType: Send + 'a;
fn ret(&self) -> Self::AssocType {
*self
}
} |
Do we have to keep |
@jethrogb Yes. The fact that it currently doesn't is a bug. |
@cramertj Ok. Should I file a separate issue for that or is my post here enough? |
Filing an issue would be great, thanks! :) |
I think the intention is to immediately deprecate this when the type-alias-impl-trait feature is implemented (i.e., put in a lint) and eventually remove it from the syntax. Someone can maybe clarify though. |
Closing this in favor of a meta-issue which tracks |
NEW TRACKING ISSUE = #63066
Implementation status
The basic feature as specified in RFC 1522 is implemented, however there have been revisions that are still in need of work:
impl Trait
requires a named lifetime #49287impl Trait
Lifetime Elision #43396let x: impl Trait
static
andconst T: impl Trait
abstract type
RFCs
There have been a number of RFCs regarding impl trait, all of which are tracked by this central tracking issue.
impl Trait
rfcs#1522impl Trait
, while expanding it to arguments rfcs#1951abstract type
in modules and implsMyTrait<AssociatedType: Bounds>
rfcs#2289 to match that syntax if that RFC gets merged.let
,const
, andstatic
positionsimpl Trait
anddyn Trait
with multiple bounds rfcs#2250impl Trait
anddyn Trait
with multiple boundsUnresolved questions
The implementation has raised a number of interesting questions as well:
impl
keyword when parsing types? Discussion: 1Send
forwhere F: Fn() -> impl Foo + Send
?impl Trait
anddyn Trait
with multiple bounds rfcs#2250.+
inimpl Trait
/dyn Trait
#45294impl Trait
after->
infn
types or parentheses sugar? [impl Trait] Should we allowimpl Trait
after->
infn
types or parentheses sugar? #45994fn foo<T>(x: impl Iterator<Item = T>>)
?impl Trait
as arguments in the list, permitting migrationexistential type Foo: Bar
ortype Foo = impl Bar
? (see here for discussion)existential type
in an impl be just items of the impl, or include nested items within the impl functions etc? (see here for example)The text was updated successfully, but these errors were encountered: