RustArena

Step 4 of 7

Conversions and flexible parameters: From, Into, AsRef

Good Rust APIs are pleasant to call: they accept what callers naturally have and convert it without fuss. Three traits do most of the work.

From and Into

Implement From<A> for B and you get both B::from(a) and a.into() (the standard library has a blanket impl Into<B> for A whenever B: From<A>). So you always implement From, never Into (clippy's from_over_into says so). Use it for conversions that can't fail and are unsurprising; for fallible ones, implement TryFrom (and get try_into()).

impl Into<String> for values you store

A constructor that stores a String should let callers pass a &str or a String they already own (without a forced extra copy):

struct Player {
    name: String,
    tags: Vec<String>,
}

impl Player {
    fn new(name: impl Into<String>) -> Self {
        Player { name: name.into(), tags: Vec::new() }
    }

    fn tag(mut self, t: impl Into<String>) -> Self {
        self.tags.push(t.into());
        self
    }
}

fn longest<S: AsRef<str>>(words: &[S]) -> Option<&str> {
    words.iter().map(|w| w.as_ref()).max_by_key(|w| w.len())
}

struct Celsius(f64);
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Self {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

fn main() {
    let owned = String::from("Ferris");
    let a = Player::new("Corro").tag("mod");
    let b = Player::new(owned).tag(String::from("crab"));
    println!("{} {:?} / {} {:?}", a.name, a.tags, b.name, b.tags);

    println!("{:?}", longest(&["a", "abc", "ab"]));
    println!("{:?}", longest(&[String::from("crab"), String::from("lobster")]));

    let f: Fahrenheit = Celsius(100.0).into();
    println!("{}", f.0);
}
Corro ["mod"] / Ferris ["crab"]
Some("abc")
Some("lobster")
212

Piece by piece:

Which parameter type?

You need to Take
read text &str (a &String coerces to it automatically)
read a list &[T], not &Vec<T>
store a string impl Into<String> (or just String)
read several strings of either kind &[S] with S: AsRef<str>
open a file path impl AsRef<Path>
consume any sequence impl IntoIterator<Item = T> (next step)

Don't over-generalize: &str is the right answer most of the time, and every generic parameter makes signatures harder to read and compile times longer. Reach for these when callers really have different types.

Common mistakes

&String parameters are a classic review comment (clippy: ptr_arg): they force callers to have a String, while &str accepts both.

fn greet(name: &String) -> String {
    format!("Hello, {name}!")
}

fn main() {
    println!("{}", greet("Ana"));
}
error[E0308]: mismatched types

Your task

Tags on articles. Implement From<&str> for Tag (trim and lowercase the text) and From<Tag> for String. Write fn normalize_all<S: AsRef<str>>(items: &[S]) -> Vec<Tag> (works for &[&str] and &[String]), and give Article a constructor new(title: impl Into<String>) and a chainable with_tag(mut self, tag: impl Into<Tag>) -> Self.

Challenge 2

Colors. Implement From<(u8, u8, u8)> for Rgb and From<Rgb> for String (lowercase hex, #ff8000), and TryFrom<&str> for Rgb that parses #rrggbb (exactly 7 characters, a #, then hex digits) with type Error = String and the error invalid color "red".

Challenge 3

Write fn total_size<P: AsRef<Path>>(paths: &[P]) -> std::io::Result<u64> that adds up the sizes in bytes of the given files (std::fs::metadata(path)?.len()). Thanks to AsRef<Path> it must accept &[&str], Vec<String> and Vec<PathBuf>. A missing file returns the I/O error. (The tests run in a folder containing a.txt with 5 bytes and b.txt with 11.)

Previous: Make invalid states unrepresentable Next: RAII guards and Drop