How to turn an 'impl Foo' of 'i32' back into an 'i32'?

trait Foo { }

impl Foo for i32 { }

fn make_foo(foo: i32) -> impl Foo {
    foo
}

fn main() {
    let foo = make_foo(42);
    
    // How can I turn an 'impl Foo' back into an 'i32'?
    let answer: i32 = foo; // fails !
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:12:23
   |
5  | fn make_foo(foo: i32) -> impl Foo {
   |                          -------- the found opaque type
...
12 |     let answer: i32 = foo; // fails !
   |                 ---   ^^^ expected `i32`, found opaque type
   |                 |
   |                 expected due to this
   |
   = note:     expected type `i32`
           found opaque type `impl Foo`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

You can’t. Unless the trait Foo itself provides a method to convert self to i32. The whole point of an impl Foo return type is that the return type of make_foo is allowed to change without causing breakage, so it isn’t possible to rely on the fact that the value is of type i32.

Well actually you can “cheat” by using the Any trait... which can of course cause panics (at the unwrap) if the opaque return type of make_foo ever changes.

use std::any::Any;

trait Foo { }

impl Foo for i32 { }

fn make_foo(foo: i32) -> impl Foo {
    foo
}

fn main() {
    let foo = make_foo(42);
    
    let answer: i32 = *(&foo as &dyn Any).downcast_ref().unwrap();
}

The actual type is abstracted away behind impl Foo and what you have available to use is about the same as you would have in this function, where x is a value of a type that implements Foo:

fn function<T: Foo>(x: Foo) {
}

What operations are available on x? This follows Rust's parametric model for generics- all operations available on x come from the trait bounds - so you only have whatever the Foo trait allows.

To follow the parametric model, we would turn to the design of Foo to answer the question. Foo must be designed to support your use case, for example have a method for such a (fallible?) conversion.

Now there is an asterisk - the parametric model is not 100% correct for impl Foo - and steffahn is showing something that takes advantage of it. Some properties of impl Foo "leak", and you can use Any here even if it requires a 'static bound, the 'static-ness leaks! (Edit: fixed, staticness doesn't leak here, it's just the syntactical default, like steffahn said.) Another example of a leak is the Send/Syncness of the underlying value. These are conscious trade-offs that were made in this feature in Rust.

3 Likes

I think it’s more a case of impl Foo being a shorthand of impl Foo + 'static.

I tested this while posting, and it doesn't look like that.

Mind sharing the test case? Also I’m having a hard time finding any documentation w.r.t. lifetimes in the context of impl Trait return types.

Sure, just this so far - using your example. Playground link.

trait Foo { }

impl Foo for i32 { }

fn make_foo<'a>(foo: i32) -> impl Foo + 'a {
    foo
}

fn only_static<T: 'static>(_x: T) { }

fn main() {
    let foo = make_foo(42);
    only_static(foo);
}

Doesn’t type inference just pick 'a == 'static for the make_foo call here?

I don't know about type inference in that way I mean, that would betray the meaning of the function's return type?

Here's a playground link for the more detailed question I had to help double check this: playground link

While I’m still trying to understand your point, here’s mine:

trait Foo { }

impl Foo for &i32 { }

fn make_foo(foo: &i32) -> impl Foo {
    foo
}

doesn’t compile, which is IMO because impl Foo stands for impl Foo + 'static. The error message is the same as if I wrote impl Foo + 'static directly; and changing it to impl Foo + '_ makes it compile.

2 Likes

I agree with you after testing the same thing.

The lifetime is a generic, so it is chosen by the caller, and the caller will just choose to call make_foo::<'static> and get an impl Foo + 'static. The second case fails because the caller cannot call make_foo_ref::<'static> with a non-static reference to x. Not because impl Foo is leaking details.

1 Like

Thanks for all your insights! And I already had a gut feeling that it has to be that way..

How would such an interface look like in case of i32 (as well as a totally genric one?) could this be formulated via an default implementation on Foo?

I think the first question to ask here is why you want to have an impl Foo return type instead of an i32 return type. If the goal is to change the type in the future without breakage, but you still want users to be able to convert to an i32 then you could add a method to Foo that does (self) -> i32 or (&self) -> i32 or perhaps (that’s the “fallible” option) even (&self) -> Option<i32>.

The fallible option might still come with a guarantee (in the documentation of make_foo) that the result of make_foo can be converted successfully. By the way, instead of modifying Foo you can also add additional trait bounds to the impl return type. E.g. by writing fn make_foo(foo: i32) -> impl Foo + Into<i32>, you allow callers to use the .into() method to get back an i32.

If you don’t plan on ever returning anything else but an i32, just let that be the return type. The most important use-case of impl Trait return types lies in unnamable types anyways, e.g. (types involving) closures or async block futures.

3 Likes

My motivation is the following: The Foo trait assoziates the i32 with a const generic for some computations and once they are done I need back the i32 but dont care about the const anymore. But in the end I dont want my computation to be restriced to i32 but also work on f32, etc..

trait Foo<const C: usize> {
    fn back(self) -> Self where Self: Sized {
        self
    }
}

impl<const C: usize> Foo<C> for i32 { }
impl<const C: usize> Foo<C> for f32 { }

fn make_foo<T: Foo<C>, const C: usize>(foo: T) -> impl Foo<C> {
    foo
}

fn main() {
    let foo = make_foo::<_, 23>(42); // or: let foo = make_foo::<_, 23>(42.0);
    
    // ... do stuff with foo relying on the const generic ...
    
    // How can I turn an 'impl Foo' back into an 'i32'?
    let answer: i32 = foo.back(); // fails !
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:20:23
   |
10 | fn make_foo<T: Foo<C>, const C: usize>(foo: T) -> impl Foo<C> {
   |                                                   ----------- the found opaque type
...
20 |     let answer: i32 = foo.back(); // fails !
   |                 ---   ^^^^^^^^^^ expected `i32`, found opaque type
   |                 |
   |                 expected due to this
   |
   = note:     expected type `i32`
           found opaque type `impl Foo<23_usize>`

EDIT:
Hey actually this made the trick:

fn make_foo<T: Foo<C>, const C: usize>(foo: T) -> impl Foo<C> + Into<T> {
    foo
}

Alright, just to avoid any potential XY problems here, can you give an example of the kind of API that’s doing “stuff relying on the const generic” that you have in mind?

Is this mostly just a way to use type inference with opaque types to avoid repeating the same C parameter multiple times?

At least judging by the example code you provided so far, I guess that the fn make_foo<T: Foo<C>, const C: usize>(foo: T) -> impl Foo<C> + Into<T> approach might work out well.

1 Like

Yes thats the main motivation here :wink:
Thanks for your help!

I just found answers in the original RFC – well, actually this is the second RFC for impl Trait return types.

This RFC proposes that all type parameters are considered in scope for impl Trait in return position, as per Assumption 2 (which claims that this suffices for most use-cases) and Assumption 1 (which claims that we'll eventually provide an explicit syntax with finer-grained control).

The lifetimes in scope include only those mentioned "explicitly" in a bound on the impl Trait . That is:

  • For impl SomeTrait + 'a , the 'a is in scope for the concrete witness type.
  • For impl SomeTrait + '_ , the lifetime that elision would imply is in scope (this is again using the strawman shorthand syntax for an elided lifetime).

Note, however, that the witness type can freely mention type parameters, which may themselves involve embedded lifetimes. Consider, for example:

fn transform(iter: impl Iterator<Item = u32>) -> impl Iterator<Item = u32>

Here, if the actual argument type was SomeIter<'a> , the return type can mention SomeIter<'a> , and therefore can indirectly mention 'a .

and

It's worth noting that this treatment of lifetimes is related but not identical to the way they're handled for trait objects.

In particular, Box<SomeTrait> imposes a 'static requirement on the underlying object, while Box<SomeTrait + 'a> only imposes a 'a constraint. The key difference is that, for impl Trait , in-scope type parameters can appear, which indirectly mention additional lifetimes, so impl SomeTrait imposes 'static only if those type parameters do:

// In these cases, we know that the concrete return type is 'static
fn foo() -> impl SomeTrait;
fn foo(x: u32) -> impl SomeTrait;
fn foo<T: 'static>(t: T) -> impl SomeTrait;

// In the following case, the concrete return type may embed lifetimes that appear in T:
fn foo<T>(t: T) -> impl SomeTrait;

// ... whereas with Box, the 'static constraint is imposed
fn foo<T>(t: T) -> Box<SomeTrait>;

This difference is a natural one when you consider the difference between generics and trait objects in general -- which is precisely that with generics, the actual types are not erased, and hence auto traits like Send work transparently, as do lifetime constraints.

So when there’s generic type parameters present, then impl Foo isn’t the same as impl Foo + 'static anymore. For example fn foo<T: Foo>(x: T) -> impl Foo { x } compiles, whereas fn foo<T: Foo>(x: T) -> impl Foo + 'static { x } doesn’t. I guess this means that if you want to have an

impl bar<'a, T>(x: T, y' &'a Baz) -> impl Qux

and your return value is supposed to include both x and y then you’ll probably have to introduce another lifetime parameter

impl bar<'r, 'a: 'r, T: 'r>(x: T, y' &'a Baz) -> impl Qux + 'r

Some testing revealed that lifetimes that are e.g. parameters to the trait also are included, so e.g.

trait Baz<'a> {}
impl<T: ?Sized> Baz<'_> for T {}


fn foo<T>(a: T, b: &i32) -> impl Baz<'_> {
    (a, b)
}

compiles, too, while

trait Bar {}
impl<T: ?Sized> Bar for T {}


fn foo<T>(a: T, b: &i32) -> impl Bar {
    (a, b)
}

doesn’t.

3 Likes