Opaque type confusion

Hello, and good morning / afternoon, depending on where you are.

I was trying to play around with opaque types, and I don't get why something like this don't work.

What is the exact meaning of opaque type and how does it differ from a generic type? Also is there any way of making this small example work?

Addendum: I know that opaque types can't currently be in trait implementations, but I don't really get the error message of the compiler.

use std::fmt::Display;

pub struct MyStruct<T: Display>(T);

pub trait MyTrait<'a> {
    type Input;

    fn do_something<T>(&self, input: Self::Input) -> MyStruct<T>
    where
        T: Display;
}

pub struct MyOtherStruct {
    string: String,
}

impl MyOtherStruct {
    fn make_upper(&self, input: &str) -> MyStruct<impl Display> {
        MyStruct(input.to_uppercase())
    }
}

impl<'a> MyTrait<'a> for MyOtherStruct {
    type Input = &'a str;
    fn do_something<T>(&self, input: Self::Input) -> MyStruct<T>
    where
        T: Display,
    {
        self.make_upper(input)
    }
}

Compiler gives:

Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/lib.rs:29:9
   |
18 |     fn make_upper(&self, input: &str) -> MyStruct<impl Display> {
   |                                                   ------------ the found opaque type
...
25 |     fn do_something<T>(&self, input: Self::Input) -> MyStruct<T>
   |                     - this type parameter            ----------- expected `MyStruct<T>` because of return type
...
29 |         self.make_upper(input)
   |         ^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `T`, found opaque type
   |
   = note: expected struct `MyStruct<T>`
              found struct `MyStruct<impl std::fmt::Display>`
   = help: type parameters must be constrained to match other types
   = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters

error: aborting due to previous error

Part one, your error.

This signature says, "the caller of this function will determine the type T, and I will return a MyStruct<T>". But the body does:

And make_upper isn't generic, it can't match whatever T is, it returns a concrete type.

Here's your same code except I replaced the opaque type with String, and as you can see it generates the same error (modulo the type).

So this particular error isn't about opaque types. Either make_upper needs to be generic or do_something needs to not be generic to make it work. Your may have some confusion here in thinking impl Trait in return position is a generic return type, but it is not.


Part two, opaque types.

It's a type where you know some of the bounds, like the trait it implements, but you can't give it a concrete name. Closures are one example, impl Traits are another. In the case of impl Trait in return position, the type is still a single, opaque type -- it's not a generic return -- but it doesn't have a name you can type out. This will error for example:

use core::fmt::Display;
fn foo(b: bool) -> impl Display {
    if true { "" } else { 0 }
}

Because, although opaque, the return must be a single, concrete, static type.

Note that because the caller doesn't know what type it is, just what bounds it has, the implementer of the function is free to change what the concrete type is without breaking anything for the callers of the function. In addition to being able to return un-name-able types like closures, this is another common use of opaque types. If you change your function to return String instead of impl Display, you can't change it from String ever again without that being a breaking change. But if you keep it impl Display, you could change the actual returned type from String to u32 (or whatever) without breaking anything for callers of the function.

You can technically have a generic and opaque type with impl Trait in an argument position like so:

fn foo(b: impl Display + Default) {
    // Can't name the type in here, so I couldn't do something
    // like `T::default()`.  But I can still utilize the bounds:
    println!("{}", b);
}

But if you want it to have a name you can give it one:

fn bar<T: Display + Default>(b: T) {
    println!("{} --{}--", b, T::default());
}

And it's not going to make a difference to the caller.

4 Likes

To expand on this a little, the single concrete type can be a Box<trait> or & dyn trait so if you really need to return something to be used with a dynamic dispatch you could do e.g.

use core::fmt::Display;

fn foo(b: bool) -> &'static dyn Display {
    if b { &"hello" } else { &0 }
}

fn bar(b: bool) -> Box<dyn Display> {
    if b { Box::new("hello") } else { Box::new(0) }
}

fn main() {
    println!("{}", foo(true));
    println!("{}", foo(false));
    
    println!("{}", bar(true));
    println!("{}", bar(false));
}
3 Likes