Ruby es un lenguaje de programación sumamente expresivo, lo que significa que resulta muy fácil de leer y comprender. Esto lo podemos encontrar en la forma de escribir las clases, de invocar los métodos y en los nombres de las funcionalidades que incorpora el lenguaje.
Dentro de todas las características que posee, el manejo de colecciones es sin duda una de las mejores. Llamamos colección a estructuras que almacena un conjunto de datos, tales como arreglos o listas. Dentro de las muchas bondades del lenguaje, Ruby nos permite iterar colecciones para procesar su información. En este artículo vamos a explorar varios métodos de iteración que nos permiten consultar y trabajar con ellas.
Antes de analizar cada función de iteración, es importante entender el concepto de bloque. Un bloque es una función anónima que puede ser pasada a un método. Un bloque tiene dos partes: los argumentos, que se especifican entre pipes (|
), y la lógica, que conforma el cuerpo del mismo. En función de si la lógica del bloque ocupa una o varias líneas, se utilizan diferentes formas para definirlos:
array.each { |element| puts element }
array.each do |element|
if element % 2
puts "#{element} is even"
else
puts "#{element} is odd"
end
end
.each
El método más utilizado seguramente sea .each
, el cual permite iterar sobre una colección y ejecutar por cada elemento el bloque de código que recibe por parámetro.
array.each { |element| puts element}
El método retorna la misma referencia de la colección por la que se está iterando. En el ejemplo, se itera por cada elemento de una colección y se lo imprime.
.map
Otro de los métodos más utilizados es .map
, cuya iteración retorna una nueva colección donde cada elemento es el resultado de la ejecución del bloque. Al igual que con .each
, dicho bloque se ejecuta por cada elemento de la colección.
Veamos un ejemplo en donde, dada una colección de números, recibimos otra donde cada valor es el doble que el de la colección original.
new_array = array.map { |element| element * 2 }
Existe una variante llamada .map!
, en la cual la colección resultante reemplaza en memoria a la original. De esta forma, en lugar de generarse otra colección, se modifica la existente.
Esto último es conocido como Bang (!) Method. La diferencia con respecto a su versión sin el signo de exclamación es que modifica el objeto in-line. Por convención, se los nombra con un signo de exclamación para denotar que son peligrosos en comparación con su versión “no bang”. Podemos utilizar esta misma práctica en los métodos de nuestras clases.
Podemos probar por consola un ejemplo comparando el resultado del uso de map
y de map!
, y analizando cómo se modifican los arreglos los originales.
> numbers = [1, 2, 3]
> result = numbers.map { |element| element + 1 }
> result
[2, 3, 4]
> numbers
[1, 2, 3]
> numbers.map! { |element| element + 1 }
> numbers
[2, 3, 4]
.select
El método .select
sirve para filtrar una colección en función del resultado de la evaluación del bloque. El bloque se ejecuta por cada elemento, y se queda con aquellos cuyo retorno del bloque es un valor truthy (es decir, diferente a nil
y false
).
En el siguiente ejemplo, nos quedamos con aquellos valores pares de una colección.
new_array = array.select { |element| element.even? }
Existe también la versión bang del método: .select!
.
Una de las características adicionales que tienen esta clase de métodos, como .map
, es que pueden escribirse de forma simplificada siempre y cuando se ejecute un método del elemento por el que se itera. En el ejemplo, podría reemplazarse por la siguiente notación:
new_array = array.select(&:even?)
Si lo que necesitamos es seleccionar valores que no cumplan con una condición, podemos optar por el método
.reject
(o.reject!
), en donde la condición del bloque determina aquellas elementos que no van a formar parte de la colección resultante.
.find
El método .find
es similar a .select
, pero se queda con el primer elemento en cumplir con la condición que indique el bloque. Una vez que encuentra un valor, corta la ejecución y retorna a dicho valor.
Un ejemplo podría ser localizar el primer valor par de una colección:
item = array.find { |element| element.event? }
item = array.find(&:even?)
.all?, .any?, .none? y .one?
Si lo que necesitamos es verificar condiciones a cumplirse entre toda la colección, tenemos diferentes iteradores que retornan valores booleanos en función de la cantidad de elementos de la colección que cumplan con la condición del bloque.
- .all?: retorna
true
si todos los elementos cumplen con la condición. - .any?: retorna
true
si al menos un elemento cumple con la condición. - .none?: retorna
true
si ningún elemento cumple con la condición. - .one?: retorna
true
si únicamente un elemento cumple con la condición.
boolean = array.any? { |element| element > 10 }
Por convención, los métodos en Ruby de consulta que retornan un valor booleano tienen un signo de pregunta al final de su nombre. No aplica para aquellos métodos que ejecutan algún cambio y, en función del resultado, retornan un booleano, sino simplemente a aquellos que únicamente consultan información.
.each_with_object
Ruby tiene iteradores de todo tipo, y uno de los más flexibles y potentes es .each_with_object
. Funciona de forma similar a .each
, pero además inyecta un objeto a la iteración. Esto permite modificar dicho objeto en cada bucle, y el valor de retorno del método es el objeto final.
Supongamos que queremos armar un hash con las claves “even” y “odd”, y agregarle a cada una aquellos valores que aparecen en el arreglo en función de si son pares o impares.
object = array.each_with_object({even: [], odd: []}) do |element, hash|
key = element.odd? ? :odd : :even
hash[key] << element
end
Quizás parezca un poco complejo al inicio, pero lo que hace el método es simplemente iterar por cada elemento del arreglo, y en función de si es par o impar, lo agrega a la estructura de datos que se inyecta al iterador (el cual es este hash: { even: [], odd: [] }
). De esta forma, el resultado es un hash con dos claves, y cada una de ellas tiene como valor un arreglo de números. Es decir, suponiendo que la entrada sea el arreglo [1, 2, 3, 4, 5, 7, 10]
, el resultado será el siguiente:
{
even: [2, 4, 10],
odd: [1, 3, 5, 7],
}
Podríamos asignar la salida el resultado de dicho método a una variable para luego trabajar con nuestro hash resultante.
Estos son algunos de los los iteradores de colecciones en Ruby más populares. Pero hay muchos más, y más detalle sobre los que describimos acá. Si querés conocer más podés visitar la documentación, o bien dejarnos alguna consult en la caja de comentarios.
Ruby es un lenguaje de programación realmente increíble, y tal como su creador lo ideó, busca ser lo más expresivo posible. Al fin y al cabo, la computadora lo ejecuta, pero somos nosotros quienes lo tenemos que entender, mantener y mejorar.