MikeTheSapien
MikeTheSapien

Reputation: 297

How does one return a vector of `Option` wrapping different types? i.e `vec![Option<i64>, Option<String>]`

I have a struct with all fields having an Option<T> type. I need some checks done whether such fields does not contain None regardless of the value.

Initially I thought of the following:

struct MyType {
    id: Option<i64>,
    id_str: Option<String>,
    abbrv: Option<String>,
    title: Option<String>,
    // ... Some other fields not necessary to check its `Option` variant
}

impl MyType {
    fn has_essentials(&self) -> bool {
        if self.id.is_none()
            && self.id_str.is_none() && self.abbrv.is_none()
            && self.title.is_none()
        {
            true
        } else {
            false
        }
    }
}

But since I might be a masochist programmer, I thought more of it and it seemed that this can be refactored into two functions, one which returns a bool and another that simply returns a vector of cloned Option<T>, like the following:

// The following snippet won't compile as there are a lot of syntax knowledge gaps on my end.
fn get_essentials<T>(&self) -> Vec<Option<T>> {
        vec![self.id.clone(), self.id_str.clone()]
}


fn has_essentials(&self) -> bool {
        self.get_essentials()
            .into_iter()
            .try_for_each(|x|{
                if x.is_none() {
                    Err(())
                } else {
                    Ok(())
                }
            })
            .is_ok()
}

I tried the following:

fn get_essentials<T>(&self) -> Vec<Option<T>>
    where
        T: ToString,
    {
        vec![self.id.clone(), self.id_str.clone()]
    }

Since I reckoned that i64 and String implements ToString.

And running cargo check, the compiler spits out the following error:

error[E0308]: mismatched types                                            
  --> madler\src\features\models\molecular_idea.rs:43:8                   
   |                                                                      
38 |     fn get_essentials<T>(&self) -> Vec<Option<T>>                    
   |                       - this type parameter                          
...                                                                       
43 |         vec![self.id.clone(), self.id_str.clone()]                   
   |              ^^^^^^^^^^^^^^^ expected type parameter `T`, found `i64`
   |                                                                      
   = note: expected enum `std::option::Option<T>`                         
              found enum `std::option::Option<i64>`                       
                                                                          
error: aborting due to previous error; 1 warning emitted                  
                                                                          
For more information about this error, try `rustc --explain E0308`.       
error: could not compile `madler`                                         

Implementing the two functions might also lead to a more readable code when changing what's considered "essential" fields for future development.

I looked into the following links but these doesn't seem to fit the solution I'm looking for:

Thanks for reading and attempting to help.

Upvotes: 1

Views: 743

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43743

You can't create a vector of mixed value types; you have to convert them to being the same type somehow. The simplest possibility is to convert them to booleans:

impl MyType {
    fn essential_field_presences(&self) -> [bool; 4] {
        [
            self.id.is_some(),
            self.id_str.is_some(),
            self.abbrv.is_some(),
            self.title.is_some(),
        ]
    }
    
    fn has_essentials(&self) -> bool {
        self.essential_field_presences() == [true; 4]
    }
}

Note that I've used an array (fixed length) instead of a vector to avoid creating a heap allocation for this otherwise very low-cost code. But vectors are often easier to use and scale better for very large problems, so don't take that as “you must do this”.

The above code isn't that much more useful than just self.id.is_some() && self.id_str.is_some() && ... If we're bothering to abstract over the fields at all, it might be nice if we could report which fields were missing, and maybe even do something with the present values. So here's how we might do that:

fn display_value<T: Display>(value: &Option<T>) -> Option<&dyn Display> {
    // Could also be written: value.as_ref().map(|value| value as &dyn Display)
    match value {
        None => None,
        Some(value) => Some(value as &dyn Display),
    }
}

impl MyType {
    fn essential_fields(&self) -> impl Iterator<Item = (&'static str, Option<&'_ dyn Display>)> {
        IntoIterator::into_iter([
            ("id", display_value(&self.id)),
            ("id_str", display_value(&self.id_str)),
            ("abbrv", display_value(&self.abbrv)),
            ("title", display_value(&self.title)),
        ])
    }
}

There's a lot going on here, but I hope that it's useful as an exploration of what one can do in Rust. I'm not suggesting that you do exactly this — take it as inspiration for what's possible, and use whatever's useful for your actual problem.

  • display_value is just a helper for essential_fields takes a reference to one of our fields of any type T (as long as it can be “displayed” — T: Display) and if the value is present, coerces the reference to a trait object reference &dyn Display — this means that there is only one type returned, “something that can be displayed”, using function pointers internally to figure out how to display that value.

  • essential_fields calls display_value on each of the fields, and pairs it up with the field's name for later display purposes. The return type is an iterator, because that is convenient to work with later and is a good abstraction boundary, but it's actually implemented using an array just like in the previous function.

  • -> impl Iterator<...> means “I'm returning something that implements the trait Iterator, but not telling you what that concrete type of iterator is”. This way, the implementation type is kept hidden and can be changed freely (e.g. if this struct gains some sort of dynamic list of “fields” and now this should be iterating over that instead of hardcoding).

  • Using IntoIterator::into_iter([...]) instead of [...].into_iter() is a temporary workaround for a Rust compilation backwards-compatibility constraint that can be removed once the Rust 2021 language edition is available, which should be in just a couple months.

Now that we have this function, we can use it:

impl MyType {
    fn report_missing(&self) -> bool {
        let mut any_missing = false;
        for (name, maybe_value) in self.essential_fields() {
            match maybe_value {
                None => {
                    any_missing = true;
                    println!("{}: missing", name);
                }
                Some(value) => {
                    println!("{}: present ({})", name, value);
                }
            }
        }
        any_missing
    }
}

fn main() {
    let x = MyType {
        id: Some(123),
        id_str: None,
        abbrv: Some(String::from("XYZ")),
        title: None,
    };

    x.report_missing();
}

This code on Rust Playground

Upvotes: 2

Related Questions