Inferencia de tipos, anotaciones y sufijos
Cómo Rust deduce los tipos con inferencia local bidireccional, cuándo estás obligado a anotarlos, el turbofish, y los sufijos de literales como 5u8 o 3.0f64.
Rust es fuertemente tipado y estáticamente tipado, pero no verboso. Casi nunca escribes tipos dentro de una función porque el compilador los deduce con un motor de inferencia potente. La destreza consiste en entender hasta dónde llega esa deducción, cuándo se queda sin información y necesita tu ayuda, y cómo dársela con precisión mediante anotaciones o sufijos.
- Entender la inferencia local y bidireccional de Rust.
- Conocer las reglas de defecto:
i32para enteros,f64para floats. - Saber cuándo el compilador exige una anotación explícita.
- Usar sufijos de literales y el turbofish para fijar tipos.
Inferencia local y bidireccional
El compilador de Rust ejecuta una variante del algoritmo de Hindley-Milner: recoge restricciones de todo el cuerpo de una función y resuelve el tipo que las satisface. La inferencia es bidireccional: la información fluye no solo desde la definición hacia el uso, sino también de vuelta desde un uso posterior hacia la declaración.
fn main() {
let mut v = Vec::new(); // ¿Vec de qué? aún no se sabe
v.push(3i32); // ahora sí: el push fija v como Vec<i32>
println!("{v:?}");
}
En la primera línea el tipo de v está sin resolver; el push posterior lo determina. Esta es la marca de la inferencia bidireccional: un uso que aparece después condiciona una declaración anterior. Sin embargo, hay una frontera dura que conviene grabar:
Rust nunca infiere tipos a través de las fronteras de una función: las firmas (parámetros y retorno) son siempre explícitas y obligatorias. Esta decisión no es una limitación técnica, sino de diseño: hace que cada firma sea un contrato legible y estable, permite comprobar cada función de forma aislada, y garantiza que un cambio en el cuerpo de una función no altere en silencio los tipos que ven quienes la llaman.
Los tipos por defecto
Cuando después de recoger todas las restricciones un literal numérico sigue siendo ambiguo, Rust aplica un fallback: los enteros sin más pistas son i32 y los floats son f64. i32 es el default porque suele ser el entero más rápido en la mayoría de arquitecturas; f64 porque su precisión rara vez cuesta.
fn main() {
let n = 42; // sin más contexto -> i32
let x = 3.14; // sin más contexto -> f64
let big = 42; // pero si lo usamos como u64 más abajo...
let _s: u64 = big; // ...la restricción gana: big es u64
}
El fallback solo actúa como último recurso: cualquier restricción real —una anotación, una operación con un tipo conocido, un argumento a una función— tiene prioridad sobre el default.
Cuándo hay que anotar
Hay tres situaciones típicas en las que la inferencia se queda sin información suficiente y el compilador te lo exige con un error como type annotations needed:
1. Colecciones vacías sin uso posterior que las fije. Si nada determina el elemento, debes decirlo:
let vacio: Vec<String> = Vec::new(); // sin esto, tipo indeterminado
2. Métodos polimórficos en el retorno, como parse o collect. Su tipo de salida depende de lo que pidas, no de la entrada:
let n: i32 = "42".parse().unwrap(); // anotación en el binding
let m = "42".parse::<i32>().unwrap(); // o turbofish en la llamada
3. collect hacia un contenedor ambiguo:
let cuadrados: Vec<i32> = (1..=5).map(|x| x * x).collect();
flowchart TD
A[Literal o expresion sin tipo] --> B{Hay restricciones}
B -->|Si| C[Resolver con Hindley-Milner]
B -->|No, pero es numerico| D[Aplicar default i32 o f64]
B -->|No, y no numerico| E[Error type annotations needed]
E --> F[Anota el binding o usa turbofish]Sufijos de literales y turbofish
Puedes fijar el tipo de un literal en el propio literal pegándole un sufijo, sin necesidad de una anotación aparte. Es la forma más local de resolver ambigüedad:
let a = 5u8; // u8
let b = 5_000i64; // i64, con separador visual
let c = 3.0f32; // f32
let d = 0xffu32; // u32 en hexadecimal
let e = 1_000_000u64;
El turbofish ::<> cumple la misma función pero sobre una llamada genérica, indicando con qué tipo instanciarla. Es indispensable cuando el tipo vive en el retorno de un método polimórfico:
let suma = (1..=100).sum::<u64>(); // fija el acumulador como u64
let parseado = "255".parse::<u8>().unwrap(); // parse a u8, no a i32
let coleccion = (0..3).collect::<Vec<_>>(); // Vec; el _ lo infiere él
Fíjate en el _ dentro de Vec<_>: puedes fijar parte del tipo y dejar que Rust deduzca el resto. Es la anotación mínima: das justo la pista que falta y ni una letra más.
El error type annotations needed frustra a los principiantes porque parece que el compilador es tonto: “¿cómo que no sabes el tipo, si es obvio?”. Pero casi siempre no es obvio para el compilador, y esa distinción es el aprendizaje. La inferencia de Rust es deliberadamente local y explícita en las fronteras: dentro de una función es casi omnisciente, pero se niega a adivinar a través de firmas o a resolver ambigüedades reales por defecto silencioso. Esto produce una propiedad valiosísima: los tipos de tu programa están determinados, no adivinados, y cualquier lector —humano o máquina— puede reconstruirlos leyendo solo la función actual y las firmas que toca, nunca todo el programa. Cuando el compilador te pide una anotación, no está fallando: te está señalando el único punto donde la intención era genuinamente ambigua, y te pide que la hagas explícita justo ahí. Aprende a leer ese error como una pregunta precisa, no como un reproche, y responderás con una anotación quirúrgica en vez de dispersar tipos por todas partes.
- Escribe
let v = Vec::new();sin usarvy observa el error; arréglalo con anotación y luego con unpushposterior. - Parsea
"128"au8con turbofish y comprueba qué pasa si lo parseas ai8. - Suma un rango con
.sum::<u64>()y luego sin turbofish anotando el binding; confirma que ambos compilan. - Declara el mismo literal
42forzándolo a seru8,i64yusizemediante sufijos. - Usa
collect::<Vec<_>>()y explica qué parte del tipo estás fijando y cuál delega en la inferencia.