Diseño de software

Patrón iterador en Swift: principios de Sequence

Al desarrollar aplicaciones con Swift, escribimos decenas de veces al día bucles como for item in array. Como resulta tan natural, solemos preguntarnos qué ocurre realmente dentro solo después de intentar recorrer un tipo personalizado con for-in y encontrarnos con un error de compilación. «¿Qué sucede exactamente dentro de este bucle?»

4 min de lectura
Imagen de portada de Patrón iterador en Swift: principios de Sequence

Al desarrollar aplicaciones con Swift, escribimos decenas de veces al día bucles como for item in array. Como resulta tan natural, solemos preguntarnos qué ocurre realmente dentro solo después de intentar recorrer un tipo personalizado con for-in y encontrarnos con un error de compilación. «¿Qué sucede exactamente dentro de este bucle?»

En resumen, el bucle for-in de Swift es el resultado del trabajo conjunto de los protocolos Sequence y IteratorProtocol. Sequence declara «puedo recorrerse», mientras que IteratorProtocol extrae los valores: «te daré el siguiente».

Aquí veremos cómo se reparten sus funciones, cómo se descompone for-in internamente y cómo crear una secuencia personalizada. Si el patrón iterador te parece difícil, sigue leyendo hasta el final.

¿En qué se diferencian Sequence e IteratorProtocol?

Empecemos por el punto más confuso. Ambos se relacionan con la iteración, pero sus funciones son claramente distintas.

IteratorProtocol solo exige una cosa: mutating func next() -> Element?. Devuelve un elemento en cada llamada y retorna nil cuando ya no quedan elementos.

En otras palabras, el iterador es un «cursor que mantiene el estado». Recuerda hasta dónde ha leído y avanza una posición cada vez que se llama a next().

Sequence está en un nivel superior. Su función es crear y entregar un iterador mediante el método makeIterator(). Es como decir: «si quieres recorrerme, usa este iterador».

Resumen:

  • IteratorProtocol: cursor que extrae valores uno a uno. La clave es next().
  • Sequence: fábrica que crea ese cursor. La clave es makeIterator().

Array, diccionario, Set, cadenas, Range… Todos los tipos iterables de la biblioteca estándar de Swift siguen Sequence.


¿Cómo se descompone for-in internamente?

Este es el punto central del artículo. El for-in que usamos sin pensar es, en realidad, azúcar sintáctico (syntactic sugar).

Este es el bucle que usamos habitualmente.

for number in [1, 2, 3] {
    print(number)
}

El compilador transforma aproximadamente este código a la forma siguiente: crea un iterador con makeIterator() y ejecuta un bucle while hasta que next() devuelve nil.

var iterator = [1, 2, 3].makeIterator()
while let number = iterator.next() {
    print(number)  // 1, 2, 3 imprimir en orden
}

for-in no es magia: al final, solo combina makeIterator() y next().

Al final, solo eran estas dos llamadas
Al final, solo eran estas dos llamadas

Con esta estructura, resulta natural entender por qué un tipo que solo adopta Sequence puede usarse directamente con for-in: for-in solo necesita makeIterator().


¿Cómo crear una secuencia personalizada?

Se entiende mejor al implementarla. Usemos como ejemplo una secuencia que genera infinitamente la sucesión de Fibonacci.

Lo más sencillo es adoptar Sequence e IteratorProtocol en un tipo pequeño. Así, la implementación predeterminada de makeIterator() copia y devuelve el propio tipo.

struct Fibonacci: Sequence, IteratorProtocol {
    var current = 0
    var nextValue = 1
    mutating func next() -> Int? {
        defer { (current, nextValue) = (nextValue, current + nextValue) }
        return current
    }
}

La tarea de next() es simple: calcula por adelantado el siguiente estado con defer justo antes de devolver el valor actual.

Ahora el tipo puede usarse directamente con for-in. Como la secuencia es infinita, hay que limitar manualmente el número de iteraciones.

for n in Fibonacci().prefix(8) {
    print(n)  // 0 1 1 2 3 5 8 13
}
Probé un tipo Fibonacci propio con for-in
Probé un tipo Fibonacci propio con for-in

También es importante que puedes usar gratis métodos como prefix, map y filter. Al adoptar Sequence, estas operaciones estándar llegan mediante extensiones del protocolo; no necesitas implementarlas.


Preguntas frecuentes

P. ¿Basta con adoptar Sequence o también es imprescindible IteratorProtocol? R. Para ejecutar for-in necesitas un iterador. Sin embargo, como en el ejemplo anterior, si un tipo adopta ambos protocolos no tienes que escribir makeIterator() aparte. Si quieres separar el estado de iteración de la colección, extrae el iterador a un struct independiente.

P. ¿Qué ocurre si vuelvo a recorrer con for-in una secuencia ya recorrida? R. Los tipos por valor, como Array, crean un iterador nuevo cada vez y pueden recorrerse varias veces con seguridad. En cambio, las secuencias consumibles, como un flujo de red, solo pueden recorrerse una vez.

P. ¿En qué se diferencia de Collection? R. Collection es un concepto superior que hereda de Sequence. Exige más capacidades, como acceso repetido por índice, contar con count y garantizar el orden. Si solo necesitas recorrer valores en una dirección, Sequence es suficiente.

Al dibujar la relación entre Sequence, Iterator y Collection, todo quedó claro de un vistazo
Al dibujar la relación entre Sequence, Iterator y Collection, todo quedó claro de un vistazo

Detrás de una sola línea de for-in se esconde una división clara entre Sequence e IteratorProtocol: el iterador actúa como cursor y la secuencia como fábrica. Al distinguirlos, crear tipos iterables personalizados resulta mucho más sencillo.

La próxima vez que uses for-in, piensa: «ahora se está llamando a next()». Te recomiendo empezar creando tu propia secuencia de Fibonacci.

Seguir leyendo