3.2 Lo que puedes exigir
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
Una buena receta no dice “harina de la marca X en un paquete de 500 gramos.” Dice “harina.” Una herramienta deficiente no hace esa distinción: exige exactamente lo que se le pasó la última vez y rechaza todo lo demás sin explicación. Esto suele llamarse rigor. La palabra exacta es especificidad accidental.
El intento
Mueve la creación de tareas dentro de add: que acepte un id y un título directamente, sin
que el llamador tenga que lidiar con Task::new. Un título es una cadena - la elección de
tipo obvia:
fn add(tasks: &mut Vec<Task>, id: u64, title: String) -> Result<(), TqError> {
tasks.push(Task::new(id, &title)?);
Ok(())
}
La llamada parece natural:
add(&mut tasks, 1, "Buy coffee")?;
El compilador nota una incompatibilidad:
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())?;
| +++++++++++++
El compilador incluso ofrece una salida: .to_string(). No está mal - eso funcionará. Pero
trasladar ese trabajo a cada llamador es exactamente lo que queríamos evitar.
Lo que sabe el compilador
String y &str son tipos distintos (el capítulo 1.2 cubre ambos). add declaró que
acepta un String. El literal "Buy coffee" es un &str. El compilador los mantiene
separados y no convierte implícitamente.
La solución no es exigir un tipo específico, sino exigir una capacidad: “dame cualquier cosa
que sepa convertirse en un String.” En Rust esto se escribe como una restricción sobre el
tipo:
title: impl Into<String>
impl Into<String> significa: cualquier tipo que implemente el trait Into<String>. Tanto
&str como String lo implementan - ambos tienen un impl From, y From da Into
automáticamente (capítulo 3.1). Dentro de la función, title.into() convierte lo que se
haya pasado en un String - una vez, en un solo lugar.
El conjuro
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(())
}
Ahora ambas variantes funcionan sin ningún cambio en el lado del llamador:
add(&mut tasks, 1, "Buy coffee")?; // &str - funciona
add(&mut tasks, 2, String::from("Buy milk"))?; // String - también funciona
main se vuelve más limpio: en lugar de add(&mut tasks, Task::new(1, "Buy coffee")?) -
simplemente add(&mut tasks, 1, "Buy coffee")?.
Dos tests cubren el nuevo comportamiento:
#[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());
}
El primero comprueba que un &str pasa tal cual. El segundo comprueba que la validación de
Task::new sigue en su lugar: un título vacío sigue siendo rechazado. La suite completa de
tests está en tq/.
make ci pasa. add acepta cualquier cadena - y no traslada trabajo innecesario al
llamador.
El código completo de
tqpara este capítulo está en3-a-voice/02-what-you-can-demand/.