Design question: generic function based on return type


#1

I’m writing a wrapper around cfitsio. This is a library that serialises/deserialises astronomical data to disk.

I want to have a function that returns a generic type, dependant on the data type in the file, for example if the data is floating point, then I would like my rust accessor function to return a floating point type.

Currently I have implemented this in my fitsio crate as a trait which is implemented on the return types available. For example (simplifyed):

trait ReadsValue {
    fn read_from_file(filename: &str) -> Self {
        ...
    }
}

impl ReadsValue for i64 {
    fn read_from_file(filename: &str) -> Self {
        ...
    }
}

and the user can decide what data type they want at the calling site with:

let value: i64 = ReadsValue::read_from_file(filename);
// or
let value = i64::read_from_file(filename);

I would ideally like a function such as:

fn read_from_file<T>(filename: &str) -> T {
    if (file_contains_float(filename)) {
        value as f32
    }
    # etc
}

The available return types are integers, floating point values and strings, nothing more complicated.

Is there a way to achieve this?


#2

What would the user be able to do with the returned value? What use is a type that is either an integer, a float, or a string?


#3

If the user doesn’t know which of the types is present, but knows it’s one of those 3, you can return an enum:

enum Result {
   Int(i32),
   Float(f64),
   Str(String) // or &'a str if you want slice, need this enum to have a lifetime param then
}

Your return type can then be Option<std::result::Result<Result, Err>>. The Option is to signal EOF (if you need that). The Err is if the value couldn’t be parsed into one of those 3 types.


#4

I think I just wanted to sanity check my design. I think I’m modelling my library a bit after the Python equivalent library fitsio which just returns the data type contained. Of course Python is a dynamic language and as such does not specify types, but if possible I’d like to re-create the ergonomics. I’m used to the Python version where you just use the data, whatever type it is. In practice, integers and floats can be treated pretty similarly (with mathematical operations etc.). @jethrogb In this sense, strings have to be treated differently, but the user is very likely to know that the string data need be treated differently anyway.

I would like to avoid the user having to check what type the data is, before “casting” it to the correct type, through the trait I have above. @vitalyd I agree, having an enum with the resulting data type would work, but I suspect this may be more annoying for the user, as their code would be littered with match statements trying to check what the value is.


#5

You could write this function:

fn read_from_file<T: ReadsValue>(filename: &str) -> T {
   T::read_from_file(filename)
}

That would allow people to write things like:

let value = read_from_file("test");

and let type inference figure out what T is sometimes. Other times, you’ll need to specify the type on that anyway.


#6

If you went with the enum approach, you could implement the standard arithmetic on your type. It’s kind of ugly, and you’d be paying the runtime cost of checking the enum at each operation. But if your users aren’t wanting to do too much with the result, that would certainly provide a more python like experience.


#7

@jethrogb this is in fact what I do. I have wrapper functions on my FitsFile class which are defined exactly as yours are, e.g. the trait definition:

fn read_section(fits_file: &FitsFile, start: usize, end: usize) -> Result<Vec<Self>>;

and the wrapper struct function:

pub fn read_section<T: ReadWriteImage>(&self, start: usize, end: usize) -> Result<Vec<T>> {
        self.make_current()?;
        T::read_section(self.fits_file, start, end)
}

I then have a macro that implements the trait for various data types.

And I agree, letting the compiler infer the type based on how the data are used is a nice idea. I did think this design was quite elegant when I first implemented it, I just wanted to sanity check the design really! Thanks all for your feedback


#8

That’s the way to go if caller knows what type should be read out. From your previous posts, it seemed like that wasn’t the case though - caller knows it’s one of those 3 but not exactly which.


#9

Why settle for either/or?

You can have a Trait and implement it for integers, floats, strings, as well as some Value enum that represents anything. If somebody doesn’t know what type they want, they can match on the output and it will be inferred to the enum.

This is how serde does it. You can deserialize a piece of json to any static type, but when there’s uncertainty, you can always fall back to serde_json::Value