Iterable: La abstracción de secuencias perezosas en Dart

Iterable<T> es la interfaz base que define una secuencia de elementos que pueden ser recorridos uno a uno. A diferencia de una List, no es una estructura de datos que almacene elementos en posiciones de memoria específicas; es un contrato que garantiza que puedes pedir el “siguiente” elemento mediante un objeto Iterator. Esta distinción es fundamental: mientras que una List es una colección estática en memoria, un Iterable es un plan de navegación.

Este diseño permite la evaluación perezosa (lazy evaluation), un mecanismo donde el cálculo de un elemento solo ocurre en el preciso momento en que se solicita. Esto es posible gracias al protocolo de iteración, donde el Iterable delega la responsabilidad del estado al Iterator. Este último utiliza el método moveNext() para avanzar el puntero y la propiedad current para exponer el valor actual. Gracias a esto, puedes trabajar con secuencias infinitas, ya que nunca se intentará calcular el “final” de la secuencia, sino solo los elementos que decidas consumir.

Debes usar Iterable cuando necesites encadenar transformaciones como map, where o take para procesar flujos de datos de forma eficiente sin crear colecciones intermedias que saturen la memoria. Sin embargo, si intentas realizar operaciones de acceso aleatorio (como iterable[5]) en un iterable que no sea una List, o si iteras múltiples veces un iterable que depende de un generador con efectos secundarios, el rendimiento caerá drásticamente o obtendrás resultados inconsistentes.

import 'dart:math';

/// Una secuencia infinita de números de Fibonacci.
/// Al implementar Iterable, definimos cómo se generan los datos, no cómo se guardan.
class Fibonacci extends Iterable<int> {
  @override
  Iterator<int> get iterator => _FibonacciIterator();
}

class _FibonacciIterator implements Iterator<int> {
  int _a = 0;
  int _b = 1;
  int _current = 0;
  bool _isFirst = true;

  @override
  bool get hasNext => true; // La secuencia es infinita

  @override
  int get current => _current;

  @override
  bool moveNext() {
    _current = _a;
    int next = _a + _b;
    _a = _b;
    _b = next;
    return true;
  }
}

void main() {
  // 1. Uso de un generador de fábrica (Iterable.generate)
  // Crea un iterable de números aleatorios de 0 a 9.
  final randomNumbers = Iterable.generate(10, (i) => Random().nextInt(100));

  // 2. Transformaciones perezosas (Lazy)
  // Fibonacci es infinita, pero 'take' limita la ejecución.
  // 'where' y 'map' no ejecutan nada hasta que llamamos a 'toList()'.
  final processedFibonacci = Fibonacci()
      .take(10)                 // Limitamos a 10 elementos para evitar un bucle infinito
      .where((n) => n % 2 == 0) // Solo pares
      .map((n) => 'Fib: $n');    // Transformamos a String

  print('--- Fibonacci Procesado ---');
  print(processedFibonacci.toList());

  print('\n--- Random Numbers (Original) ---');
  // Aquí se materializa el iterable en una lista real en memoria
  final listified = randomNumbers.toList();
  print(listified);
  
  // 3. Diferencia crucial: Iterable vs List
  // Un Iterable no tiene un 'length' garantizado sin recorrerlo (excepto en List)
  // pero podemos usarlo para inspeccionar elementos.
  print('\nPrimeros 3 números aleatorios: ${randomNumbers.take(3).toList()}');
}

Análisis del código

En la clase Fibonacci, hemos implementado el protocolo de iteración manualmente. Al extender Iterable, nos vemos obligados a proporcionar un Iterator. El _FibonacciIterator mantiene el estado de la secuencia (_a y _b) de forma interna. Fíjate que hasNext siempre devuelve true: esto es legal en un Iterable y es lo que permite crear secuencias infinitas.

Cuando en el main ejecutamos Fibonacci().take(10).where(...).map(...), no se ha realizado ningún cálculo matemático. Dart ha creado una cadena de objetos “envoltorios”. take(10) es el único que tiene el control de detener la ejecución. Solo cuando llamamos a toList(), se inicia el proceso: se pide un iterator, se llama a moveNext(), el valor pasa por el filtro de where, luego por el map y finalmente se guarda en la nueva List.

La función Iterable.generate es un método de utilidad que nos ahorra implementar todo el protocolo manualmente cuando solo necesitamos una secuencia basada en un índice. Sin embargo, el resultado de generate sigue siendo un Iterable, no una List, lo que significa que el cálculo de Random().nextInt se pospone hasta que la iteración ocurre.

El error frecuente

Un error común al trabajar con transformaciones es asumir que un Iterable es una estructura de datos estática. Si la lógica de transformación tiene efectos secundarios (como modificar una variable externa o realizar una llamada a una API), iterar el mismo Iterable dos veces producirá resultados diferentes o comportamientos erráticos.

int contador = 0;

void main() {
  // Generamos un iterable que incrementa un contador cada vez que se accede a un elemento
  final iterableConEfecto = List.generate(3, (i) {
    contador++;
    return i;
  });

  print('Primera iteración: ${iterableConEfecto.toList()} | Contador: $contador');
  // El contador es 3

  print('Segunda iteración: ${iterableConEfecto.toList()} | Contador: $contador');
  // El contador es 6, ¡pero el contenido es el mismo!
  
  // El peligro real ocurre cuando la lógica de transformación NO es pura:
  var transformacionPeligrosa = iterableConEfecto.map((e) {
    contador++; 
    return e * 2;
  });

  print('Transformación 1: ${transformacionPeligrosa.toList()} | Contador: $contador');
  print('Transformación 2: ${transformacionPeligrosa.toList()} | Contador: $contador');
  // El contador saltará descontroladamente cada vez que intentes leer transformacionPeligrosa.
}

Dominar la evaluación perezosa es la diferencia entre un proceso que consume toda la RAM y uno que procesa gigabytes de datos con un uso de memoria constante.

37

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio