Iteradores de Ruby y la palabra clave yield (con ejemplos)

Iteradores de Ruby y la palabra clave yield

Los iteradores de Ruby son un tipo especial de método soportado por colecciones. Son como cualquier otro método regular, pero reciben una entrada adicional en la forma de un bloque de código. Los iteradores son una de las características más útiles del lenguaje Ruby, y usarlos efectivamente es una gran forma de crear métodos y clases limpios.

En este artículo, exploraremos los conceptos fundamentales detrás de los métodos iteradores. Aprenderemos cómo funciona la palabra clave yield y escribiremos nuestras propias réplicas de iteradores más comúnmente usados.

En código de producción, rara vez (si acaso) crearás tus propias implementaciones de estos iteradores comunes. Aún así, construir tus propias versiones es una gran forma de aprender este tema en profundidad, y te preparará para crear tus propios iteradores si surge la necesidad.

Iterando a través de bloques de código

Los iteradores son métodos regulares que reciben entrada extra como parte de su sintaxis de llamada. Esta entrada extra es un bloque de código: una serie de declaraciones válidas regulares de Ruby que el iterador puede llamar una o más veces. Para entender esto, veamos un ejemplo de uno de los iteradores más simples, times.

Times es un método en la clase Integer que ejecutará un bloque de código n veces, donde n es el entero sobre el que se llama. El siguiente código imprimirá un mensaje 10 veces.

10.times { puts "Este mensaje aparecerá 10 veces, ¡sí!"}

En este caso, el código entre las llaves (la declaración puts) es el bloque de código. Como se esperaba, ejecutar este código imprime el siguiente resultado en la terminal:

Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!
Este mensaje aparecerá 10 veces, ¡sí!

Las llaves no son la única forma de pasar un bloque de código, también puedes usar do/end. El siguiente fragmento es equivalente al que escribimos arriba, y produce los mismos resultados:

10.times do
    puts "Este mensaje aparecerá 10 veces, ¡sí!"
end

Algunos iteradores también pueden pasar datos a un bloque de código. Solo necesitas especificar el argumento en el bloque de código usando símbolos ‘||’. Por ejemplo, el iterador each pasará al bloque de código cada elemento en el array sobre el que se llama:

test_array = [1, 10, 100, 1000, 10000]

test_array.each do |element|
    puts "Genial, el iterador me dio este valor: #{element}"
end

Si ejecutas este código, imprimirá este mensaje:

Genial, el iterador me dio este valor: 1
Genial, el iterador me dio este valor: 10
Genial, el iterador me dio este valor: 100
Genial, el iterador me dio este valor: 1000
Genial, el iterador me dio este valor: 10000

Ahora que estamos familiarizados con los iteradores, construiremos nuestras propias versiones usando el operador yield.

Escribiendo tus propios iteradores

Escribir tus propios iteradores no es una tarea difícil, todo lo que necesitas hacer es pasar bloques de código y luego mover la ejecución al bloque de código. En Ruby, esto se puede lograr usando el operador yield. Para entender esto mejor, echa un vistazo al siguiente ejemplo:

 def metodo_yield_mas_simple
    puts "Estoy en el método yield más simple, después de esto, la ejecución se moverá al bloque de código"
    yield
    puts "Estoy de vuelta en el método yield más simple"
 end


metodo_yield_mas_simple do
    puts "Ejecutando actualmente el bloque de código"
end

En nuestro ejemplo, yield moverá la ejecución a lo que sea que hayas pasado como bloque de código a metodo_yield_mas_simple. Cuando el bloque de código termine de ejecutarse, la ejecución regresará y ejecutará las declaraciones después de la llamada yield. Como se esperaba, si ejecutas este código obtendrás este resultado:

Estoy en el método yield más simple, después de esto, la ejecución se moverá al bloque de código
Ejecutando actualmente el bloque de código
Estoy de vuelta en el método yield más simple

Ejecutando un bloque de código infinitas veces

Probemos un ejemplo más interesante: un método llamado loop_infinito que ejecuta un bloque de código un número infinito de veces. La implementación es bastante directa:

def loop_infinito
    while true
        yield
    end
end

loop_infinito do
    puts "Este mensaje aparecerá infinitas veces"
end

Ejecuta este código y verás el ciclo while cediendo control al bloque de código una y otra vez. También podemos crear una variación de este método que solo se ejecuta N veces. Para eso, podemos usar monkey-patching para agregar un método llamado ejecutar_veces a la clase Integer:

class Integer
    def ejecutar_veces
        counter = 0
        until counter == self
            yield
            counter += 1
        end
    end
end

5.ejecutar_veces do
    puts "Este mensaje aparecerá exactamente 5 veces"
end

Ejecutar este código resulta en:

Este mensaje aparecerá exactamente 5 veces
Este mensaje aparecerá exactamente 5 veces
Este mensaje aparecerá exactamente 5 veces
Este mensaje aparecerá exactamente 5 veces
Este mensaje aparecerá exactamente 5 veces

Acabamos de aprender cómo mover la ejecución al bloque de código usando la palabra clave yield. Esto es por sí mismo bastante poderoso, pero sería muy limitado si mover datos entre el iterador y el bloque de código no fuera posible. Aprenderemos cómo hacer esto implementando un par de demos.

Moviendo datos hacia y desde un bloque de código

Puedes pasar información a un bloque de código pasándola como argumentos para yield. Puedes recibirla en el bloque de código especificándola cuando escribes el bloque. De nuevo, esto es mucho más fácil de entender con un ejemplo. Creemos nuestra propia versión del método each de Array.

class Array
    def mi_each
        counter = 0
        until counter == self.size
            # Pasaremos al bloque de código cada elemento del array, uno por uno
            yield self[counter]
            counter += 1
        end
        self # Igual que el método each normal, devolvemos self al final
    end
end

test_array = [1, 10, 100, 1000, 10000, 100000]

# Así es como especificamos los argumentos pasados al bloque de código
# En este caso, decidimos llamarlo 'element', pero los nombres dependen de ti
test_array.mi_each do |element|
    puts "El array de prueba contiene el número #{element}"
end

En este caso, elegimos element como el nombre del argumento para el bloque, pero puedes nombrarlo como quieras.

También es posible devolver datos desde un bloque de código y usarlo en el iterador. En este caso, el valor de retorno del bloque de código puede verse como el valor de retorno de llamar la función yield. De nuevo, esto es más fácil de entender con un ejemplo.

Re-implementemos la función Array.map. Map devuelve un array donde cada entrada es el resultado de aplicar la función del bloque al elemento respectivo del array original. Nuestra implementación sería:

class Array
    def mi_mapeador
        mapped_array = []
        counter = 0

        until counter == self.size
            mapped_value = yield self[counter]
            mapped_array.push(mapped_value)
            counter += 1
        end
        # queremos devolver el array mapeado al final
        mapped_array
    end
end

test_array = [1,2,3,4,5,6,7,8,9]

# Generemos un array con los cuadrados de test_array

squares =   test_array.mi_mapeador do |element|
                element * element
            end

puts squares

Presta atención a la línea donde usamos yield. El valor de retorno de yield se pone en la variable mapped_value y se empuja a un array. En este ejemplo, este nuevo array se llenará con los cuadrados de cada elemento en el array original.

Ejecutar este código imprime el siguiente resultado:

1
4
9
16
25
36
49
64
81

Los iteradores son útiles

Ahora sabes cómo escribir tus propios iteradores.

Como mencionamos antes, puedes lograr la mayoría de tareas de programación cotidianas con una combinación de los iteradores incorporados. Aún así, saber cómo aprovechar el poder de yield puede llevar a soluciones elegantes y métodos limpios.

Si aún quieres profundizar un poco más en el tema, trata de implementar algunos de los otros iteradores como select, take_while e inject. He encontrado que escribir tus propias copias de funciones iteradoras comunes es la forma más fácil de dominar este tema.

Qué hacer a continuación:

Juan Luis Orozco Villalobos

¡Hola! Soy Juan, ingeniero de software y consultor en Budapest. Me especializo en computación en la nube e IA, y me encanta ayudar a otros a aprender sobre tecnología e ingeniería