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
tqcode for this chapter is in3-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>.