Synesso
Synesso

Reputation: 38958

Iterator map with HashMap lookup. Fail on any key not present

I have a String with colon-separated values. Each sub-string should be a key in a map. I want to write a function that converts the String into a Vec of map values, or a failure if any key is not present.

My attempt so far is incomplete, but I'm taking it in small steps. The first step is to convert the string into a vector of optional u32s. (This may not the best approach):

fn parse(string: &String, lookup: HashMap<String, u32, FastHasher>) -> Vec<Option<u32>> {
    string.split(":").map(|s: &str| lookup.get(s)).collect()
}

This results in

error: the trait bound `std::vec::Vec<std::option::Option<u32>>: std::iter::FromIterator<std::option::Option<&u32>>` is not satisfied [--explain E0277]
 --> <anon>:8:52
  |>
8 |>     string.split(":").map(|s: &str| lookup.get(s)).collect()
  |>                                                    ^^^^^^^
note: a collection of type `std::vec::Vec<std::option::Option<u32>>` cannot be built from an iterator over elements of type `std::option::Option<&u32>`

I believe this means I need to import or write my own from-iterator behaviour for Option<&u32>, right?

After I've done that, how can I wrap in an Ok or Err depending upon the presence of any Nones?

Upvotes: 1

Views: 351

Answers (3)

4e6
4e6

Reputation: 10776

If you really want to return the references, you should specify lifetimes explicitly:

pub fn parse<'a, 'b>(string: &'a str, lookup: &'b HashMap<String, u32>) -> Vec<Option<&'b u32>> {
    string.split(":").map(|s| lookup.get(s)).collect()
}

Regarding the second part of the question about the conversion to Result type:

And after I've done that, how can I wrap in an Ok or Err depending upon the presence of any Nones?

This can be done by folding and accumulating the results into Ok<Vec<u32>>. Following example illustrates the idea. See Rust Playground runnable example.

use std::collections::HashMap;
use std::result::Result;

#[derive(Debug)]
pub struct ParseError {
    key: String,
}

impl ParseError {
    fn new(k: &str) -> ParseError {
        ParseError { key: k.to_owned() }
    }
}

fn parse(string: &str, lookup: &HashMap<String, u32>) -> Result<Vec<u32>, ParseError> {
    string.split(":")
        .fold(Ok(Vec::new()), |res, s| {
            let mut vec = try!(res);
            match lookup.get(s) {
                Some(&v) => {
                    vec.push(v);
                    Ok(vec)
                }
                None => Err(ParseError::new(s)),
            }
        })
}

Upvotes: 1

Lukas Kalbertodt
Lukas Kalbertodt

Reputation: 88536

The HashMap::get() method returns an optional reference to the value inside of the map. So you have an iterator over Option<&u32>, but you want to have an iterator over Option<u32>. This is done by saying:

lookup.get(s).cloned()
//           ^^^^^^^^^

This makes your current code compile.


To answer the question in your title: there is a neat little FromIterator impl:

impl<A, V> FromIterator<Option<A>> for Option<V> where V: FromIterator<A>

This means, that, for example, you can collect an iterator over items of types Option<u32> into Option<Vec<u32>>. This is exactly what you want! So just change your return type:

fn parse(string: &str, lookup: &HashMap<String, u32, FastHasher>) -> Option<Vec<u32>> {
//                                                                   ^^^^^^^^^^^^^^^^
    string.split(":").map(|s| lookup.get(s).cloned()).collect()
}

You can try the working code here on playground.


Also note the following, question-independent changes I made:

  • The string argument is &str now instead of &String. There is practically no reason for ever passing &String instead of &str, so the latter is preferred for being more generic.
  • The explicit type annotation of the closure argument is not required.
  • You probably want to pass a reference to the HashMap, as your function does not need to own it.

Upvotes: 6

ljedrz
ljedrz

Reputation: 22163

In order to avoid this issue, instead of Vec<Option<&u32>> you could just return Vec<Option<u32>>:

fn parse(string: &String, lookup: HashMap<String, u32>) -> Vec<Option<u32>> {
    string.split(":").map(|s: &str| if let Some(&e) = lookup.get(s) { Some(e) } else { None }).collect()
}

Which, as Matthieu suggested, can be simplified to:

string.split(":").map(|s: &str| lookup.get(s).cloned()).collect()

I'm not sure if it adds value if you wrap it in a Result; you can easily check for None afterwards:

let v = vec![Some(1), None, Some(3)];
println!("{:?}", v.contains(&None));

Upvotes: 3

Related Questions