← Table of contents

3.2 What You Can Demand

A good recipe does not say “brand-X flour in a 500-gram packet.” It says “flour.” A poor tool makes no such distinction: it demands exactly what was passed last time and refuses everything else without explanation. This is usually called strictness. The accurate word is accidental specificity.

The Attempt

Move task creation inside add: let it accept an id and title directly, without the caller having to deal with Task::new. A title is a string - the obvious type choice:

fn add(tasks: &mut Vec<Task>, id: u64, title: String) -> Result<(), TqError> {
    tasks.push(Task::new(id, &title)?);
    Ok(())
}

The call looks natural:

add(&mut tasks, 1, "Buy coffee")?;

The compiler notices a mismatch:

error[E0308]: mismatched types
  --> src/main.rs:XX:XX
   |
XX |     add(&mut tasks, 1, "Buy coffee")?;
   |                        ^^^^^^^^^^^^ expected `String`, found `&str`
   |
   = note: expected struct `String`
             found reference `&str`
help: try using a conversion method
   |
XX |     add(&mut tasks, 1, "Buy coffee".to_string())?;
   |                                   +++++++++++++

The compiler even offers a way out: .to_string(). It is not wrong - that will work. But pushing that work onto every caller is exactly what we wanted to avoid.

What the Compiler Knows

String and &str are different types (chapter 1.2 covers both). add declared that it accepts a String. The literal "Buy coffee" is a &str. The compiler keeps them separate and does not convert implicitly.

The solution is not to demand a specific type, but to demand a capability: “give me anything that knows how to become a String.” In Rust this is written as a bound on the type:

title: impl Into<String>

impl Into<String> means: any type that implements the Into<String> trait. Both &str and String implement it - both have a From impl, and From gives Into automatically (chapter 3.1). Inside the function, title.into() converts whatever was passed into a String - once, in one place.

The Enchantment

fn add(tasks: &mut Vec<Task>, id: u64, title: impl Into<String>) -> Result<(), TqError> {
    let title = title.into();
    tasks.push(Task::new(id, &title)?);
    Ok(())
}

Now both variants work without any change on the caller’s side:

add(&mut tasks, 1, "Buy coffee")?;               // &str - works
add(&mut tasks, 2, String::from("Buy milk"))?;   // String - works too

main becomes cleaner: instead of add(&mut tasks, Task::new(1, "Buy coffee")?) - simply add(&mut tasks, 1, "Buy coffee")?.

Two tests cover the new behaviour:

#[test]
fn add_accepts_str_literal() {
    let mut tasks = Vec::new();
    assert!(add(&mut tasks, 1, "Buy coffee").is_ok());
    assert_eq!(tasks.len(), 1);
}

#[test]
fn add_rejects_empty_title() {
    let mut tasks = Vec::new();
    assert!(add(&mut tasks, 1, "").is_err());
}

The first checks that a &str passes through as-is. The second checks that the validation from Task::new is still in place: an empty title is still rejected. The full test suite is in tq/.

make ci passes. add accepts any string - and does not push unnecessary work onto the caller.

The complete tq code for this chapter is in 3-a-voice/02-what-you-can-demand/.