Modo historia · Módulo 01: Las prácticas de Lía#
Lía es estudiante de último año y acaba de empezar sus prácticas en Brote, una tienda online pequeña que está reescribiendo su backend en Rust. Durante cuatro capítulos vas a acompañarla en su trabajo y a decidir por ella cuando el código, el compilador o su mentor Bruno le planteen un problema.
¿Prefieres jugarla? Abre «Las prácticas de Lía» en 3D: la misma historia en la oficina de Brote, con Lía, Bruno y rustc animados. El progreso es el mismo en las dos versiones.
Cada capítulo corresponde a una lección del Módulo 01 y cada escena a una de sus preguntas de comprensión profunda:
| Capítulo | Lección |
|---|---|
| 1 | 01 · Tipos y flujo de control |
| 2 | 02 · Ownership y préstamos |
| 3 | 03 · Modelar errores y escribir pruebas |
| 4 | 04 · Colecciones e iteración |
Cómo se juega: lee la escena, elige qué haría Lía y lee la explicación. Si fallas puedes volver a intentarlo; solo cuentan como aciertos las respuestas a la primera. Al acertar se abre la siguiente escena. Tu progreso se guarda en este navegador.
Capítulo 1 · El punto y coma que costó un envío#
Lía empieza sus prácticas en Brote y descubre que, en Rust, el punto y coma, los tipos y el orden de las condiciones deciden lo que hace el código.
Se desbloquea al terminar el capítulo anterior.
Lía acaba de empezar sus prácticas en Brote, una tienda online pequeña que está reescribiendo su backend en Rust. Es su último año de carrera y, hasta ahora, sus errores los detectaba JavaScript o Python, casi siempre al ejecutar el programa.
Su mentor, Bruno, la recibe el primer día con una taza de café y una advertencia: en Rust, el compilador no deja pasar casi nada. Lía sonríe. Todavía no sabe cuánto le va a enseñar esa frase.
Escena 1.1 · El bloque que no devolvía nada#
Una de las primeras tareas de Lía es revisar el cálculo del total del carrito en el nuevo backend. Bruno le pasa un fragmento que un compañero dejó a medias, con dos bloques casi idénticos.
BrunoAntes de compilar, dime qué guarda cada variable. Piensa en el valor, no solo en el tipo.
LíaLos dos deberían valer 20, ¿no? Es la misma multiplicación.
NarradorLía no ha ejecutado nada todavía. Tiene que razonar sola qué ocurre con la segunda variable.
let result_a = {
let a = 10_u32;
a * 2
};
let result_b = {
let a = 10_u32;
a * 2;
};
¿Qué tipo y qué valor adquiere result_b?
Es el error de tratar el ; como un simple terminador, como en C o Java. En Rust cambia el significado: la última línea deja de ser el valor del bloque.
Es el error de suponer un valor por defecto. Rust no rellena nada: el bloque produce (), que no es un número y no vale 0.
Correcto. El ; convierte a * 2 en una instrucción que descarta el 20. El bloque termina en (), el tipo que significa «no hay dato útil».
Libreta de Lía: En Rust, la última línea sin ; es el valor del bloque; con ; ese valor se descarta y el bloque produce ().
Escena 1.2 · El cero que significaba éxito#
Brote integra una pasarela de pagos. Según su documentación, el código 0 significa que el cobro salió bien, y los valores 1, 2 y 3 indican distintos fallos.
Lía está trasladando la lógica de reintentos que tenía en Python y escribe, casi sin pensar, una condición al estilo C.
BrunoEsa forma de comprobar el código viene de C, ¿verdad? Antes de cambiarla, mira lo que dice el compilador.
rustcerror[E0308]: mismatched types
LíaPero en C el 0 es falso, así que no entiendo por qué aquí sería distinto.
fn handle_payment(status_code: u32) {
if status_code {
retry_charge();
}
}
¿Qué riesgo tendría aceptar if status_code con el 0 tratado como false?
Correcto. Con 0 como falso, el código de éxito entraría en la rama de fallo y viceversa. Por eso Rust exige escribir status_code == 0.
Es pensar que la ambigüedad es solo un problema de legibilidad. El verdadero problema es que la rama que se ejecuta depende de una convención implícita.
Es el error de tratar la comparación como una cuestión de estilo. Con códigos numéricos, que el 0 sea falso cambia la lógica, no solo la forma de escribirla.
Libreta de Lía: En Rust, if solo acepta bool; compara siempre los códigos de forma explícita, por ejemplo status_code == 0.
Escena 1.3 · El descuento sin plan B#
Bruno le pide a Lía que añada una marca de envío prioritario que depende de una bandera active, la que llega desde el panel de administración. Lía escribe una línea corta, convencida de que basta.
BrunoSi active es true, ¿qué valor tiene code?
LíaPues 1, claro. Y si es false, supongo que code queda vacío.
BrunoEse «vacío» tiene un tipo en Rust. Piensa qué tipo le daría el compilador a la rama que no escribiste.
let active = true;
let code = if active { 1_u32 };
¿Por qué rustc rechaza let code = if active { 1_u32 };?
Confunde la comprobación de tipos con la ejecución. Rust valida los tipos en compilación, sin depender del valor concreto que tome active.
Correcto. Una expresión if que se asigna necesita un valor en ambos caminos. Como () y u32 no unifican, la variable no tendría un tipo fijo.
Es la idea de que if solo sirve para ejecutar acciones, como en muchos lenguajes. En Rust if también es una expresión y puede dar valor, si todas sus ramas dan el mismo tipo.
Libreta de Lía: En Rust, un if que produce valor necesita un else, o al menos ramas que produzcan el mismo tipo.
Escena 1.4 · El céntimo que no cuadraba#
Brote tenía un script antiguo que calculaba los totales con números decimales de punto flotante. Esa semana, un cliente reclamó que su factura sumaba un céntimo menos de lo que marcaba el resumen.
Bruno le pide a Lía que reproduzca el problema en Rust.
BrunoEscribe la suma en Rust y mira qué imprime.
NarradorEn la terminal aparece 0.30000000000000004, no 0.3.
Lía¿Y por qué el ordenador no puede sumar 0.1 y 0.2 con exactitud?
let sum = 0.1_f64 + 0.2_f64;
println!("{sum}");
¿Qué representación debería elegir Lía para guardar el precio de 19,99 $?
Correcto. Guardar la unidad mínima (céntimos) como entero hace que las sumas y restas sean exactas. Los 19,99 $ pasan a ser 1999.
Es subestimar el error. Parece mínimo en un pedido, pero en muchos cálculos repetidos se acumula y los totales dejan de cuadrar con la contabilidad.
Parece razonable, pero el redondeo llega tarde: el valor ya se guardó como aproximación binaria. La solución de raíz es cambiar la representación, no retocar el resultado.
Libreta de Lía: Para dinero, guarda la unidad mínima en un entero sin signo (u32 o u64), nunca en un f64.
Escena 1.5 · La comprobación barata primero#
Bruno le enseña a Lía la regla para editar un documento: solo su propietario puede editarlo, y solo si no está archivado. La regla cabe en una línea.
Antes de darla por buena, Bruno quiere saber cómo la evalúa Rust.
BrunoImagina que la segunda condición consulta una base de datos lenta. ¿Qué te gustaría que pasara cuando la primera ya es falsa?
LíaQue no la consulte, supongo. Pero, ¿Rust lo hace así?
BrunoPiensa en lo que garantiza el operador && y dime después qué hace el programa.
fn can_edit(owner_id: u32, actor_id: u32, archived: bool) -> bool {
owner_id == actor_id && !archived
}
¿Qué ocurre en owner_id == actor_id && !archived cuando owner_id es distinto de actor_id?
Rust no reordena estas comprobaciones por tipo. Evalúa de izquierda a derecha, así que lo que escribes primero es lo primero que se comprueba.
Es la idea de que el orden no importa. En Rust && evalúa de izquierda a derecha y puede saltarse la segunda parte, así que el orden sí cuenta.
Correcto. Para && basta un false para que el resultado sea false, así que Rust corta ahí. La consulta costosa de la segunda parte se evita en las peticiones no autorizadas.
Libreta de Lía: En && el orden importa: pon primero las comprobaciones baratas que más peticiones descartan.
Al final de la semana, Lía tiene cinco notas en su libreta y una función que todavía no pasa todas las pruebas de la tienda. Bruno le recuerda que el compilador revisa la forma de los datos, pero no la intención del negocio.
Eso lo tendrá que comprobar ella con pruebas. El siguiente capítulo le pondrá delante una regla de envíos gratuitos con umbrales, donde un solo signo de comparación puede cambiar lo que paga un cliente.
Capítulo 2 · La nota que se quedaba sin dueño#
Lía descubre que en Rust cada dato tiene un único dueño, y que prestar un dato no es lo mismo que entregarlo.
Se desbloquea al terminar el capítulo anterior.
Lía lleva una semana en Brote y por fin le han dado una tarea con datos reales: el módulo que prepara las notas de los pedidos para el almacén. Cada pedido trae un texto libre que escribe el cliente, como «Llamar antes de entregar» o «Regalo, sin factura».
El código compila casi a la primera, pero al probarlo con pedidos de verdad aparecen errores que el compilador detecta antes de ejecutar nada. Bruno le propone seguir cada error hasta entender qué ocurre con la memoria, en lugar de cambiar líneas hasta que el compilador se calle.
Escena 2.1 · La nota del pedido 1042#
Lía tiene que pasar la nota del pedido 1042 a la variable que alimenta la etiqueta de envío. Escribe una asignación sencilla y, más abajo, vuelve a usar la variable original para el registro de auditoría.
Antes de corregir nada, quiere saber qué ocurre en la memoria cuando un String cambia de variable.
rustcerror[E0382]: borrow of moved value: nota
LíaYa la he usado después de pasarla, pero yo creía que etiqueta tenía su propia copia del texto.
BrunoPiensa en lo que guarda nota en la pila antes de decidir cuánto cuesta esa línea.
let nota = String::from("Llamar antes de entregar");
let etiqueta = nota;
Cuando Lía escribe let etiqueta = nota;, ¿cuántos bytes nuevos se reservan en el heap?
Es la idea de copiar objetos de JavaScript o Python: asignar crea otra copia. En Rust el texto del heap no se duplica al asignarlo, porque el String tiene un único dueño.
Exacto. Se copian los datos del descriptor en la pila y el texto del heap no se toca. Como ya no hay dos dueños, Rust invalida nota para que la memoria no se libere dos veces.
El descriptor vive en la pila, no en el heap, y al asignar no se reserva nada nuevo en el heap. El error está en pensar que cada variable guarda su propio bloque de memoria.
Libreta de Lía: Asignar un String transfiere su propiedad: el texto del heap sigue teniendo un único dueño y no se duplica.
Escena 2.2 · El registro que no quería ser Copy#
Para el registro de pedidos, Lía quiere que la estructura Pedido se pueda copiar como si fuera un par de números, sin llamar a clone() cada vez. Añade Copy al derive y el compilador se queja sobre el trait.
La estructura tiene dos campos: un identificador numérico y el nombre del cliente.
LíaSolo quiero copiar Pedido como si fuera un (i32, bool). ¿Qué tiene de malo añadir Copy?
rustcerror[E0204]: the trait Copy cannot be implemented for this type
BrunoMira qué campos tiene. ¿Todos viven solo en la pila?
#[derive(Clone, Copy)]
struct Pedido {
id: u32,
cliente: String,
}
¿Por qué Rust no deja que Pedido implemente Copy con un String dentro?
El coste de rendimiento no es la razón de fondo. Rust prohíbe Copy por seguridad de los recursos, no por cuánto tarda la copia.
Los tipos con Copy no tienen que ser solo números: (i32, bool) o [u8; 32] lo cumplen. Lo que importa es si el valor tiene algo que liberar al terminar.
Exacto. Si Pedido se copiara, cada copia intentaría liberar la misma memoria del heap. Por eso Copy solo se permite cuando el tipo no tiene nada que limpiar, como (i32, bool).
Libreta de Lía: Un tipo que gestiona un recurso, como un String, no puede ser Copy; solo lo son los datos que viven enteros en la pila.
Escena 2.3 · Cuánto ocupa cada referencia#
La función que imprime la etiqueta recibe un texto prestado, y Lía tiene que decidir qué tipo de parámetro usar. Antes quiere comprobar cuánto ocupa en la pila cada variable que apunta al mismo texto.
Tiene tres variables: el String dueño, una referencia a él y un trozo prestado de su contenido.
BrunoTienes text, r_text y slice, y los tres apuntan al mismo texto. ¿Crees que pesan lo mismo en la pila?
LíaSi apuntan al mismo sitio, deberían pesar lo mismo: una dirección.
BrunoPiensa en qué más necesita saber un &str para saber dónde termina el texto.
let text: String = String::from("systems");
let r_text: &String = &text;
let slice: &str = &text[..];
¿Cuántos bytes ocupa slice en la pila en una máquina de 64 bits?
Una referencia &T normal sí ocupa 8 bytes, pero &str necesita saber también cuánto mide el texto. Suponer que todas las referencias son iguales lleva a este error.
El String ocupa 24 bytes porque es dueño del texto y guarda capacidad y longitud. slice no es dueño, solo presta una vista, así que no hereda ese tamaño.
Exacto. Un &str es un fat pointer: guarda la dirección del inicio del texto y su longitud, dos valores de 8 bytes. No necesita la capacidad, que solo tiene sentido para el String dueño.
Libreta de Lía: Una referencia puede ser un puntero simple (&String) o un fat pointer con longitud (&str); su tamaño depende de lo que presta.
Escena 2.4 · Dos ventanas a la misma habitación#
Lía tiene que descontar el stock de un producto al confirmar un pedido. El código anterior del equipo tenía dos referencias al mismo número, una para leer y otra para escribir, y Lía quiere entender por qué aquella versión no era segura.
Bruno le pide que piense en lo que el compilador puede asumir sobre cada referencia.
BrunoLía, antes de elegir, piensa en el optimizador: ¿qué necesita saber para guardar un valor en un registro en vez de releerlo de la memoria?
rustcerror[E0502]: cannot borrow stock as mutable because it is also borrowed as immutable
LíaEso pasaría si leyera lectura después de crear escritura. El mensaje tiene sentido, pero no sé qué garantía lo evita.
let mut stock = 10;
let lectura = &stock;
println!("stock: {}", lectura);
let escritura = &mut stock;
*escritura -= 1;
Mientras lectura (un &T) existe, ¿qué garantía permite al compilador asumir que el valor no cambia?
Exacto. Mientras el préstamo compartido existe, el dato no puede cambiar, así que el optimizador puede mantenerlo en un registro sin releerlo. Es la regla de muchos lectores o un solo escritor.
Una referencia no copia el dato: &T apunta al mismo valor que el original. Si se copiara, no existiría el préstamo y la lectura no vería los cambios.
En un solo hilo sí puede haber conflicto: una escritura por otro puntero cambia lo que lee el otro, aunque no haya concurrencia. Pensar que un solo hilo lo evita es un error típico de quien viene de JavaScript.
Libreta de Lía: Con un &T vivo nadie puede mutar el dato, y esa certeza permite al compilador optimizar los accesos.
Escena 2.5 · El saludo que desaparecía#
Para la tarjeta de regalo, Lía escribe una función que genera el texto «hola mundo». Su primera versión devuelve una referencia al texto que la propia función acaba de crear, y el compilador la rechaza.
Lía tiene que decidir qué tipo de retorno debe tener la función para que sea segura.
Bruno¿De quién es el texto que se crea dentro de create_greeting?
LíaDe la variable s, supongo. Cuando la función termine, s desaparece, así que la referencia apuntaría a algo que ya no existe.
BrunoBien visto. Ahora decide qué debe recibir quien llama a la función.
fn create_greeting() -> &str {
let s = String::from("hola mundo");
&s
}
¿Qué tipo debe devolver create_greeting para que sea segura y no deje referencias colgantes?
Cambiar &str por &String no resuelve el problema: la referencia sigue apuntando a s, que se libera al salir de la función.
Un &'static str solo sirve para textos que existen durante todo el programa, como los literales escritos en el código. Este texto nace dentro de la función, así que no cumple esa condición.
Exacto. Al devolver String, la propiedad del texto pasa a quien llama y la memoria no se libera al terminar la función. Así no hay referencias colgantes.
Libreta de Lía: Si una función crea un dato, lo devuelve por valor para transferir su propiedad; una referencia solo puede prestar algo que ya existe fuera de ella.
Al final de la semana, Lía ya no ve el compilador como un enemigo. Cada error le cuenta quién es el dueño de un dato, cuándo se mueve y cuándo solo se presta.
En el siguiente capítulo, Bruno le pasará la tarea de los tickets de prioridad, donde un texto puede no ser válido. Lía tendrá que decidir cómo representar esos errores sin que el programa se rompa.
Capítulo 3 · Tickets sin prioridad#
Lía convierte prioridades escritas a mano en valores fiables y aprende a distinguir ausencia, fallo y pánico.
Se desbloquea al terminar el capítulo anterior.
Brote recibe los tickets de soporte desde un formulario web. Cada prioridad llega escrita por un cliente, con espacios de más, letras por error o números imposibles.
Lía tiene que escribir el parser que las convierte en valores del sistema. Bruno le pide una regla sencilla: si algo falta o falla, debe quedar escrito en el tipo de la función.
Escena 3.1 · La función que no podía devolver un error#
Lía escribe una función pequeña que lee la prioridad que el cliente ha escrito y la muestra en el panel de soporte. Ya tiene la línea con parse, pero el compilador se detiene antes de ejecutar nada.
BrunoAntes de tocar el parse, dime qué promete tu función a quien la llama.
LíaNada: no tiene flecha ->, así que devuelve (). Pero necesito ? por si el texto no es un número.
rustcerror[E0277]: the ? operator can only be used in a function that returns Result or Option
fn mostrar_prioridad(texto: &str) {
let prioridad = texto.trim().parse::<u8>()?;
println!("Prioridad: {prioridad}");
}
¿Qué debe cambiar Lía en la función para que ? sea válido?
Correcto: ? hace un retorno anticipado con Err, y eso solo es válido si el tipo de retorno puede transportarlo. Con Result, la función lo declara y quien la llama debe tratarlo.
.unwrap() no propaga el error: si el texto no es válido, entra en panic! y tumba el hilo. Es una mala salida para una entrada escrita por un cliente.
Indica el tipo del valor en el camino feliz, pero no da un lugar al Err que ? necesita devolver cuando el texto no es un número.
Libreta de Lía: ? solo puede usarse en una función que pueda devolver Err; si la firma no lo permite, el error no tiene dónde ir.
Escena 3.2 · El coste de un fallo#
Antes de trabajar en Brote, Lía programó en Java y estaba acostumbrada a que cada excepción llenara la consola de líneas. Ahora, en el parser de prioridades, se pregunta qué le cuesta de verdad a la máquina cada fallo.
BrunoCuando en Java se lanza una excepción, ¿qué le pasa al hilo antes de seguir?
LíaTiene que construir la traza de la pila. Me imagino que eso cuesta bastante.
BrunoY tu Result<u8, PriorityError>, ¿qué hace el procesador al comprobar si es Ok o Err?
fn parse_priority(input: &str) -> Result<u8, PriorityError> {
// ...
todo!()
}
¿Qué debería responder Lía sobre el coste de Result frente a una excepción?
Rust no tiene try/catch. El error viaja como valor de retorno y se trata con match o con ?, sin una captura oculta.
Correcto: el error es un valor pequeño y la comprobación es una comparación seguida de un salto. No se construye ninguna traza, por eso el coste es casi el de un entero.
Rust no lanza excepciones para errores ordinarios. Err es un valor que vuelve por retorno, no un salto que busque un bloque que lo capture.
Libreta de Lía: Un Result es un valor pequeño que se mira con una comparación; no es un salto invisible por la pila.
Escena 3.3 · Un test que no compila#
Lía escribe su primera prueba para el enum de errores. Quiere comprobar que la prioridad 0 produce el error de fuera de rango. Al compilar el archivo de pruebas, el compilador rechaza el código antes de ejecutar nada.
BrunoAntes de arreglar nada, mira qué hace assert_eq! con sus dos argumentos.
LíaLos compara con == y, si la prueba falla, imprime los dos lados.
rustcerror[E0277]: PriorityError doesn't implement Debug
enum PriorityError {
Empty,
OutOfRange(u8),
}
#[test]
fn rechaza_cero() {
let r: Result<u8, PriorityError> = Err(PriorityError::OutOfRange(0));
assert_eq!(r, Err(PriorityError::OutOfRange(0)));
}
¿Qué necesita PriorityError para que assert_eq! compile y muestre un fallo legible?
Correcto: assert_eq! compara con ==, que exige PartialEq, y si la prueba falla imprime ambos lados con {:?}, que exige Debug.
Eq añade reflexividad sobre PartialEq, pero no da formato de impresión. Sin Debug, la prueba seguiría sin poder mostrar el fallo.
Duplicar valores no resuelve la comparación. assert_eq! necesita poder usar == y mostrar los valores, y eso no lo dan Clone ni Copy.
Libreta de Lía: assert_eq! necesita PartialEq para comparar y Debug para mostrar los valores si la prueba falla.
Escena 3.4 · El pedido que no existe#
En Brote, la página de seguimiento busca pedidos por su número. Un día un cliente teclea un número que no existe. Lía tiene que decidir qué devuelve la función en ese caso, sin mezclarlo con una base de datos caída.
BrunoEl cliente escribe el 9999 y tu consulta vuelve sin filas. ¿Qué crees que ha pasado?
LíaLa base de datos ha respondido bien, pero no hay ningún pedido con ese número.
Bruno¿Eso es un fallo del sistema o un resultado normal de la búsqueda?
fn buscar_pedido(id: u32) -> Result<Option<Pedido>, DbError> {
todo!()
}
¿Qué debería devolver buscar_pedido cuando el número no existe y la base de datos responde bien?
Correcto: la base respondió sin filas, y eso es una ausencia esperada. Así la capa web puede responder con un 404 sin tratarlo como una caída.
Un Err indica que la operación no pudo completarse, por ejemplo por red o credenciales. Aquí la consulta sí se completó.
Un número mal escrito por el cliente no rompe el programa. Con panic!, una petición normal tumbaría el hilo que la atiende.
Libreta de Lía: Ok(None) es una ausencia esperada, Err es un fallo que impide completar la operación, y panic! queda para invariantes rotas.
Escena 3.5 · El mensaje que se rompía al cambiar una palabra#
Brote quiere mostrar un aviso distinto en la tienda según el motivo por el que se rechaza un pago. El equipo de frontend revisa el código de Lía y deja un comentario: su pantalla compara el texto del error para decidir qué mensaje enseña.
LíaEl frontend compara el texto con "insufficient funds". Funciona, pero depende de la frase exacta.
Bruno¿Qué pasa si mañana alguien corrige la redacción a «insufficient balance»?
LíaLa rama del frontend deja de cumplirse y el usuario ve un mensaje genérico, sin aviso de nada.
fn procesar_pago(importe: u32) -> Result<u32, String> {
let saldo: u32 = 500;
if importe > saldo {
return Err("insufficient funds".to_string());
}
Ok(importe)
}
¿Qué debería hacer Lía para que el frontend distinga los motivos sin depender del texto?
Correcto: el compilador obliga a cubrir cada variante y el mensaje se puede traducir sin tocar la lógica. El motivo queda como dato tipado, no como frase.
Sigue siendo texto. El frontend seguiría comparando frases, y un cambio de redacción rompería la misma rama sin aviso.
El mensaje sigue mezclando código y redacción, y el compilador no revisa que todos los casos estén cubiertos.
Libreta de Lía: Un error con variantes tipadas se revisa con match; un error escrito como texto se rompe al cambiar una palabra.
Lía cierra la semana con un parser que devuelve Err cuando el ticket no tiene sentido y Ok cuando sí. Ya no esconde el fallo en un texto ni en una excepción.
Pero el lote de tickets llega con cientos de líneas, no con una sola. En el siguiente capítulo tendrá que recorrer colecciones y decidir qué hacer con cada resultado.
Capítulo 4 · El inventario que había que contar bien#
Lía cuenta, filtra y recorre el inventario de Brote, y aprende a elegir entre iteradores y estructuras de datos antes de presentar su trabajo.
Se desbloquea al terminar el capítulo anterior.
Lía lleva casi dos meses en Brote y esta semana le toca el inventario. El backend nuevo en Rust tiene que contar existencias, filtrar catálogos y decidir qué colección usar antes de recibir pedidos reales.
El viernes presenta su trabajo en la demo del equipo. Bruno le ha dicho que no busca código perfecto, sino que sepa explicar por qué hace cada cosa.
Escena 4.1 · Los dos asteriscos del filtro#
El panel de almacén muestra cuántos productos tienen más de 15 unidades. El número que sale del código de Lía no coincide con el que ve el equipo de compras.
Lía ha usado iter() sobre el vector de existencias y filter para contar. Bruno se acerca a su mesa y le pide que mire la clausura antes de cambiar nada.
BrunoAntes de tocar nada, dime: ¿de qué tipo es x dentro de la clausura del filter?
LíaPues son números, ¿no? El vector tiene 10, 20 y 30.
rustcerror[E0308]: mismatched types
let stock = vec![10, 20, 30];
let count = stock
.iter()
.filter(|x| *x > 15) // error[E0308]
.count();
¿Por qué falla *x > 15 y hace falta escribir **x?
Rust no envuelve los números en objetos como Python: el vector guarda i32 directamente. La doble capa viene de las referencias que añade el iterador.
Los asteriscos no son decorativos: sin ellos x > 15 no compila, porque x no es un i32. filter no devuelve el valor, solo lo mira prestado.
Correcto. iter() rinde &i32; filter pide prestado ese elemento, así que la clausura recibe &&i32. Por eso hacen falta dos * para llegar al número.
Libreta de Lía: iter() presta y entrega &T; filter añade otra capa de referencia, así que la clausura recibe &&T.
Escena 4.2 · Mayúsculas sin copiar el catálogo#
Brote quiere que la web de almacén muestre los nombres de producto en mayúsculas. Lía tiene el vector names con todos los nombres ya cargados desde la base de datos.
La tarea parece sencilla: recorrer el vector y transformar cada nombre. Lo que elija afectará a lo que el resto del código puede hacer con names después del bucle.
BrunoEl informe de la tarde sigue leyendo names después del bucle. Tenlo en cuenta.
LíaEntonces el vector tiene que seguir existiendo cuando termine el bucle.
BrunoCorrecto. Tú decides cómo recorrerlo. ¿Qué método usarías?
let mut names = vec![String::from("cafe"), String::from("te")];
// for name in names.¿?() {
// name.make_ascii_uppercase();
// }
println!("{:?}", names); // el informe sigue usando names
¿Qué método usa Lía para pasar cada nombre a mayúsculas?
Correcto. iter_mut() presta el vector de forma exclusiva y entrega &mut String en cada vuelta, así que make_ascii_uppercase cambia cada nombre sin copiar el vector.
into_iter() consume el vector: cada String llega como valor, pero al terminar el bucle names ya no existe y el informe no podría leerlo.
Con iter() el cuerpo recibe &String, una referencia compartida: sirve para leer, no para modificar. Un préstamo compartido no permite cambiar el contenido.
Libreta de Lía: iter() lee, iter_mut() cambia en sitio e into_iter() consume el vector.
Escena 4.3 · El informe que no imprimía nada#
Cada noche, Brote genera un informe con los pedidos más grandes para el equipo de ventas. Lía ha escrito un pipeline nuevo con map y take(2) y quiere comprobar que hace lo que espera.
Ha puesto un println! dentro del map para ver qué pedidos se procesan. Ejecuta el programa tal cual, sin añadir nada después de la línea del pipeline.
Bruno¿Qué ves en consola cuando el programa termina?
LíaSupongo que los cinco pedidos, uno por línea. El map se aplica a todo, ¿no?
BrunoRazónalo despacio: ¿quién pide los elementos en esa línea?
let orders = vec![120, 45, 300, 80, 15];
let pipeline = orders.iter().map(|total| {
println!("Procesando {total}");
total * 2
}).take(2);
¿Cuántas líneas «Procesando» aparecen en la consola?
Array.map de JavaScript se ejecuta al momento, pero en Rust un adaptador no hace trabajo hasta que un consumidor lo pide.
Creer que take lanza el map al crear la cadena confunde el límite con el arranque: take solo corta cuando alguien pide elementos.
Correcto. Los adaptadores son perezosos: solo montan la cadena. Sin un consumidor como collect o count, el map nunca se llama y no imprime nada.
Libreta de Lía: Los adaptadores como map y take son perezosos: no hacen nada hasta que un consumidor como collect pide elementos.
Escena 4.4 · Un índice que tumbaba el servidor#
El endpoint del carrito recibe un selected_index desde el navegador y devuelve el artículo elegido. En una prueba de seguridad alguien envió un número enorme y el servidor de staging se cayó.
Lía revisa el código del endpoint en la revisión del martes. Bruno le pide que mire primero el log antes de proponer un arreglo.
BrunoMira el log del servidor de staging. ¿Qué te dice?
rustcthread 'main' panicked: index out of bounds: the len is 2 but the index is 999999
LíaQue el índice se pasó del tamaño del vector y el programa se paró entero.
BrunoBien visto. Y, en ese caso, ¿qué te gustaría que hiciera el endpoint?
let cart_items = vec!["camiseta", "gorra"];
let selected_index: usize = 999_999;
let item = cart_items[selected_index];
¿Qué cambio propone Lía para el endpoint?
Con > 0, el número 999999 pasa la comprobación y el pánico sigue. Hace falta comparar con el tamaño real del carrito, que es lo que hace get.
Correcto. get devuelve Option<&T>: None cuando el índice no existe. El endpoint decide la respuesta 400 y el proceso sigue vivo.
try/catch es de JavaScript y Python. Un pánico por índice no se controla así en el flujo normal; la forma idiomática es no provocarlo.
Libreta de Lía: Con get, un índice fuera de rango es un None que el endpoint puede tratar, no un pánico que tumba el servidor.
Escena 4.5 · La lista negra de seis palabras#
Brote filtra los comentarios de los usuarios antes de publicarlos. Cada comentario se revisa contra una lista negra de seis palabras, y en hora punta llegan unos 50.000 comentarios por segundo.
Un compañero propone usar un HashSet para hacer las búsquedas en tiempo constante. Lía quiere comparar antes de decidir, porque la lista es corta y el coste real importa.
BrunoLa teoría dice que O(1) siempre gana. ¿Qué crees que pasa con solo seis palabras?
LíaPues que el HashSet es más rápido, ¿no? Tiene búsqueda constante.
BrunoPiensa en cómo lee la CPU la memoria: ¿dónde están los datos y cuántos saltos necesita?
// Palabras que el buscador debe bloquear (no cambian nunca)
// "spam", "bot", "ad", "casino", "oferta", "gratis"
fn is_blocked(word: &str) -> bool {
todo!()
}
¿Qué estructura elige Lía para la lista negra?
La complejidad teórica no basta. Con seis elementos, calcular el hash y saltar por direcciones dispersas cuesta más que comparar seis palabras seguidas.
Correcto. Las seis palabras están contiguas: la CPU las lee desde caché sin fallos y sin calcular hash. La ventaja de HashSet aparece con cientos o miles de elementos.
Clonar cada palabra añade asignaciones en el heap sin mejorar la búsqueda. Para comparar basta con &str prestados que ya existen.
Libreta de Lía: Para listas pequeñas, la contigüidad en memoria puede ganar a O(1): la escala real de los datos decide la estructura.
Lía cierra la semana con el inventario contado, el filtro explicado y el endpoint protegido. En la demo del viernes enseña cada decisión, y Bruno le recuerda que una buena respuesta es la que sabe justificar.
Con esto termina el Módulo 01. El siguiente paso es el backend: en el Módulo 02, Lía recibirá pedidos por HTTP y los convertirá en datos tipados, usando de nuevo Option, Result y colecciones, esta vez en la frontera con el cliente.