Function that takes an Iterator of Into<String>

Hi,

I want to write a function that can work with the following types:

  • impl Iterator<Item = String>
  • impl Iterator<Item = &str>

The function is roughly like below:

use std::collections::HashMap;

fn make_hashmap(words: ???) {
    let mut map: HashMap<String, usize> = HashMap::new();
    for w in words {
        // insert (w, length of w) into the map
    }
}

I hope to allow callers to call my function in the following ways:

fn main() {
    let some_data = vec!["Allen", "Bella", "Cathy"];

    // Caller A
    make_hashmap(&some_data);

    // Caller B
    make_hashmap(some_data.iter());

    // Caller C
    make_hashmap(some_data.into_iter());

    // Caller D
    make_hashmap("Allen Bella Cathy".split_whitespace());
}

As you can see, the function should support both consuming iterators and non-consuming iterators. And the iterator's element type should be able to be coverted into a String.

My attempt is like below:

use std::collections::HashMap;

fn make_hashmap(words: impl Iterator<Item = impl Into<String>>) {
    let mut map: HashMap<String, usize> = HashMap::new();
    for w in words {
        let s = w.into();
        let size = s.len();
        map.insert(s, size);
    }
}

However, it can only work with Caller C and Caller D, both being consuming iterators.

My motivation is to make my function as friendly as possible, such that callers can pass any iterable of string-like.

Is it possible to support all 4 callers?

One thing you can do is change Iterator to std::iter::IntoIterator, this will allow passing a Vec directly.

1 Like

The other issue is that &&str does not implement Into<String>, so you can solve this with your own trait:

trait IntoString {
    fn into_string (self: Self) -> String;
}

impl IntoString for String {
    fn into_string (self: Self) -> String
    {
        self
    }
}

impl IntoString for &'_ str {
    fn into_string (self: Self) -> String
    {
        self.to_owned()
    }
}

impl IntoString for &'_ &'_ str {
    fn into_string (self: Self) -> String
    {
        (*self).to_owned()
    }
}

fn make_hashmap(words: impl IntoIterator<Item = impl IntoString>) {
    let mut map: HashMap<String, usize> = HashMap::new();
    for w in words {
        let s = w.into_string();
        let size = s.len();
        map.insert(s, size);
    }
}
4 Likes

The problem with callers A and B is that their Item type is &&str, not &str. You could say that it's "their fault" that compilation fails.

For example, this works fine:

    let some_data = vec!["Allen", "Bella", "Cathy"];
    // Caller B'
    make_hashmap(some_data.iter().cloned());

However, maybe your trait bound should be ToOwned<Owned = String>. I'm not sure your code would work with an iterator whose items are &String.

2 Likes

@Yandros beat me to the punch, but I'd just like to make two comments:

  1. If you impl IntoString for &'_ str, you should also impl it for &'_ String, otherwise you'll have to call .map(|s| s.as_str()) a lot.
  2. You can actually use iterators a bit more effectively in make_hashmap():
let map: HashMap<String, usize> = words
    .into_iter()
    .map(|word| word.into_string())
    .map(|word| (word, word.len()))
    .collect();

since HashMap impls FromIterator.

4 Likes

Thank you guys so much! Really appreciated.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.