From and Into

Let's go back to where our string journey started:

#![allow(unused)]
fn main() {
let ticket = Ticket::new(
    "A title".into(), 
    "A description".into(), 
    "To-Do".into()
);
}

We now know enough to start unpacking what .into() is doing here.

The problem

This is the signature of the new method:

#![allow(unused)]
fn main() {
impl Ticket {
    pub fn new(
        title: String, 
        description: String, 
        status: String
    ) -> Self {
        // [...]
    }
}
}

We've also seen that string literals (such as "A title") are of type &str.
We have a type mismatch here: a String is expected, but we have a &str. No magical coercion will come to save us this time; we need to perform a conversion.

From and Into

The Rust standard library defines two traits for infallible conversions: From and Into, in the std::convert module.

#![allow(unused)]
fn main() {
pub trait From<T>: Sized {
    fn from(value: T) -> Self;
}

pub trait Into<T>: Sized {
    fn into(self) -> T;
}
}

These trait definitions showcase a few concepts that we haven't seen before: supertraits and implicit trait bounds. Let's unpack those first.

Supertrait / Subtrait

The From: Sized syntax implies that From is a subtrait of Sized: any type that implements From must also implement Sized. Alternatively, you could say that Sized is a supertrait of From.

Implicit trait bounds

Every time you have a generic type parameter, the compiler implicitly assumes that it's Sized.

For example:

#![allow(unused)]
fn main() {
pub struct Foo<T> {
    inner: T,
}
}

is actually equivalent to:

#![allow(unused)]
fn main() {
pub struct Foo<T: Sized> 
{
    inner: T,
}
}

In the case of From<T>, the trait definition is equivalent to:

#![allow(unused)]
fn main() {
pub trait From<T: Sized>: Sized {
    fn from(value: T) -> Self;
}
}

In other words, both T and the type implementing From<T> must be Sized, even though the former bound is implicit.

Negative trait bounds

You can opt out of the implicit Sized bound with a negative trait bound:

#![allow(unused)]
fn main() {
pub struct Foo<T: ?Sized> {
    //            ^^^^^^^
    //            This is a negative trait bound
    inner: T,
}
}

This syntax reads as "T may or may not be Sized", and it allows you to bind T to a DST (e.g. Foo<str>). It is a special case, though: negative trait bounds are exclusive to Sized, you can't use them with other traits.

&str to String

In std's documentation you can see which std types implement the From trait.
You'll find that String implements From<&str> for String. Thus, we can write:

#![allow(unused)]
fn main() {
let title = String::from("A title");
}

We've been primarily using .into(), though.
If you check out the implementors of Into you won't find Into<String> for &str. What's going on?

From and Into are dual traits.
In particular, Into is implemented for any type that implements From using a blanket implementation:

#![allow(unused)]
fn main() {
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}
}

If a type U implements From<T>, then Into<U> for T is automatically implemented. That's why we can write let title = "A title".into();.

.into()

Every time you see .into(), you're witnessing a conversion between types.
What's the target type, though?

In most cases, the target type is either:

  • Specified by the signature of a function/method (e.g. Ticket::new in our example above)
  • Specified in the variable declaration with a type annotation (e.g. let title: String = "A title".into();)

.into() will work out of the box as long as the compiler can infer the target type from the context without ambiguity.

Exercise

The exercise for this section is located in 04_traits/09_from