← Table of contents

3.4 A Store for Anything

A good guild does not rely on the craftsman’s memory. It relies on a document: the piece must accept, find, enumerate - the craftsman signed. The store can do everything needed. Capability without a signed obligation is usually called reliability. The accurate word is coincidence.

What You Need

We just built TaskStore, where the methods add, get, and all live in impl TaskStore. But nowhere is it written that the store must have them - remove all and the compiler says nothing.

A named contract is needed: a description of what the store is obligated to do. Then the implementation and the requirement live separately - and any mismatch between them is caught by the compiler, not only by whoever wrote the code.

The Build

For a store to find an item by identifier, each item must be able to provide one. This is the minimum requirement for anything the store accepts:

trait HasId {
    fn id(&self) -> u64;
}

For Task:

impl HasId for Task {
    fn id(&self) -> u64 {
        self.id
    }
}

Several methods return Result with the same error type - and this repeats across signatures. A type alias removes the repetition:

type TqResult<T> = Result<T, TqError>;
Without aliasWith alias
fn add(...) -> Result<(), TqError>fn add(...) -> TqResult<()>
fn get(...) -> Result<&T, TqError>fn get(...) -> TqResult<&T>
Task::new(...) -> Result<Task, TqError>Task::new(...) -> TqResult<Task>

T is the success type: TqResult<()> means “nothing”, TqResult<&Task> means “a reference to a task”.

The store’s contract is described by a trait. T is a placeholder for the item type; : HasId says that this type knows how to provide an identifier:

trait Store<T: HasId> {
    fn add(&mut self, item: impl Into<T>) -> TqResult<()>;
    fn get(&self, id: u64) -> TqResult<&T>;
    fn all(&self) -> &[T];
}

TaskStore implements this contract. The methods move from impl TaskStore into impl Store<Task> for TaskStore - impl TaskStore keeps only the constructor:

impl TaskStore {
    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)  // HasId
            .ok_or(TqError::NotFound(id))
    }

    fn all(&self) -> &[Task] {
        &self.items
    }
}

impl Store<Task> for TaskStore - “TaskStore fulfils the Store contract for tasks.” The compiler checks: all three methods are present, signatures match. In get, task.id() is called - the method from HasId. In add, the field task.id is written directly: HasId provides only reading.

The Result

In main, only the signature changes - Result<(), TqError> becomes TqResult<()>. Everything else is untouched:

fn main() -> TqResult<()> {

The existing tests pass without changes. That is their job: to confirm that internal changes have not broken behaviour from the outside.

make ci passes. TaskStore has signed: impl Store<Task> for TaskStore - these are not just methods, they are an obligation.

The complete tq code for this chapter is in 3-a-voice/04-a-store-for-anything/.


Lore: Bounds on a Trait

Store<T: HasId> - the bound sits directly on the trait parameter. It is not possible to implement Store<T> for a type without HasId; the compiler will check this on any attempt.

When there are multiple bounds, the notation T: HasId + Clone grows long. In that case, use where:

impl<T> Store<T> for TaskStore
where
    T: HasId,
{
    // ...
}

Both forms compile identically. where is more convenient with longer bounds.