Iteradores

P: ¿Qué es un iterador?
P: ¿Cómo puedo pasar un bloque a un iterador?
P: ¿Cómo se utiliza un bloque en un iterador?
P: ¿Qué hace Proc.new sin un bloque?
P: ¿Cómo puedo ejecutar iteradores en paralelo?

P: ¿Qué es un iterador?

R: Un iterador es un método que acepta un bloque o un objeto Proc. En un programa fuente, estos aparecen inmediatamente después de la llamada a un método. Se utilizan para crear estructuras de control definidas por los usuarios - especialmente bucles.

Examinemos un ejemplo para ver como funcionan. Los iteradores se utilizan frecuentemente para repetir la misma acción sobre cada el elemento de una colección, como aquí:

	    
	    data = [1, 2, 3]
	    data.each do |i|
	      print i, "\n"
	    end
	  
Produce:
	    1
	    2
	    3
	  

Al método each del array data se le pasa el bloque do ... end y se ejecuta repetidamente. En cada llamada al bloque se le pasan sucesivamente los elementos del array.

Se pueden definir los bloques con { .. } en vez de con do ... end.

	    
	    data = [1, 2, 3]
	    data.each { |i|
	      print i, "\n"
	    }
	  
Produce:
	    1
	    2
	    3
	  

Este código es semánticamente igual que el anterior. Sin embargo, en algunos casos, con situaciones de precedencia do .. end y { .. } actúan de forma diferente.

	    
	    foobar a, b do .. end     #foobar es el iterador
	    foobar a, b { .. }        #b es el iterador
	  
Esto es debido a que { .. } se asocia a la expresión precedente a diferencia del bloque do. El primer ejemplo es equivalente a foobar (a, b) do ... mientra que el segundo es equivalente a foobar(a, b { ... }).

P: ¿Cómo puedo pasar un bloque a un iterador?

R: Simplemente situando el bloque después de la llamada al iterador. También se puede pasar un objeto Proc precediendo la variable con & o con un nombre de constante que referencie a un objeto Proc.

P: ¿Cómo se utiliza un bloque en un iterador?

R: Existen tres formas de ejecutar un bloque dentro de un método iterador: (1) con la estructura de control yield; (2) utilizando la llamada call del argumento Proc; y (3) utiliando Proc.new seguido de call.

La sentencia yield llama a un bloque, opcionamente le pasa uno o más parámetros.

	    
	    def miIterador
	      yield 1, 2
	    end

	    miIterador { |a, b| puts a, b }
	  
Produce:
	    1
	    2
	  

Si la definición de un método tiene un argumento de tipo bloque (el último parámetro formal va precedido de un (&)), recibirá un bloque, convertido a un objeto Proc. Este puede llamarse utilizando method.call(args...).

	    
	    def miIterador (&b)
	      b.call(2,3)
	    end
	    miIterador{ |a,b| puts a, b }
	  
Produce:
	    2
	    3
	  

Proc.new (o las llamadas equivalentes proc o lambda) utilizadas dentro de la definición de un iterador, toman el bloque que se le pasa al método como argumento y generan un objeto procedimiento. (proc y lambda son realmente sinónimos).

	    
	    def miIterador
	      Proc.new.call(3,4)
	      proc.call(4,5)
	      lambda.call(5,6)
	    end
	    miIterador {|a,b| puts a, b }
	  
Produce:
	    3
	    4
	    4
	    5
	    5
	    6
	  

Sorprendentemente, Proc.new y equivalentes no consumen el bloque añadido al método - cada llamada a Proc.new genera un nuevo objeto procedimiento a partir del mismo bloque.

Se puede comprobar si existe un bloque asociado al método llamando a block_given?.

P: ¿Qué hace Proc.new sin un bloque?

R: Proc.new sin un bloque no puede generar un objeto procedimiento y da un error. Sin embargo, en la definición de un método si Proc.new no tiene un bloque supone que los tendrá en el momento en que se le llame y no da error.

P: ¿Cómo puedo ejecutar iteradores en paralelo?

R: La solución de Matz [ruby-talk:5252] utiliza hilos:

	    
	    require 'thread'

	    def combine(*args)
	      queues = []
	      threads = []
	      for it in args
	        queue = SizeQueue.new(1)
		th = Thread.start(it,queue) do |i,q|
		  self.send(it) do |x|
		    q.push x
		  end
		end
	        queues.push queue
	        threads.push th
              end  
	      loop do
	        ary = []
	        for q in queues
	          ary.push q.pop
	        end
	        yield ary
	        for th in threads
	          return unless th.status
	        end
	      end
	    end
	    public :combine

	    def it1 ()
	      yield 1; yield 2; yield 3
	    end

	    def it2 ()
	      yield 4; yield 5; yield 6
	    end
	    
	    combine('it1','it2') do |x|
	      # x es (1, 2), luego (2, 5), luego (3, 6)
	    end