Trying to implement `new` constructor for my struct

fn main() {
    #[derive(Debug, Clone, Hash, Default, PartialEq, Eq)]
    pub struct Meta {
        pub title: Option<String>,
        pub passwords: Vec<String>,
        pub tags: Vec<String>,
        pub category: Option<String>,
    }
    
    impl Meta {
        pub fn new<S: Into<String>>(title: Option<S>, passwords: Vec<S>, tags: Vec<S>, category: Option<S>) -> Self {
            Self {
                title: match title {
                    Some(text) => Some(text.into()),
                    None => None,
                },
                passwords: passwords.iter().map(|s: &S| -> String {s.into()}).collect(),
                tags: passwords.iter().map(|s: &S| -> String {s.into()}).collect(),
                category: match category {
                    Some(text) => Some(text.into()),
                    None => None,
                },
            }
        }
    }

    let meta = Meta::new(Some("hello"), vec!["hello", "world"], vec!["hello", "world"], Some("world"));
    println!("{:?}", meta)
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `String: From<&S>` is not satisfied
  --> src/main.rs:17:70
   |
17 |                 passwords: passwords.iter().map(|s: &S| -> String {s.into()}).collect(),
   |                                                                      ^^^^ the trait `From<&S>` is not implemented for `String`
   |
   = note: required for `&S` to implement `Into<String>`
help: consider dereferencing here
   |
17 |                 passwords: passwords.iter().map(|s: &S| -> String {(*s).into()}).collect(),
   |                                                                    ++ +
help: consider introducing a `where` clause, but there might be an alternative better way to express this requirement
   |
10 |     impl Meta where String: From<&S> {
   |               ++++++++++++++++++++++

error[E0277]: the trait bound `String: From<&S>` is not satisfied
  --> src/main.rs:18:65
   |
18 |                 tags: passwords.iter().map(|s: &S| -> String {s.into()}).collect(),
   |                                                                 ^^^^ the trait `From<&S>` is not implemented for `String`
   |
   = note: required for `&S` to implement `Into<String>`
help: consider dereferencing here
   |
18 |                 tags: passwords.iter().map(|s: &S| -> String {(*s).into()}).collect(),
   |                                                               ++ +
help: consider introducing a `where` clause, but there might be an alternative better way to express this requirement
   |
10 |     impl Meta where String: From<&S> {
   |               ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` (bin "playground") due to 2 previous errors

I'm trying to implement a wider new constructor that can accept more types than just calling Meta { ... }. I do see the errors but I still can't wrap my head around what's really wrong and how do I go about fixing it

you have S: Into<String>, but the closure requires &S: Into<String>.

change passwords.iter() to passwords.into_iter() should fix the compile error.

3 Likes

That worked!

fn main() {
    #[derive(Debug, Clone, Hash, Default, PartialEq, Eq)]
    pub struct Meta {
        pub title: Option<String>,
        pub passwords: Vec<String>,
        pub tags: Vec<String>,
        pub category: Option<String>,
    }
    
    impl Meta {
        pub fn new<S: Into<String>>(title: Option<S>, passwords: Vec<S>, tags: Vec<S>, category: Option<S>) -> Self {
            Self {
                title: match title {
                    Some(text) => Some(text.into()),
                    None => None,
                },
                passwords: passwords.into_iter().map(Into::into).collect(),
                tags: tags.into_iter().map(Into::into).collect(),
                category: match category {
                    Some(text) => Some(text.into()),
                    None => None,
                },
            }
        }
    }

    let meta = Meta::new(Some("hello"), vec!["hello", "world"], vec!["hello", "world"], Some("world"));
    println!("{:?}", meta)
}

(Playground)

Output:

Meta { title: Some("hello"), passwords: ["hello", "world"], tags: ["hello", "world"], category: Some("world") }

I have another question: Can passwords and tags be made more generic? After all I don't particularly care if it's a vector as long as I can get a vector out of it?

Something that implements IntoIterator<Item=S> would be the way to go more generic on that, since all you are doing is calling into_iter(), working on the iterator and then collecting to a Vec.

2 Likes

this is what I managed to get:

    pub fn new(
        title: Option<impl Into<String>>,
        passwords: impl IntoIterator<Item = impl Into<String>>,
        tags: impl IntoIterator<Item = impl Into<String>>,
        category: Option<impl Into<String>>,
    ) -> Self {
        Self {
            title: title.map(Into::into),
            passwords: passwords.into_iter().map(Into::into).collect(),
            tags: tags.into_iter().map(Into::into).collect(),
            category: category.map(Into::into),
        }
    }
}

playground

some notes:

  • your original implementation used one single parameter type S, which means all the arguments must have the same S. I used multiple generic type parameters, so you can mix and match different types.

  • for passwords and tags, this implementation can accept arrays or vectors, but not slices.

    • it is possible to make it also accept slices, but doing so without sacrifice efficiency is not trivial.

EDIT:

the easiest way to also accept iterators with borrowed values (e.g. slices) is to use AsRef<str> to replace the Into<String> bound, but this will be less efficient when called with String arguments, because you always allocate new Strings.

using Into<String> has the benefit that when called with a String as arguments, the Strings are moved, and no new Strings are allocated (new Vec might still need to be allocated though).

just in case you have a slice of strings, and want to use that as argument, I would rather let the caller do the conversion, e.g.:

let passwords: &[&str] = &["hello", "world"];
let tags: &[String] = &[String::from("hello"), String::from("world")];
let meta = Meta::new(
    Some("hello"),
    passwords.iter().copied(), // `&str` is `Copy`
    tags.iter().cloned(), // `String` is `Clone` but not `Copy`
    Some(String::from("world")),
);
1 Like

That covers my needs perfectly! Appreciate the help and explanations.

Thank you @nerditation @jameseb7