Generalising a function to take and return type implementing trait (compiler error)

Newbie question here - I have a simple function that takes a grey-scale image (I'm using the image crate) and returns a resized grey-scale image.

fn shrink_image(img: image::GrayImage, block_size: u32) -> image::GrayImage {
    let (width, height) = img.dimensions();
    let (new_width, new_height) = (width / block_size, height / block_size);

    resize(&img, new_width, new_height, FilterType::Lanczos3)
}

This works fine in my program, but it doesn't really have to only be working on grey-scale images, does it? So I wanted to make it more general so that it could take any image.

By looking at the docs for image I figured that image::GreyImage is just a type synonym for ImageBuffer<Luma<u8>, Vec<u8>> and that, in turn, ImageBuffer implements the GenericImageView trait, which is where the dimensions and resize methods I used in the function are coming from.

So I tried modifying the function so it takes and return an object of type T that implements GenericImageView:

fn shrink_image<T: GenericImageView>(img: T, block_size: u32) -> T {
    let (width, height) = img.dimensions();
    let (new_width, new_height) = (width / block_size, height / block_size);

    resize(&img, new_width, new_height, FilterType::Lanczos3)
}

But I get this compile error

error[E0308]: mismatched types
  --> src/lib.rs:46:5
   |
42 |   fn shrink_image<T: GenericImageView>(img: T, block_size: u32, squeeze: u8) -> T {
   |                   - expected this type parameter                                - expected `T` because of return type
...
46 | /     resize(&img, new_width * squeeze as u32, new_height, FilterType::Lanczos3,
47 | |     )
   | |_____^ expected type parameter `T`, found `ImageBuffer<<... as GenericImageView>::Pixel, ...>`
   |
   = note: expected type parameter `T`
                      found struct `ImageBuffer<<T as GenericImageView>::Pixel, Vec<<<T as GenericImageView>::Pixel as Pixel>::Subpixel>>`

For more information about this error, try `rustc --explain E0308`.

What am I doing wrong?

1 Like

the signature of resize() looks like this:

pub fn resize<I: GenericImageView>(
    image: &I,
    nwidth: u32,
    nheight: u32,
    filter: FilterType,
) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>>

note it takes an generic type I which implements GenericImageView, but returns a ImageBuffer, not the input the type I. the reason being, GenericImageView, as the name suggested, is a "view" type, not necessarily "owns" the image. but the resize() operation needs to modify the image so it cannot simply return a "view" of the original image.

if this helps to understand, you can think it like str::to_ascii_uppercase(), where you take a borrowed &str but returns an owned String because it needs to change the contents.

to solve this, you have two options:

  • instead of taking a T: GenericImageView as argument, take an &ImageBuffer, something like:

    fn shrink_image<P: Pixel>(
        image: &ImageBuffer<P, Vec<P::Subpixel>>,
        block_size: usize,
        squeeze: u8
    ) -> ImageBuffer<P, Vec<P::Subpixel>> {
        //...
    }
    
  • change the return type to mimic signature of resize(), i.e.

    fn shrink_image<T: GenericImageView>(
        image: &T,
        block_size: usize,
        squeeze: u8
    ) -> ImageBuffer<T::Pixel, Vec<<T::Pixel as Pixel>::Subpixel>
    where ... {
        //...
    }
    

between the two, the latter is more preferable because it's more general.

5 Likes

Thanks! I got it working based the second method you suggested (I also had to add the lifetime annotation following the compiler error):

fn shrink_image<T: GenericImageView>(
    img: &T,
    block_size: u32
) -> ImageBuffer<T::Pixel, Vec<<T::Pixel as Pixel>::Subpixel>>
where
    <T as GenericImageView>::Pixel: 'static
{
    let (width, height) = img.dimensions();
    let (new_width, new_height) = (width / block_size, height / block_size);

    resize(img, new_width, new_height, FilterType::Lanczos3)
}

Let me check if I understood your explanation correctly...

So the input type is any type T that implements GenericImageView, but that could be anything, and in particular (as the name "view" suggest) it could be a type that potentially doesn't even own the image data.

However, resize needs to build an actual, concrete image, so that necessarily has to be an ImageBuffer.

For this reason, we cannot require that for any input type T (implementing the trait) the output is the same T; that could maybe work if the input is an actual ImageBuffer but wouldn't work in all other cases.

Did I get it right?

1 Like

yes, that's correct.

here's how to read the type signature of resize(), especially the (fully qualified) return type ImageBuffer<<I as GenericImageView>::Pixel, Vec<<<I as GenericImageView>::Pixel as Pixel>::Subpixel>>

the resize() function essentially creates a new image (with type ImageBuffer), whose content is re-sampled from the given image, so the input argument type must implements GenericImageView, which determines how to access the pixels of the image,

the function also needs to allocate memory for the new image, and the implementation choose to use the standard Vec container to store the pixels. however, the pixel format (e.g. R8G8B8, or B8G8R8A8, or , Luma8, etc) is derived from the input image view: the GenericImageView has an associated type called Pixel, which in turn is bounded by the Pixel trait and has an associated type called Subpixel.

in short, resize() takes any type implementing GenericImageView and always returns a ImageBuffer type. ImageBuffer itself has generic type parameters, which are is derived from the input type use the associated types of GenericImageView trait.

1 Like

You can also return impl GenericImageView (or GenericImage).

fn shrink_image<T>(img: T, block_size: u32) -> impl GenericImageView
where
    T: GenericImageView,
    T::Pixel: 'static,
{
    let (width, height) = img.dimensions();
    let (new_width, new_height) = (width / block_size, height / block_size);

    resize(&img, new_width, new_height, FilterType::Lanczos3)
}

The problem with your first approach is that the caller chooses T, which could be different from what the function returns. In fact, when calling shrink_image<GrayImage>, the function must return GrayImage. This is not the case when returning impl T, where the callee chooses the return type.

Thanks! I was thinking about something along those lines actually (it's true that I return a ImageBuffer<...> but that still implements the GenericImageView...)

The function seems to compile OK like this, but now I get an error in the part of the code where I'm trying to use the return value; in fact, at some point I'm passing that return value to a function that actually expects an ImageBuffer.

error[E0308]: mismatched types
  --> src/lib.rs:97:33
   |
41 | fn shrink_image<T>(img: &T, config: &Config) -> impl GenericImageView
   |                                                 --------------------- the found opaque type
...
97 |     println!("{}", img_to_ascii(img_smaller));
   |                    ------------ ^^^^^^^^^^^ expected `ImageBuffer<Luma<u8>, ...>`, found opaque type
   |                    |
   |                    arguments to this function are incorrect
   |
   = note:   expected struct `ImageBuffer<Luma<u8>, Vec<u8>>`
           found opaque type `impl GenericImageView`

What's the best way to handle that? The receiving function actually needs a specific type (grey scale image) in order to do its job. But the shrink_image could be generic...

You should stick to @nerditation's answer in that case. If you need the actual precise type (the "hidden" type) and can't operate on the trait, you have to return it.

The reference says:

Functions can use impl Trait to return an abstract return type. These types stand in for another concrete type where the caller may only use the methods declared by the specified Trait.

1 Like