DEV Community

Cover image for Rust From & Into Traits - A Full Beginner’s Guide 🦀
Anson Heung
Anson Heung

Posted on

Rust From & Into Traits - A Full Beginner’s Guide 🦀

Rust provides the From and Into traits for converting one type into another. Mastering these two traits is crucial for beginners since they are used very frequently. I faced some questions and difficulties when I was learning them, and I think I’m not alone here. Hopefully this article will clear things up! 🦀


Table of Contents


What is From trait?

The From trait has an associated function called from for converting a value’s type to another one. Here’s the simplified definition:

trait From<T> {
    // Converts to this type from the input type.
    fn from(value: T) -> Self;
}
Enter fullscreen mode Exit fullscreen mode

Okay that isn’t too useful… Let’s look at an example.

Suppose we have a struct called Rectangle. We want users to easily create a rectangle from a tuple of dimensions (e.g. (10, 20)). To support type conversion from (usize, usize) to Rectangle, we implement the From trait:

#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
    width: usize,
    height: usize,
}

// Convert (usize, usize) -> Rectangle
impl From<(usize, usize)> for Rectangle {
    fn from((width, height): (usize, usize)) -> Self {
        Self { width, height }
    }
}

fn main() {
    let rect = Rectangle::from((30, 50));
    println!("{rect:?}"); // Rectangle { width: 30, height: 50 }
}
Enter fullscreen mode Exit fullscreen mode

Initially, I found impl From<A> for B tricky to read. If we read it out loud as “implement from A for B”, I got confused by whether it is “from A to B” or “from B to A” 😅. Eventually, I’ve come up with this visual cue:

From trait visual cue

You may already be using the From trait without noticing. For example, you can convert a string slice &str into a String by calling String::from().

fn main() {
    let str_slice = "Hello World!";
    let string = String::from(str_slice);
    println!("{string}");
}
Enter fullscreen mode Exit fullscreen mode

This is because the standard library implements From trait for String to allow conversion from &str:

// In Rust standard library's source code
impl From<&str> for String {
        fn from(s: &str) -> String {
            // ...
        }
}
Enter fullscreen mode Exit fullscreen mode

(🔼 Table of Contents)


What is Into trait?

The Into trait is another trait for type conversion. It has a method called into that turns a type into another. Here’s the simplified definition:

trait Into<T> {
    // Converts this type into the (usually inferred) input type.
    fn into(self) -> T;
}
Enter fullscreen mode Exit fullscreen mode

Let’s head back to our Rectangle example. We can implement Into for (usize, usize) to convert it into Rectangle.

#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
    width: usize,
    height: usize,
}

// Convert (usize, usize) -> Rectangle
impl Into<Rectangle> for (usize, usize) {
    fn into(self) -> Rectangle {
        Rectangle {
            width: self.0,
            height: self.1,
        }
    }
}

fn main() {
    let rect: Rectangle = (30, 50).into();
    println!("{rect:?}"); // Rectangle { width: 30, height: 50 }
}
Enter fullscreen mode Exit fullscreen mode

💡 In practice we ONLY need to implement From trait instead of Into trait. We’ll explain that later in the next section.

Type Annotation

Using .into() has a huge caveat. Rust must be able to infer the type that you’re converting “into”. If we remove the type annotation from rect, it fails to compile:

error[E0283]: type annotations needed
  --> src/main.rs:17:9
   |
17 |     let rect = (30, 50).into();
   |         ^^^^            ---- type must be known at this point
   |
Enter fullscreen mode Exit fullscreen mode

This is because (30, 50) can convert into other types that are not Rectangle, so Rust don’t know what you’re converting to! For example, Rust can convert the tuple back to a primitive array in addition to a Rectangle:

fn main() {
    let arr: [usize; 2] = (30, 50).into();
    println!("{arr:?}"); // [30, 50]

    let rect: Rectangle = (30, 50).into();
    println!("{rect:?}"); // Rectangle { width: 30, height: 50 }

    // 🚨 ERROR: Not sure what you're convering into!
    let unknown = (30, 50).into();
}
Enter fullscreen mode Exit fullscreen mode

(🔼 Table of Contents)


Implement From gives you Into for free

If you look at From trait’s documentation:

One should always prefer implementing From over Into because implementing From automatically provides one with an implementation of Into thanks to the blanket implementation¹ in the standard library.

¹ See “What are Blanket implementations in Rust?”

To put it simply:

From Into meme

Let’s head back to the Rectangle example:

#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
    width: usize,
    height: usize,
}

// Converts (usize, usize) to Rectangle
// You receive `impl Into<Rectangle> for (usize, usize)` for free!
impl From<(usize, usize)> for Rectangle {
    fn from((width, height): (usize, usize)) -> Self {
        Self { width, height }
    }
}

fn main() {
    let r1 = Rectangle::from((3, 4));
    let r2: Rectangle = (3, 4).into();
    assert_eq!(r1, r2);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, simply implementing From trait gives us two ways to perform type conversion:

  1. From - Rectange::from((3, 4))
  2. Into - (3, 4).into()

💡 IMPORTANT: Both ways converts (usize, usize) into Rectangle. The Into implementation you get for free has the SAME direction of type conversion as From (i.e. tuple to Rectangle)!

(🔼 Table of Contents)


When to use From or Into?

If both From and Into are for converting type A to B, you may ask:

When should I use from() or.into()?

The answer is:

  • Use .into() if you want less verbosity
    • 👍 No need to import the type, no need to wrap in from() function call
    • 👎 Less obvious on what type it is converted to
    • 👎 More difficult to locate how the type conversion is implemented
  • Use from() if you want to be more explicit on what type it converts to

Verbosity

Using from() is more verbose than .into() because you may need to import the type that it’s converted to, and wrapping in function calls make the code longer.

If the struct has a long name (e.g. UserDefinedRectangle), then using .into() is much cleaner:

fn is_same(r1: UserCreatedRectangle, r2: UserCreatedRectangle) -> bool {
    r1.width == r2.width && r1.height == r2.height
}

fn main() {
    let dim_1 = (3, 4);
    let dim_2 = (5, 6);

    // 🤢 `from()` is a long beam of text!
    is_same(UserCreatedRectangle::from(dim_1), UserCreatedRectangle::from(dim_2));

    // 😍 `.into()` is very clean
    is_same(dim_1.into(), dim_2.into());
}
Enter fullscreen mode Exit fullscreen mode

Moreover, it’s common to use .into() instead of constructors (e.g. new()) to reduce verbosity. For example:

#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
    width: usize,
    height: usize,
}

impl Rectangle {
    // Rectangle constructor
    fn new(width: usize, height: usize) -> Self {
        Self { width, height }
    }
}

impl From<(usize, usize)> for Rectangle {
    fn from((width, height): (usize, usize)) -> Self {
        Self { width, height }
    }
}

fn compare_rectangles(r1: Rectangle, r2: Rectangle) -> bool {
    r1.width == r2.width && r1.height == r2.height
}

fn main() {
    // 🤢 Constructors are verbose
    assert!(compare_rectangles(
        Rectangle::new(2, 3),
        Rectangle::new(2, 3)
    ));

    // 😍 Much cleaner with `.into()`
    assert!(compare_rectangles((2, 3).into(), (2, 3).into()))
}
Enter fullscreen mode Exit fullscreen mode

Locating implementation

One drawback of .into() over from() is that it’s more difficult to find how the type is converted.

Usually from() is directly associated to a concrete struct/enum, so using “Go to Definition” in your editor will directly jump to the implementation 👇

from() go to definition

However, the .into() method is usually derived from a blanket implementation of From<T>, so using “Go to Definition” on .into() will jump to the Rust’s blanket implementation! 👇

into() go to definition

To see how the type conversion is implemented, you’ll need to find where impl From<(usize, usize)> for Rectangle is located. While our example places this implementation next to the Rectangle’s definition, this may not be the case for other libraries.

(🔼 Table of Contents)


Use TryFrom if conversion may fail

Sometimes converting from one type to another may fail. A noob may call panic if type conversion fail, but this may be a bad idea since the entire program will exit immediately.

For example, let’s say every Rectangle must not have a zero area:

impl From<(usize, usize)> for Rectangle {
    fn from((width, height): (usize, usize)) -> Self {
        if width == 0 || height == 0 {
            // 👎👎 AVOID panics! Handle it gracefully!
            panic!("Zero area rectangles are not allowed");
        }
        Rectangle { width, height }
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead, use the TryForm trait. The conversion will return a Result according to the trait’s definition. You’ll need to specify the input type T and also the error type Error if conversion fails:

// Simplified definition
trait TryFrom<T> {
    type Error;

    fn try_from(value: T) -> Result<Self, Self::Error>;
}
Enter fullscreen mode Exit fullscreen mode

Here’s an example:

#[derive(Debug, Clone, PartialEq)]
struct Rectangle {
    width: usize,
    height: usize,
}

// We prefer enums over strings for errors
#[derive(Debug)]
enum RectangleConversionError {
    ZeroArea,
}

// ✅ Rectangle::try_from((usize, usize))
impl TryFrom<(usize, usize)> for Rectangle {
    type Error = RectangleConversionError;

    fn try_from((width, height): (usize, usize)) -> Result<Self, Self::Error> {
        if width == 0 || height == 0 {
            Err(RectangleConversionError::ZeroArea)
        } else {
            Ok(Rectangle { width, height })
        }
    }
}

fn main() {
    let r1 = Rectangle::try_from((5, 2));
    println!("{r1:?}"); // Ok(Rectangle { width: 5, height: 2 })

    let r2 = Rectangle::try_from((0, 2));
    println!("{r2:?}"); // Err(ZeroArea)
}
Enter fullscreen mode Exit fullscreen mode

💡 Similar to From, implementing TryFrom gives you TryInto for free!

fn main() {
    // Don't forget to add type annotation, so Rust knows what you're casting into
    //        vvvvvvvvvvvvvvvvvvvvvv
    let result: Result<Rectangle, _> = (0, 0).try_into();
    assert!(result.is_err());
}
Enter fullscreen mode Exit fullscreen mode

(🔼 Table of Contents)


impl Into<T> in function parameters

One trick that you can do is to use impl Into<T> as the type of a function parameter instead of just T. This is known as “impl trait type”.

For example, instead of requiring each call’s argument to add .into(), such as:

fn compare_rectangles(r1: Rectangle, r2: Rectangle) -> bool {
    r1.width == r2.width && r1.height == r2.height
}

fn main() {
    // I need to add `.into()` for each argument
    compare_rectangles((3, 5).into(), (3, 6).into());
}
Enter fullscreen mode Exit fullscreen mode

You can specify the parameter type as impl Into<Rectangle>:

//                        vvvvvvvvvvvvvvvvvvvv      vvvvvvvvvvvvvvvvvvvv
fn compare_rectangles(r1: impl Into<Rectangle>, r2: impl Into<Rectangle>) -> bool {
    let r1: Rectangle = r1.into();
    let r2: Rectangle = r2.into();
    r1.width == r2.width && r1.height == r2.height
}

fn main() {
    // Notice I don't need to call `.into()` for the arguments
    println!("{}", compare_rectangles((3, 5), (3, 5))); // true
    println!("{}", compare_rectangles((3, 5), (5, 3))); // false
}
Enter fullscreen mode Exit fullscreen mode

This works because impl Into<Rectangle> basically means:

“I accept any types that can turn into a Rectangle (i.e. implements Into<Rectangle>)”

Pros and cons:

  • 👍 Less verbose to call .into() for every argument. This is especially useful if the function is frequently called using arguments with .into()
  • 👎 Function signature looks more complicated

(🔼 Table of Contents)


That’s pretty much all I want to share. Don’t forget to like and share this post if you found it useful, cheers! 🙌

Top comments (1)

Collapse
 
crusty-rustacean profile image
Jeff Mitchell

Excellent breakdown, thank you!