Help with generics and type mismatch error


#1

I am still quite new to Rust and this is my first posting to this community forum. I apologize if this is a simple thing. I have run into a particularly tricky type mismatch error recently when trying to write some code that relies on generics. I’ve tried to create a minimal example below and would appreciate any feedback that can be provided. The error occurs within the ‘new’ function.

use std::io::Error;
use std::io::ErrorKind;

pub struct Raster<T: RasterDataFile> {
    file_name: String,
    file_mode: String,
    raster: T,
    raster_type: RasterType,
}

impl<T: RasterDataFile> Raster<T> {
    pub fn new<'a>(file_name: &'a str, file_mode: &'a str) -> Result<Raster<T>, Error> {
        match get_raster_type_from_file(file_name.to_string()) {
            RasterType::MyRaster => {
                let mrf = MyRasterFile { value: 10.0f64 };
                // This is where the error occurs.
                // Error states,
                // "Mismatched types Expected type Raster<T> found type Raster<MyRasterFile>"
                return Ok(Raster {
                    file_name: file_name.to_string(),
                    file_mode: file_mode.to_string(),
                    raster: mrf,
                    raster_type: RasterType::MyRaster
                });
            },
            RasterType::Unknown => { return Err(Error::new(ErrorKind::Other, "Unrecognized raster type")); },
        }
        Err(Error::new(ErrorKind::Other, "Error creating raster"))
    }
}

pub trait RasterDataFile {
    fn get_value(&self) -> f64;
}

pub enum RasterType {
    Unknown,
    MyRaster
}

pub struct MyRasterFile {
    value: f64,
}

impl RasterDataFile for MyRasterFile {
    fn get_value(&self) -> f64 {
        self.value
    }
}

fn get_raster_type_from_file(file_name: String) -> RasterType {
    if file_name.to_lowercase().ends_with(".tas") {
        return RasterType::MyRaster;
    }
    RasterType::Unknown
}

#2
impl<T: RasterDataFile> Raster<T> {
    pub fn new<'a>(file_name: &'a str, file_mode: &'a str) -> Result<Raster<T>, Error> {
        //...
    }
}

The signature of the function new is generic over the type T, but T only occurs in the return type.
This means, that the function must be able to return an arbitrary type Raster<T> determined by the caller, not by the function itself.
For example, I can define my own RasterDataFile:

struct MyRasterDataFile;
impl RasterDataFile for MyRasterDataFile {
    //...
}

let raster = try!(Raster::<MyRasterDataFile>::new("filename", "r"));

Now the function has to know that format and return a corresponding raster.

If you want to read the raster file type from the file itself, you cannot use generics, instead you have to use runtime polymorphism (aka trait objects).

One possibility would be to make a trait for Raster:

trait RasterTrait {
    //...
}

impl <T: RasterDataFile> RasterTrait for Raster<T> {
    // ...
}

// Return value is polymorphic at runtime
pub fn read_raster(file_name: &str, file_mode: &str) -> Result<Box<RasterTrait>, Error> {
    // ...
}

Another possibility is to remove the type parameter from Raster and use runtime polymorphism for the RasterDataFile:

pub struct Raster {
    file_name: String,
    file_mode: String,
    raster: Box<RasterDataFile>, // <- Runtime polymorphism here
    raster_type: RasterType,
}

impl Raster {
    pub fn new(file_name: &str, file_mode: &str) -> Result<Raster, Error> {
        //...
    }
}

#3

Thank you troplin for your quick response. I certainly see your point about the type not being used outside of the return value. I must admit that I don’t fully understand your solutions yet but I’ll do some research until I do. Thanks again for taking the time to help a newbie like me on a Sunday night!


#4

Here’s a simpler example to explain the difference between generics and trait objects:

This is a generic function:

fn some_function<T: Trait>() -> T { ... }

Type parameters are always defined by how the caller is using the function, never by the function itself. The result here is returned by value, which means, the caller has to know the concrete type at compile time.

This is a function using a trait object:

fn some_other_function() -> Box<Trait> { ... }

here, the function itself can chose a type at runtime.
That means, that the caller does not know the size of the type, it has to be allocated on the heap (using Box).
This is the “traditional” way how interfaces work, e.g. in Java.


#5

This is very interesting. So I assume, based on your description, that there is a significant performance penalty to be paid for using trait objects rather than generics. Is this the case and if so, how significantly can I expect the performance to degrade? I realize that that isn’t something that can be answered precisely but I’m just looking for ranges (e.g. anywhere between 10-100% performance degradation).


#6

In general the cost of trait object is equivalent to the cost of uninlined function call. If you are going to call trait methods a lot, the trait object might have a bigger impact on performance. I cannot say for sure with the current code, but if you are dealing with the pixelwise manipulation it may actually matter (imagine a format-dependent function called for each pixel).


#7

Thanks lifthrasiir. That is actually a big help and useful advice. I am intending on extensive pixel manipulation. I think that I’ve now arrived at a design where I no longer need to worry about dynamic dispatch or trait objects any more. It’s not the approach that I would have adopted at first, but now that I have a much better understanding of the compromises required, I can move forward with this other solution.