Quick guide to Rust's From/Into and TryFrom/TryInto Traits

While working with Rust, I find myself implementing a lot of From/Into or TryFrom/TryInto traits in my code. Whether you're dealing with simple conversions or handling more complex scenarios with potential errors, mastering these traits is key to writing elegant Rust code.

From and Into Traits

We use the From trait to convert another type to our type. When we implement the From trait for our type, Rust automatically provides an implementation of the Into trait to convert our type to that another type.

Let's consider a scenario where we have a Car struct with a property called brand, which is of type String. Now, suppose we have a variable brand_1: String, and we want to convert it to an instance of Car. We need to implement the From trait for the Car struct.

#[derive(Debug)]
#[allow(dead_code)]
struct Car {
    brand: String
}

impl From<String> for Car {
    fn from(brand: String) -> Self {
        Car { brand }
    }
}

fn main() {
    let brand_1 = String::from("FORD");

    let car_1 = Car::from(brand_1);

    println!("car_1: {:?}", car_1); // car_1: Car { Brand: "FORD" }
}

if we want to convert a Car into a String type, it might be natural to assume that we can achieve this by using the Into trait. However, in Rust, it is not the recommended approach, as the Rust documentation states:

One should avoid implementing Into and implement From instead. Implementing From automatically provides one with an implementation of Into thanks to the blanket implementation in the standard library.

Therefore, following this guideline, we will implement From trait for the String type. This will enable us to invoke into on a variable of String type to convert it to a Car instance.

#[derive(Debug)]
#[allow(dead_code)]
struct Car {
    brand: String
}

impl From<String> for Car {
    fn from(brand: String) -> Self {
        Car { brand }
    }
}

impl From<Car> for String {
   fn from(car: Car) -> Self {
        String::from(car.brand)
    }
}

fn main() {
    let brand_1 = String::from("FORD");
    let car_1 = Car::from(brand_1); 
    println!("car_1: {:?}", car_1); // car_1: Car { brand: "FORD" }

    let car_1_brand: String = car_1.into();
    println!("car_1_brand: {:?}", car_1_brand); // car_1_brand: "FORD"
}

TryFrom and TryInto Traits

The TryFrom and TryInto traits are similar to the From and Into traits, but they handle conversions with the consideration of potential errors.

In our case, let's say we want to ensure that our Car struct does not accept an empty brand name.

#[derive(Debug)]
#[allow(dead_code)]
struct Car {
    brand: String
}

impl TryFrom<String> for Car {
    type Error = &'static str;

    fn try_from(brand: String) -> Result<Self, Self::Error> {
        if brand.len() < 1 {
            Err("Invalid brand name")
        } else {
            Ok(Car { brand })
        }
    }
}

fn main() {
    let brand_1 = String::from("FORD");
    let car_1 = Car::try_from(brand_1).unwrap();
    println!("car_1: {:?}", car_1); // Car { brand: "FORD" }

    let car_2 = Car::try_from(String::from("")).unwrap_err();
    println!("car_2: {:?}", car_2); // "Invalid brand name"
}

Now we want to allow the conversion of our Car struct to a String, but not for certain specific brands.

#[derive(Debug)]
#[allow(dead_code)]
struct Car {
    brand: String
}

impl TryFrom<String> for Car {
    type Error = &'static str;

    fn try_from(brand: String) -> Result<Self, Self::Error> {
        if brand.len() < 1 {
            Err("Invalid brand name")
        } else {
            Ok(Car { brand })
        }
    }
}

impl TryFrom<Car> for String {
    type Error = &'static str;
    fn try_from(car: Car) -> Result<Self, Self::Error> {
        if car.brand == "KIA" || car.brand == "BMW" {
             Err("Not allowed!")
        } else {
            Ok(String::from(car.brand))
        }
    }
}

fn main() {
    let brand_1 = String::from("FORD");
    let car_1 = Car::try_from(brand_1).unwrap();
    println!("car_1: {:?}", car_1); // Car { brand: "FORD" }

    let car_2 = Car::try_from(String::from("")).unwrap_err();
    println!("car_2: {:?}", car_2); // "Invalid brand name"

    let brand_1: Result<String, &'static str> = car_1.try_into();
    println!("brand_1: {:?}", brand_1.unwrap()); // "FORD"

    let car_3 = Car::try_from(String::from("KIA")).unwrap();
    let brand_2: Result<String, &'static str> = car_3.try_into();
    println!("brand_2: {:?}", brand_2.unwrap_err()); // "Not allowed!"
}

Conclusion

By now, you should feel confident in your ability to leverage these traits effectively, streamlining your code and improving its readability. Happy coding!