18

How would one translate the following Python, in which several files are read and their contents are used as values to a dictionary (with filename as key), to Rust?

countries = {region: open("{}.txt".format(region)).read() for region in ["canada", "usa", "mexico"]}

My attempt is shown below, but I was wondering if a one-line, idiomatic solution is possible.

use std::{
    fs::File,
    io::{prelude::*, BufReader},
    path::Path,
    collections::HashMap,
};

macro_rules! map(
    { $($key:expr => $value:expr),+ } => {
        {
            let mut m = HashMap::new();
            $(
                m.insert($key, $value);
            )+
            m
        }
     };
);

fn lines_from_file<P>(filename: P) -> Vec<String>
where
    P: AsRef<Path>,
{
    let file = File::open(filename).expect("no such file");
    let buf = BufReader::new(file);
    buf.lines()
        .map(|l| l.expect("Could not parse line"))
        .collect()
}

fn main() {
    let _countries = map!{ "canada" => lines_from_file("canada.txt"),
                           "usa"    => lines_from_file("usa.txt"),
                           "mexico" => lines_from_file("mexico.txt") };
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Sean Pianka
  • 2,157
  • 2
  • 27
  • 43
  • 2
    i'm not sure if that would be considered idiomatic python, much less whether it should be emulated in another language - the files are not explicitly closed after being opened. – Haleemur Ali Dec 09 '18 at 00:53
  • 1
    @HaleemurAli Replace `open().read()` with an ad-hoc function to read (and close) and it's ok, and idiomatic. However, I don't know how useful it is to just read these text files. Depends on contents I guess, but if there is any structure it should probably be exploited (text lines / csv table / json / ...). –  Dec 09 '18 at 01:02
  • Python's comprehensions are just sugar for a for loop and accumulator. Rust has [macros](https://doc.rust-lang.org/stable/rust-by-example/macros.html)--you can make any sugar you want. – gilch Dec 09 '18 at 02:18
  • @gilch would you mind submitting an example as an answer? – Sean Pianka Dec 09 '18 at 02:19
  • 2
    See also [Does Rust have an equivalent to Python's list comprehension syntax?](https://stackoverflow.com/q/45282970/155423) – Shepmaster Dec 09 '18 at 13:56

3 Answers3

30

Rust's iterators have map/filter/collect methods which are enough to do anything Python's comprehensions can. You can create a HashMap with collect on an iterator of pairs, but collect can return various types of collections, so you may have to specify the type you want.

For example,

use std::collections::HashMap;

fn main() {
    println!(
        "{:?}",
        (1..5).map(|i| (i + i, i * i)).collect::<HashMap<_, _>>()
    );
}

Is roughly equivalent to the Python

print({i+i: i*i for i in range(1, 5)})

But translated very literally, it's actually closer to

from builtins import dict

def main():
    print("{!r}".format(dict(map(lambda i: (i+i, i*i), range(1, 5)))))

if __name__ == "__main__":
    main()

not that you would ever say it that way in Python.

gilch
  • 10,813
  • 1
  • 23
  • 28
21

Python's comprehensions are just sugar for a for loop and accumulator. Rust has macros--you can make any sugar you want.

Take this simple Python example,

print({i+i: i*i for i in range(1, 5)})

You could easily re-write this as a loop and accumulator:

map = {}
for i in range(1, 5):
    map[i+i] = i*i
print(map)

You could do it basically the same way in Rust.

use std::collections::HashMap;

fn main() {
    let mut hm = HashMap::new();
    for i in 1..5 {
        hm.insert(i + i, i * i);
    }
    println!("{:?}", hm);
}

You can use a macro to do the rewriting to this form for you.

use std::collections::HashMap;
macro_rules! hashcomp {
    ($name:ident = $k:expr => $v:expr; for $i:ident in $itr:expr) => {
        let mut $name = HashMap::new();
        for $i in $itr {
            $name.insert($k, $v);
        }
    };
}

When you use it, the resulting code is much more compact. And this choice of separator tokens makes it resemble the Python.

fn main() {
    hashcomp!(hm = i+i => i*i; for i in 1..5);
    println!("{:?}", hm);
}

This is just a basic example that can handle a single loop. Python's comprehensions also can have filters and additional loops, but a more advanced macro could probably do that too.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
gilch
  • 10,813
  • 1
  • 23
  • 28
3

Without using your own macros I think the closest to

    countries = {region: open("{}.txt".format(region)).read() for region in ["canada", "usa", "mexico"]}

in Rust would be

let countries: HashMap<_, _> = ["canada", "usa", "mexico"].iter().map(|&c| {(c,read_to_string(c.to_owned() + ".txt").expect("Error reading file"),)}).collect();

but running a formatter, will make it more readable:

let countries: HashMap<_, _> = ["canada", "usa", "mexico"]
        .iter()
        .map(|&c| {
            (
                c,
                read_to_string(c.to_owned() + ".txt").expect("Error reading file"),
            )
        })
        .collect();

A few notes: To map a vector, you need to transform it into an iterator, thus iter().map(...). To transform an iterator back into a tangible data structure, e.g. a HashMap (dict), use .collect(). This is the advantage and pain of Rust, it is very strict with types, no unexpected conversions.

A complete test program:

use std::collections::HashMap;
use std::fs::{read_to_string, File};
use std::io::Write;

fn create_files() -> std::io::Result<()> {
    let regios = [
        ("canada", "Ottawa"),
        ("usa", "Washington"),
        ("mexico", "Mexico city"),
    ];
    for (country, capital) in regios {
        let mut file = File::create(country.to_owned() + ".txt")?;
        file.write_fmt(format_args!("The capital of {} is {}", country, capital))?;
    }
    Ok(())
}

fn create_hashmap() -> HashMap<&'static str, String> {
    let countries = ["canada", "usa", "mexico"]
        .iter()
        .map(|&c| {
            (
                c,
                read_to_string(c.to_owned() + ".txt").expect("Error reading file"),
            )
        })
        .collect();
    countries
}

fn main() -> std::io::Result<()> {
    println!("Hello, world!");
    create_files().expect("Failed to create files");
    let countries = create_hashmap();
    {
        println!("{:#?}", countries);
    }

    std::io::Result::Ok(())
}

Not that specifying the type of countries is not needed here, because the return type of create_hashmap() is defined.

Johan
  • 342
  • 3
  • 14