collect polimórfico: Vec, String, HashMap y el turbofish
collect es un consumidor polimórfico: el mismo iterador puede desembocar en un Vec, una String, un HashMap o un BTreeMap, y quien lo decide es el tipo de destino vía FromIterator. Cómo elegirlo con anotaciones o con el turbofish collect::<Vec<_>>().
collect es el consumidor universal: transforma un iterador perezoso en una colección concreta. Lo asombroso es que un único método puede producir un Vec, una String, un HashMap, un HashSet o un BTreeMap, y quien decide cuál no es la cadena de adaptadores, sino el tipo de destino. Detrás de esa flexibilidad hay un trait, FromIterator, y una inferencia de tipos que fluye hacia atrás. Entender collect es entender cómo Rust despacha por el tipo de retorno.
- Entender que
collectes genérico sobre su destino mediante el traitFromIterator. - Recolectar en
Vec,Stringy colecciones asociativas comoHashMapyBTreeMap. - Elegir el destino con anotación de tipo o con el turbofish
collect::<...>(). - Reconocer cuándo la inferencia basta y cuándo hay que desambiguar el tipo.
collect y el trait FromIterator
collect no sabe construir nada por sí mismo. Su firma dice, en esencia, “dame un tipo B que sepa fabricarse a partir de un iterador de mis elementos, y te lo devuelvo”:
// Firma simplificada de collect
fn collect<B: FromIterator<Self::Item>>(self) -> B
Toda la maquinaria vive en FromIterator. Un tipo que lo implemente declara cómo consumir un iterador y agregarse a sí mismo. Vec, String, HashMap, HashSet, BTreeMap, Result y muchos más lo hacen. Por eso collect es un solo método con un número abierto de comportamientos: el destino elige la semántica, y cualquiera puede añadir la suya implementando el trait para su propia colección.
Un iterador, muchos destinos
El mismo pipeline puede desembocar en colecciones distintas sin cambiar una coma de la cadena, solo su tipo de llegada. A Vec, lo evidente: una secuencia por posición.
let cuadrados: Vec<i32> = (1..=5).map(|n| n * n).collect();
assert_eq!(cuadrados, [1, 4, 9, 16, 25]);
A String, cuando los elementos son char o &str: collect los concatena.
let grito: String = "hola".chars().map(|c| c.to_ascii_uppercase()).collect();
assert_eq!(grito, "HOLA");
A un HashMap, cuando el iterador produce pares (clave, valor): cada tupla se inserta como entrada. Es el idioma para construir un índice.
use std::collections::HashMap;
let precios = [("pan", 2), ("leche", 1)];
let mapa: HashMap<&str, i32> = precios.into_iter().collect();
assert_eq!(mapa["pan"], 2);
Cambia HashMap por BTreeMap y obtienes el mismo índice ordenado por clave; cambia a HashSet y recolectas valores únicos. Un solo verbo, collect, y la elección del contenedor decide de golpe estructura, orden y unicidad.
Vec y String
Secuencias por posición. Vec<T> acumula en orden de llegada; String concatena char o &str.
HashMap y BTreeMap
Índices clave-valor desde tuplas. El primero se ordena por hash; el segundo, por clave.
HashSet y BTreeSet
Conjuntos que descartan duplicados. Ideales para deduplicar un flujo en un solo paso.
El turbofish: desambiguar el destino
Como collect es polimórfico en su salida, el compilador necesita conocer el tipo de destino. Casi siempre lo deduce de una anotación —let v: Vec<_> = ...— o del contexto —un return, un argumento de función—. Cuando no hay ningún indicio, la inferencia falla con el célebre error “type annotations needed”, y hay que decírselo en el propio collect con el turbofish:
let v = (1..=3).collect::<Vec<i32>>(); // turbofish: destino explicito
let n = (1..=3).collect::<Vec<_>>().len(); // el _ deja inferir el elemento
El turbofish, ::<...>, ancla el parámetro de tipo justo donde vive la ambigüedad. Es especialmente útil cuando quieres encadenar un método sobre el resultado —como .len() arriba— sin introducir una variable intermedia solo para poder anotarla. El _ interno delega en la inferencia lo que sí es deducible: fija el contenedor —Vec— y deja que el compilador complete el elemento.
En collect::<Vec<_>>(), el _ no significa “cualquier tipo”: significa “este lo deduces tú”. Fijas la parte ambigua —que el contenedor es un Vec— y dejas que la inferencia complete el elemento a partir de lo que produce la cadena. Es el punto justo de anotación: ni de más, escribiendo el tipo entero, ni de menos, dejando al compilador sin pistas.
Recolectar en tu propio tipo
Como collect delega en FromIterator, cualquier tipo que lo implemente se vuelve un destino válido sin tocar la biblioteca estándar. Basta describir cómo agregarse a partir de un iterador de elementos:
struct Promedio {
media: f64,
n: usize,
}
impl FromIterator<f64> for Promedio {
fn from_iter<I: IntoIterator<Item = f64>>(iter: I) -> Self {
let mut suma = 0.0;
let mut n = 0;
for x in iter {
suma += x;
n += 1;
}
let media = if n == 0 { 0.0 } else { suma / n as f64 };
Promedio { media, n }
}
}
let p: Promedio = [2.0, 4.0, 6.0].into_iter().collect();
assert_eq!(p.media, 4.0);
assert_eq!(p.n, 3);
Ahora collect sabe fabricar un Promedio, y toda la maquinaria de iteradores —map, filter, chain— desemboca en él como en cualquier Vec. Implementar FromIterator es la vía para que tus propios tipos se integren, como ciudadanos de primera, en el lenguaje de flujos del nivel.
flowchart LR it[Iterador de elementos] --> col[collect con destino B] col --> vec[Vec secuencia] col --> str[String texto] col --> map[HashMap indice] col --> set[HashSet unicos] style it fill:#cba6f7,color:#11111b style col fill:#89b4fa,color:#11111b style vec fill:#a6e3a1,color:#11111b
Casi todos los lenguajes eligen qué función ejecutar mirando los argumentos: el tipo de lo que entra decide el método. collect hace algo que a un programador de Java o Python le resulta casi imposible de imaginar: despacha por el tipo de retorno. La misma llamada, sobre el mismo iterador, produce un Vec, una String, un HashMap o un Result, y lo que lo decide no es nada visible en la expresión, sino el tipo que el contexto espera después. Esto solo es posible porque la inferencia de tipos de Rust —heredera de Hindley-Milner— fluye en ambas direcciones: no solo de las hojas hacia la raíz, deduciendo el tipo de una expresión a partir de sus partes, sino también de la raíz hacia las hojas, propagando hacia atrás lo que el destino exige. Cuando escribes let m: HashMap<_, _> = pares.collect(), esa anotación viaja hacia el interior del collect, elige la implementación de FromIterator para HashMap y con ella la semántica entera —insertar pares, resolver claves repetidas quedándose con la última—. El turbofish no es más que la vía para inyectar esa información cuando el contexto no la aporta. Lo profundo es lo que este diseño revela: la abstracción no se paga en ejecución —cada destino tiene su FromIterator monomorfizado, tan directo como un bucle de push— y sin embargo se expresa con un único verbo universal. Un método, collect, y una familia abierta de tipos que saben recibirlo: cualquiera puede implementar FromIterator para su colección y heredar, gratis, toda la maquinaria de iteradores del lenguaje. Es polimorfismo genuino, resuelto en compilación, sin una sola indirección. La rareza de despachar por el retorno deja de ser un truco y se muestra como lo que es: la consecuencia natural de tomarse en serio, a la vez, la expresividad y el coste cero.
- Recolecta
(1..=5)en unVec<i32>y, por separado, en unaStringde sus dígitos; explica qué implementación deFromIteratorentra en juego en cada caso. - Construye un
HashMap<&str, usize>que asocie cada palabra de una frase con su longitud usandomapycollect. - Provoca a propósito el error “type annotations needed” quitando toda anotación, y arréglalo de tres formas: anotando la variable, con turbofish completo y con
Vec<_>. - Recolecta una lista con duplicados en un
HashSety en unBTreeSet; compara qué garantiza cada uno sobre orden y unicidad. - Explica, apoyándote en la inferencia bidireccional, por qué
let v = coleccion.iter().collect();sin más contexto no puede compilar.