← Table of contents

3.1 The Common Tongue

There are agreements that require no signing. Two craftsmen from the same guild know them by heart: what may be asked, what must be given, what may be refused. A craftsman who does not know these agreements explains - every time, from scratch, by hand. This is usually called a personal touch. Those who practice it long enough know a different word.

What You Need

Task can report errors honestly and name the absent by name. But it still speaks its own language. Creating a task requires Task::new(id, title)? - explicitly, with a check, the same way every time.

The Rust ecosystem has an agreement for one common situation: “B can be obtained from A.” When a type declares this, the rest of the code takes it as given: functions expecting A automatically accept B - no explanation, no intermediary.

The agreement is expressed through the keyword trait. Task should declare one: that it knows how to create a new task from a &str.

The Build

In the standard library, the agreement for type conversion looks like this:

pub trait From<T> {
    fn from(value: T) -> Self;
}

trait defines a contract: any type implementing this trait must provide the method fn from(value: T) -> Self. The compiler will verify that the promise is kept. Self means the type for which the impl is written - here that will be Task.

Implement it for Task:

impl From<&str> for Task {
    fn from(title: &str) -> Self {
        Task {
            id: 0,
            title: title.to_string(),
            status: Status::Todo,
        }
    }
}

from has no access to the task list - there is nothing to assign the next identifier from. For now, use id: 0; in chapter 3.3 the store will take responsibility for assigning identifiers, and Task::from will become the natural path for creation on add.

Now in main:

let from_task = Task::from("Quick note");
println!("{:?}", from_task);

But From has a consequence.

The standard library contains a general rule: by implementing From<A> for B, you automatically gain the ability to call .into() on a value of type A to get a B. This does not need to be implemented separately:

let task: Task = "Quick note".into();

The type annotation : Task is necessary here - the compiler must know what to convert the string into.

The Result

Task::from("Buy coffee") and "Buy coffee".into() both work. Task now speaks the guild standard - and any code that accepts “something a Task can be made from” will accept a string without further explanation. The next chapter will put this to use in add.

Two tests cover both paths:

#[test]
fn task_from_str_sets_title() {
    let task = Task::from("Buy coffee");
    assert_eq!(task.title, "Buy coffee");
}

#[test]
fn str_converts_into_task() {
    let task: Task = "Buy milk".into();
    assert_eq!(task.title, "Buy milk");
}

make ci passes. Task no longer needs explaining.

The complete tq code for this chapter is in 3-a-voice/01-the-common-tongue/.


Lore: Required and Optional

A trait can declare any number of methods - but not all of them need to be provided. A method without a default implementation must be supplied: the compiler will demand it on every impl. A method with a default implementation can be overridden or left as-is.

pub trait Greet {
    fn name(&self) -> &str;                           // required
    fn hello(&self) { println!("Hi, {}!", self.name()); } // optional
}

A type implementing Greet must write name - and gets hello for free.

From<T> is entirely required: one method, no default.


Lore: From and Into - Two Sides of One Agreement

By implementing From<&str> for Task, you get Into<Task> for &str for free. The standard library contains a blanket impl (generics are covered fully in chapter 3.3):

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

The practical rule from this: always implement From. Never implement Into by hand - it appears on its own.

From is a contract without exceptions: by convention it must always succeed. If a conversion can fail, Rust offers TryFrom<T> - the same idea, but the method returns Result<Self, E>. In effect, Task::new already behaves like TryFrom<&str>: it takes a string and returns Result<Task, TqError>.