Rustify combinators application to create Map from String

I'm trying to parse key value pairs encoded in a single into a hash map using combinators. So far I have come up with the following solution -- please see Rust Playground for a full example.

   let tags_string = "KEY1=VAL1,KEY2=VAL2";
   let mut tags: HashMap<String,String> = HashMap::new();

   tags_string.split(',')
              .map(|kv| kv.split('=').collect::<Vec<&str>>())
              .map(|vec| (vec[0].to_string(), vec[1].to_string()))
              .fold((), |_, (k, v)| {
                  tags.insert(k, v);
              });

Even tough the code works, I was wondering if a) this can be simplified and b) rustified. By the latter I mean, is this an idiomatic solution in Rust.

I'm open to any suggestions and hope to learn some idioms that have proven useful.

Thanks,
Lukas

1 Like

If you don't mind some unwrap() this is a possible solution:

fn main() {
    use std::collections::HashMap;

    let tags_string = "KEY1=VAL1,KEY2=VAL2";

    let tags = tags_string
               .split(',')
               .map(|kv| kv.split('='))
               .map(|mut kv| (kv.next().unwrap().into(),
                              kv.next().unwrap().into()))
               .collect::<HashMap<String, String>>();

    println!("{:?}", tags);
}

But we can do a bit better with:

You can write something like (marginally better), untested:

    let tags = tags_string
               .split(',')
               .map(|kv| kv.split('=').map(|s| s.to_string()))
               .map(|mut kv| kv.to_tuple2::<String>().unwrap())
               .collect::<HashMap<_, _>>();
1 Like

I would suggest three improvements:

  • Don't declare something mut to initialize it. It's usually better to init in one step, or to use the following pattern
let value = {
    let mut value = ...;
    // modify value
    value
}
  • Don't use fold, use collect (it can do almost everything, and surely it can collect pairs into associative container)

  • Verify your assertions!

So I would write this as

use std::collections::HashMap;

fn main() {
    let tags_string = "KEY1=VAL1,KEY2=VAL2";
    let tags: HashMap<String, String> = tags_string.split(',')
        .map(|kv| kv.split('=').collect::<Vec<&str>>())
        .map(|vec| {
            assert_eq!(vec.len(), 2);
            (vec[0].to_string(), vec[1].to_string())
        })
        .collect();
}

Also, you may be able to use HashMap<&str, &str> here.

3 Likes

A completely non-idiomatic way of doing it:

#!/usr/bin/env run-cargo-script
//! ```cargo
//! [dependencies]
//! scan-rules = { version = "0.1.1", features = ["regex"] }
//! ```
#[macro_use] extern crate scan_rules;

use std::collections::HashMap;
use scan_rules::scanner::re_str;

fn main() {
    let mut input = String::new();
    let _ = std::io::stdin().read_line(&mut input).unwrap();
    let input = input.trim();

    let_scan!(input; (
        [let keys <| re_str("[^=]+"), "=", let values <| re_str("[^,]+")],*
    ));
    let map: HashMap<_, _> = keys.into_iter().zip(values.into_iter()).collect();

    println!("map: {:#?}", map);
}

Aside from the use of let_scan! (because I'm being lazy; normally, you'd use regular scan!) and that unwrap, the above should ensure the correct number of splits, and report a sort-of readable error if the input is malformed.

You can run that directly with cargo-script.

> echo 'KEY1=VAL1,KEY2=VAL2' | cargo script map-parse.rs
   Compiling map-parse v0.1.0 (file:///C:/Users/drk/AppData/Local/Cargo/script-cache/file-map-parse-5fe471961e16149e)
map: {
    "KEY1": "VAL1",
    "KEY2": "VAL2"
}
1 Like

Thanks guys that was very quick and helpful. I'm glad I wasn't totally off with my solution, but I definitely like to use `collect()' instead of a side effect as well as assigning the the computation result to a immutable variable.