Desugar Return Impl Trait Within Trait

How does a return positioned impl inside a trait desugar into? I have this code below:

trait Foo {
    fn get(&self) -> impl Sized;
}

struct TheFoo(String);

impl Foo for TheFoo {
    fn get(&self) -> impl Sized {
        &self.0
    }
}

This compiles just fine. But as my understanding, the trait Foo aboves should be desugar into something like this:

trait Foo {
    type Output: Sized;
    fn get(&self) -> Self::Output;
}

But, then, how do I implemented it? I tried this:

struct TheFoo(String);

impl Foo for TheFoo {
    type Output = &String;
    fn get(&self) -> impl Sized {
        &self.0
    }
}

But, it doesn't compile because the lifetime of &String in the type Output = &String is not named.

error: in the trait associated type is declared without lifetime parameters, so using a borrowed type for them requires that lifetime to come from the implemented type
  --> src/lib.rs:21:19
   |
21 |     type Output = &String;
   |                   ^ this lifetime must come from the implemented type

What is actually happening here?
To be honest, I thought the first code should not compile as well because we expect impl Sized that doesn't say anything about lifetime. But, I returned &self.0 which captures the lifetime of self. I thought it shouldn't compile, but it does.


As a note: I don't have any specific use case. I'm just trying to understand what's the correct mental model for associated type and the semantic of returned impl Trait.

You can think of it like this:

trait Foo {
    type Rpit<'this>: Sized where Self: 'this;
    fn get(&self) -> Self::Rpit<'_>;
}

struct TheFoo(String);

impl Foo for TheFoo {
    type Rpit<'this> = &'this String;
    fn get(&self) -> Self::Rpit<'_> {
        &self.0
    }
}

Except the GAT is unnameable, and never normalizes (so its generic parameters are always invariant).

Though as I understand it's technically closer to something like this...
//           vv generics from the method
trait FooGet<'a> {
    type Rpit: Sized; // <-- bounds from the `-> impl ..`
}

// Made up syntax accouting for implied bounds in the method signature
//             vvvvvvvvvvvvvvvvvvv
trait Foo: for <'a where Self: 'a> FooGet<'a> {
    fn get<'this>(&'this self) -> <Self as FooGet<'this>>::Rpit;
}

struct TheFoo(String);

impl<'a> FooGet<'a> for TheFoo {
    type Rpit = &'a String;
}

impl Foo for TheFoo {
    fn get<'this>(&'this self) -> <Self as FooGet<'this>>::Rpit {
        &self.0
    }
}

-> impl Trait in traits captures all generics in scope. Hence a GAT, not a non-generic associated type. You're suppose to just know what gets captured when you see -> impl (poor design IMO).

2 Likes

I see.... I think I was mistaken because I thought returned impl captures only type parameter, not lifetime. I just realized the semantic changed just recently. But, isn't this breaking change?

It's all in the blog post, but anyway, -> impl Trait in traits, like async fn, has always captured everything, whereas outside of traits it captures everything but unmentioned lifetimes -- until edition 2024 where it will also capture everything. But you can now select which lifetimes get captured outside of traits with + use<'a, 'b> bounds (on any edition).

1 Like