study.plynte.com
Glosario Rust Glosario Algoritmos Legal & IA @franguh
📑 Índice de esta página (4 secciones)

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.

💡 Consejo Práctico

¿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:

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.

Tu progreso 0 / 20 escenas
Continuar donde lo dejé

Capítulo 1 · El punto y coma que costó un envío#

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.

RUST
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?

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.

RUST
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?

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.

RUST
let active = true;
let code = if active { 1_u32 };

¿Por qué rustc rechaza let code = if active { 1_u32 };?

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?

RUST
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 $?

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.

RUST
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?

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 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.

RUST
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?

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?

RUST
#[derive(Clone, Copy)]
struct Pedido {
    id: u32,
    cliente: String,
}

¿Por qué Rust no deja que Pedido implemente Copy con un String dentro?

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.

RUST
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?

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.

RUST
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?

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.

RUST
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?

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#

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

RUST
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?

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?

RUST
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?

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

RUST
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?

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?

RUST
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?

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.

RUST
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?

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 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

RUST
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?

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?

RUST
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?

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?

RUST
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?

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?

RUST
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?

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?

RUST
// 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?

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.