foldBy

Reduce los valores de cada clave en una sola pasada: el agregado, sin los grupos.

Map<K, Acc> foldBy<A, K, Acc>(K Function(A a) key, Acc seed, Acc Function(Acc acc, A a) f, Iterable<A> iterable) Future<Map<K, Acc>> foldByAsync<A, K, Acc>(FutureOr<K> Function(A a) key, FutureOr<Acc> seed, FutureOr<Acc> Function(Acc acc, A a) f, FxAsyncIterable<A> iterable) Map<K, Acc> Fx.foldBy<K, Acc>(K Function(T a) key, Acc seed, Acc Function(Acc acc, T a) f) // chain (sync) Future<Map<K, Acc>> FxAsync.foldBy<K, Acc>(FutureOr<K> Function(T a) key, FutureOr<Acc> seed, FutureOr<Acc> Function(Acc acc, T a) f) // chain (async)

Lección

foldBy es fold ejecutado una vez por clave en lugar de una vez sobre toda la fuente. Cada elemento elige su clave y su valor se reduce dentro del acumulador de esa clave, así que el resultado es un Map<K, Acc> de respuestas, no de elementos.

La razón de que exista es lo que no hace. groupBy seguido de una reducción por grupo tiene que construir antes una List para cada clave: asignación proporcional a la entrada para una respuesta proporcional al número de claves. Si lo único que quieres es el total por categoría, esas listas se construyen y se tiran. foldBy acumula directamente en el mapa de resultado, como el bucle escrito a mano:

// lo que escribirías a mano
for (final t in txns) {
  totals[t.category] = (totals[t.category] ?? 0) + t.amount;
}

// lo mismo, con nombre
foldBy((Tx t) => t.category, 0.0, (sum, t) => sum + t.amount, txns);

Sobre un millón de transacciones repartidas en cinco categorías, agrupar primero cuesta 2,7× el bucle escrito a mano. foldBy cuesta 0,91× — es algo más rápido que el bucle que tiene al lado, y no es un artefacto de redondeo. El bucle lee el mapa y luego lo reescribe, así que cada transacción calcula el hash de su categoría dos veces; foldBy acumula en una celda mutable alojada en el mapa, de modo que este se escribe una vez por categoría y no una vez por transacción. Varios de los ejemplos de Dart vs FxDart se pasaron a él justo por eso.

No leas demasiado en ese margen: aquí el callback del fold es una suma, así que el mapa es casi todo el trabajo. Con un acumulador más pesado el ahorro sigue ahí, pero desaparece dentro del coste del propio callback — mira la nota sobre records al final. La razón para elegir foldBy es que dice lo que quieres decir; ser una pizca más rápido que el bucle es una ventaja añadida, no el argumento.

Las claves salen en el orden en que aparecen por primera vez, igual que en groupBy. No es un port de FxTS: la forma viene de groupingBy().fold() de Kotlin.

La semilla es un valor, no una fábrica. Igual que en fold, seed es un único valor que se usa como punto de partida para todas las claves. Eso está bien con números y cadenas, que reduces hacia valores nuevos. Una semilla mutable — una lista, un conjunto, un mapa — la compartirían todas las claves y todas la mutarían. Si necesitas acumular en una estructura mutable por grupo, usa groupBy.

Demo 1 · Lo básico

Demo 2 · Asíncrono

Pruébalo tú

Ejercicio: la demo cuenta palabras por letra inicial. Cámbiala para que sume las letras bajo cada inicial, de modo que fig y fx den {f: 5}.

Cuándo no usarlo: un acumulador que necesita dos valores en curso — una media necesita una suma y un recuento — tiene que llevarlos en un record, y se asigna un record por elemento. Eso cuesta más que la agrupación que evitas; ahí usa groupBy.
Relacionados: groupBy — conserva los elementos en lugar de reducirlos · countByfoldBy con un contador, ya con nombre · fold — la misma reducción sobre un solo acumulador · groupedBy — agrupación que se queda en la cadena