3.6 Rooms in the Grimoire
In a workshop without rooms, everything can be found. For this you need to know exactly where - that knowledge arrives on its own, explaining it in advance is not customary. With one craftsman and one workbench, this is enough. Then there are more workbenches. The knowledge stays the same. This is usually called experience. The accurate word is accumulated disorder.
The Attempt
Eight types, six traits, two hundred and fifty lines in main.rs. The task’s shape, the
store, the errors, the voice - all in one file. A reasonable desire: give each part its own
file.
Start small - move just Status and Task. Create src/task.rs with those two types:
// src/task.rs
#[derive(Debug)]
enum Status {
Todo,
Done,
}
#[derive(Debug)]
struct Task {
id: u64,
title: String,
status: Status,
}
Delete those two declarations from main.rs. The compiler notices:
error[E0412]: cannot find type `Task` in this scope
--> src/main.rs:XX:XX
|
XX | impl Task {
| ^^^^ not found in this scope
What the Compiler Knows
Rust does not include files automatically. Creating src/task.rs does not tell the
compiler about it. That requires a declaration in src/main.rs:
// src/main.rs
mod task;
mod task; says: find src/task.rs and include its contents as a module named task.
The module now exists, but Task and Status have not been brought into the current
scope. Add:
// src/main.rs
use task::{Status, Task};
Try again:
error[E0603]: struct `Task` is private
--> src/main.rs:XX:XX
|
XX | use task::{Status, Task};
| ^^^^^^ private struct
The compiler sees the module and knows Task is in there - but it is closed. In Rust
everything is private by default: defining a type in a module does not open it for use
outside. That is what pub is for:
// src/task.rs
#[derive(Debug)]
pub enum Status {
Todo,
Done,
}
#[derive(Debug)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
}
pub on the type name makes it visible from outside. pub on a field opens that field for
reading and writing from outside the module - without it the struct is visible but its
contents are unreachable.
The Enchantment
Three files. Each holds what belongs to it. Start with src/error.rs - it depends on
nothing else inside the crate:
// src/error.rs - NEW
use std::fmt;
#[derive(Debug)]
pub enum TqError {
EmptyTitle,
NotFound(u64),
}
pub type TqResult<T> = Result<T, TqError>;
impl fmt::Display for TqError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TqError::EmptyTitle => write!(f, "task title cannot be empty"),
TqError::NotFound(id) => write!(f, "task {} not found", id),
}
}
}
Then src/task.rs. It uses types from error.rs - and refers to them through crate::,
the path from the crate root:
// src/task.rs - NEW
use crate::error::{TqError, TqResult};
use std::fmt;
pub trait HasId {
fn id(&self) -> u64;
}
#[derive(Debug)]
pub enum Status {
Todo,
Done,
}
#[derive(Debug)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
}
impl Task {
pub fn new(id: u64, title: &str) -> TqResult<Task> {
if title.is_empty() {
return Err(TqError::EmptyTitle);
}
Ok(Task {
id,
title: String::from(title),
status: Status::Todo,
})
}
pub fn complete(&mut self) {
self.status = Status::Done;
}
pub fn is_done(&self) -> bool {
match self.status {
Status::Done => true,
Status::Todo => false,
}
}
}
impl From<&str> for Task {
fn from(title: &str) -> Self {
Task {
id: 0,
title: title.to_string(),
status: Status::Todo,
}
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}: {} [{:?}]", self.id, self.title, self.status)
}
}
impl HasId for Task {
fn id(&self) -> u64 {
self.id
}
}
HasId lives here, next to Task: for now it is only needed by her. src/store.rs will
refer to it through crate::task::HasId.
Now src/store.rs. The items field has become private - store.items[0] from main.rs
is no longer reachable. That is the point of the module: implementation details stay
inside. In place of direct access - a get_mut method:
// src/store.rs - NEW
use crate::error::{TqError, TqResult};
use crate::task::{HasId, Task};
pub trait Store<T: HasId> {
fn add(&mut self, item: impl Into<T>) -> TqResult<()>;
fn get(&self, id: u64) -> TqResult<&T>;
fn get_mut(&mut self, id: u64) -> TqResult<&mut T>;
fn all(&self) -> &[T];
}
pub struct TaskStore {
items: Vec<Task>,
next_id: u64,
}
impl TaskStore {
pub fn new() -> Self {
TaskStore {
items: Vec::new(),
next_id: 1,
}
}
}
impl Store<Task> for TaskStore {
fn add(&mut self, item: impl Into<Task>) -> TqResult<()> {
let mut task = item.into();
task.id = self.next_id;
self.next_id += 1;
self.items.push(task);
Ok(())
}
fn get(&self, id: u64) -> TqResult<&Task> {
self.items
.iter()
.find(|task| task.id() == id)
.ok_or(TqError::NotFound(id))
}
fn get_mut(&mut self, id: u64) -> TqResult<&mut Task> {
self.items
.iter_mut()
.find(|task| task.id() == id)
.ok_or(TqError::NotFound(id))
}
fn all(&self) -> &[Task] {
&self.items
}
}
get_mut was not planned - the private field demanded it. That is the effect of a module
boundary: visibility forces you to think about what should be accessible from outside.
Finally, src/main.rs - module declarations, imports, updated calls:
// src/main.rs - CHANGED
mod error;
mod store;
mod task;
use error::TqResult;
use store::{Store, TaskStore};
use task::Task;
fn main() -> TqResult<()> {
let mut store = TaskStore::new();
store.add("Buy coffee")?;
store.add("Buy milk")?;
store.add("Buy eggs")?;
for task in store.all() {
println!("{}", task);
}
match store.get(1) {
Ok(task) => println!("found: {}", task),
Err(e) => println!("{}", e),
}
match store.get(99) {
Ok(task) => println!("{}", task),
Err(e) => println!("{}", e),
}
store.get_mut(1)?.complete(); // CHANGED: was &mut store.items[0]
println!("done: {}", store.get(1)?.is_done());
match Task::new(0, "") {
Ok(task) => store.add(task)?,
Err(e) => println!("rejected: {}", e),
}
Ok(())
}
The output is unchanged:
#1: Buy coffee [Todo]
#2: Buy milk [Todo]
#3: Buy eggs [Todo]
found: #1: Buy coffee [Todo]
task 99 not found
done: true
rejected: task title cannot be empty
Tests follow the same scheme - the full suite is in
3-a-voice/06-rooms-in-the-grimoire/.
make ci passes. Every type now has its own room.
The complete
tqcode for this chapter is in3-a-voice/06-rooms-in-the-grimoire/.