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.