Capítulo 10. Iteradores

Los iteradores no son un concepto original de Ruby. Son comunes en otros lenguajes orientados a objetos. También se utilizan en Lisp aunque no se les conoce como iteradores. Sin embargo este concepto de iterador es muy poco familiar para muchas personas por lo que se explorará con detalle.

Como ya se sabe, el verbo iterar significa hacer la misma cosa muchas veces, por lo tanto un iterador es algo que hace la misma cosa muchas veces.

Al escribir código se necesitan bucles en diferentes situaciones. En C, se codifican utilizando for o while. Por ejemplo:


char *str;
for (str = "abcdefg"; *str != '\0'; str++) {
  /* aquí procesamos los caracteres */
}

La sintaxis del for(...) de C nos dota de una abstracción que nos ayuda en la creación de un bucle pero, la comprobación de si *str es la cadena nula requiere que el programador conozca los detalles de la estructura interna de una cadena. Esto hace que C se parezca a un lenguaje de bajo nivel. Los lenguajes de alto nivel se caracterizan por un soporte más flexible a la iteración. Consideremos el siguiente guión de la shell sh:


#!/bin/sh

for i in *.[ch]; do
  # ... aquí se haría algo con cada uno de los ficheros
done

Se procesarían todos los ficheros fuentes en C y sus cabeceras del directorio actual, el comando de la shell se encargaría de los detalles de coger y sustituir los nombres de los ficheros uno por uno. Pensamos que este es un método de trabajo a nivel superior que C, ¿Verdad?

Pero hay más cosas a tener en cuenta: aunque está bien que un lenguaje tenga iteradores para todos los tipos de datos definidos en él, es decepcionante tener que volver a escribir bucles de bajo nivel para los tipos de datos propios. En la POO, los usuarios definen sus propios tipos de datos a partir de otros, por lo tanto, esto puede ser un problema serio.

Luego, todos los lenguajes OO incluyen ciertas facilidades de iteración. Algunos lenguajes proporcionan clases especiales con este propósito; Ruby nos permite definir directamente iteradores.

El tipo strings de Ruby tiene algunos iteradores útiles:


ruby> "abc".each_byte{|c| printf"{%c}", c}; print "\n"
{a}{b}{c}
nil

each_byte es un iterador sobre los caracteres de una cadena. Cada carácter se sustituye en la variable local c. Esto se puede traducir en algo más parecido a C ...


ruby> s="abc";i = 0
0
ruby> while i < s.length
ruby|   printf "{%c}",s[i]; i+=1
ruby| end; print "\n"
{a}{b}{c}
nil

... sin embargo el iterador each_byte es a la vez conceptualmente más simple y tiene más probabilidades de seguir funcionando correctamente incluso cuando, hipotéticamente, la clase string se modifique radicalmente en un futuro. Uno de los beneficios de los iteradores es que tienden a ser robustos frente a tales cambios, además, ésta es una característica del buen código en general. (Si, tengamos paciencia también hablaremos de lo que son las clases)

each_line es otro iterador de String.


ruby> "a\nb\nc\n".each_line{|l| print l}
a
b
c
"a\nb\nc\n"

Las tareas que más esfuerzo llevan en C (encontrar los delimitadores de línea, generar subcadenas, etc.) se evitan fácilmente utilizando iteradores.

La sentencia for que aparece en capítulos previos itera como lo hace el iterador each. El iterador each de String funciona de igual forma que each_line, reescribamos ahora el ejemplo anterior con un for:


ruby> for l in "a\nb\nc\n"
ruby|   print l
ruby| end
a
b
c
"a\nb\nc\n"

Se puede utilizar la sentencia de control retry junto con un bucle de iteración y se repetirá la iteración en curso desde el principio.


ruby> c = 0
0
ruby> for i in 0..4
ruby|   print i
ruby|   if i == 2 and c == 0
ruby|           c = 1
ruby|           print "\n"
ruby|           retry
ruby|   end
ruby| end; print "\n"
012
01234
nil

A veces aparece yield en la definición de un iterador. yield pasa el control al bloque de código que se pasa al iterador (esto se explorará con más detalle es el capítulo sobre los objetos procedimiento). El siguiente ejemplo define el iterador repeat, que repite el bloque de código el número de veces especificado en el argumento.


ruby> def repeat(num)
ruby|   while num > 0
ruby|           yield
ruby|           num -= 1
ruby|   end
ruby| end
nil
ruby> repeat(3){ print "foo\n" }
foo
foo
foo
nil

Con retry se puede definir un iterador que funciona igual que while, aunque es demasiado lento para ser práctico.


ruby> def WHILE(cond)
ruby|   return if not cond
ruby|   yield
ruby|   retry
ruby| end
nil
ruby> i=0;WHILE(i<3){ print i; i+=1 }
012nil

¿Se entiende lo que son los iteradores? Existen algunas restricciones pero se pueden escribir iteradores propios; y de hecho, al definir un nuevo tipo de datos, es conveniente definir iteradores adecuados para él. En este sentido los ejemplos anteriores no son terriblemente útiles. Volveremos a los iteradores cuando se sepa lo que son las clases.