Search code examples
rustlazy-loadingborrowing

Implementing a lazy load in by way of enum type in Rust


In my use case, I have a large list of Layer types that consist of image data and some metadata about the image:

extern crate image;

type Img = image::ImageBuffer<image::Rgba<u8>, Vec<u8>>;

#[derive(Debug, Clone)]
struct Layer {
    // some metadata fields
    lazy_image: Img,
}

So far so good. However, I perform some culling of layers before actually processing the image data, so I'd like to perform the image loading in a lazy manner to avoid having to load up all the images I won't actually need.

This was my first attempt:

extern crate image;

type Img = image::ImageBuffer<image::Rgba<u8>, Vec<u8>>;

#[derive(Debug, Clone)]
enum LazyImage {
    Path(String),
    Image(Img)
}

#[derive(Debug, Clone)]
struct Layer {
    // some metadata fields
    lazy_image: LazyImage,
}

impl Layer {
    fn get_image(&mut self) -> Img {
        match self.lazy_image {
            LazyImage::Image(img) => img,
            LazyImage::Path(path) => {
                let img = image::open(path).expect("Could not open image").to_rgba();
                self.lazy_image = LazyImage::Image(img);
                img
            }
        }
    }
}

As you can guess, this doesn't work because of ownership issues with my match statement in get_image. The compiler suggests I borrow the self.lazy_image, essentially doing:

impl Layer {
    fn get_image(&mut self) -> Img {
        match &self.lazy_image {
            LazyImage::Image(img) => img,
            LazyImage::Path(path) => {
                let img = image::open(path).expect("Could not open image").to_rgba();
                self.lazy_image = LazyImage::Image(img);
                img
            }
        }
    }
}

Now, however, I have type issues: the first branch (if the image is already loaded) returns a reference &Img instead of the actual Img. Okay, no problem, lets go full reference. Only catch is that, since I'm performing processing on these images, they need to be mutable:

impl Layer {
    fn get_image(&mut self) -> &mut Img {
        match &self.lazy_image {
            LazyImage::Image(img) => img,
            LazyImage::Path(path) => {
                let img = image::open(path).expect("Could not open image").to_rgba();
                self.lazy_image = LazyImage::Image(img);
                &mut img
            }
        }
    }
}

Now they're the same type, but differ in mutability: in the first branch (if the images is already loaded) I get an immutable reference. I've tried flailing around a bit more but have been unsuccessful in getting this to do what I want.

As you may be able to tell, I'm somewhat new to Rust and am flailing a little bit. Moreover, I'm not actually sure what I want to be doing in order to implement my desired goal.

Any help, whether telling me how to satisfy the compiler, or even just telling me I'm going about this all wrong, would be greatly appreciated.


Solution

  • I'd suggest implementing a get() method directly on the LazyImage type, since it feels that loading the image is an internal matter of that type.

    Since you most likely do not want to move the image out of the data structure, you should return an &mut Img. This reference in turn needs to point to the value stored within the Image enum variant. An enum variant field can only be extracted by desctructuring, so we need to destructure again after loading the image.

    One possible solution is to use two if let statements for destructuring:

    impl LazyImage {
        fn get(&mut self) -> &mut Img {
            if let LazyImage::Path(path) = self {
                let img = image::open(path).expect("Could not open image").to_rgba();
                *self = LazyImage::Image(img);
            }
            if let LazyImage::Image(img) = self {
                img
            } else {
                unreachable!()
            }
        }
    }
    

    After the first if let, the enum is guaranteed to be an Image. The second destructuring could also be done using a match expression. The compiler will insist that we include the else case, since the compiler can't easily see the other branch is unreachable.

    Note that we make use of match ergonomics to avoid some noise in the code.