Trabajando con Enumerables: each_with_object


ruby En esta ocasión quería trabajar sobre ejemplos de algunos Enumerables interesantes y hacer un Benchmark con ejemplos.

Enumerable: Each_with_object comparado con otros enumerables que normalmente utilizo, reduce y un inject

A veces tenemos que  construir un nuevo objeto de la colección partiendo de los elementos de otra colección. Tenemos la solución en  each_with_object.

 

Vamos a resolver el siguiente problema. Imagina que tenemos un array inicial compuesto por una serie de números, en este caso un array del 1 al 10  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] y necesitamos obtener una hash sumando un valor  en cada uno de los valores de dicha hash. Lo normal es que arranquemos utilizando each o un for, dependiendo de los conocimientos de otros lenguajes y qué lenguajes sepas, pero sin meternos en mucho más, arrancamos con un each. Nos creamos el resultado partiendo de un valor 5 como inicio, que será nuestra hash:

result = Hash.new(5)

Ahora podemos iterar sobre nuestro inicial array

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

nums.each { |n| puts N(#{n});result[n] += 1; puts Result #{result}}

y el resultado que tenemos es:

N(1)  Result {1=>6}  / N(2) Result {1=>6, 2=>6} / N(3) Result {1=>6, 2=>6, 3=>6} …………..N(10) Result {1=>6, 2=>6, 3=>6, 4=>6, 5=>6, 6=>6, 7=>6, 8=>6, 9=>6, 10=>6}

 Ahora podríamos hacerlo con reduce:

nums.reduce(Hash.new(5)) { |a, e| a[e] += 1; a }

el resultado de la hash es:  {1=>6, 2=>6, 3=>6, 4=>6, 5=>6, 6=>6, 7=>6, 8=>6, 9=>6, 10=>6}

En este caso es un poco, en mi opinión, con algo más de añadir complejidad al resultado necesitando devolver el array a y podríamos resolverlo sin esta necesidad utilizando en este caso lo mismo pero con each_with_object:

nums.each_with_object(Hash.new(5)) { |e, a| a[e] += 1 }

Como puedes ver invocamos el bloque para cada elemento con un argumento objeto arbitrario, y devuelve el objeto, eliminando así la necesidad de devolverlo a nosotros mismos como resultado, como fue el caso anterior.

Finalmente voy a hacer un Benchmark de los tres y verificar los resultados de velocidad sobre 10millones de acciones en el bucle utilizando una hash en los tres casos con inject, each_with_object y con inject:

Benchmark.measure { (1..10_000_000).each_with_object({})  { |i, hash| i + 1 } }

=>   1.550000   0.020000   1.570000 (  1.398000)

> Benchmark.measure { (1..10_000_000).inject({})  { |hash, i| i + 1; hash } }

=>   1.790000   0.020000   1.810000 (  1.633000)

> Benchmark.measure { (1..10_000_000).reduce({})  { |hash, i| i + 1; hash } }

=>   1.690000   0.010000   1.700000 (  1.655000)

En este caso es más rápido each_with_object frente a reduce y finamente inject.  Aumentando el numero en 100 millones y la diferencia es importante también:

> Benchmark.measure { (1..100_000_000).reduce({})  { |hash, i| i + 1; hash } }

=>  16.200000   0.090000  16.290000 ( 15.992000)

> Benchmark.measure { (1..100_000_000).each_with_object({})  { |i, hash| i + 1 } }

=>  12.930000   0.050000  12.980000 ( 12.711000)

> Benchmark.measure { (1..100_000_000).inject({})  { |hash, i| i + 1; hash } }

=>  16.560000   0.100000  16.660000 ( 16.470000)

Algunas cosas a considerar

Finalmente me gustaría comentar lo que puedes y no puedes hacer

Objetos inmutables Integer

Por ejemplo, si necesitas sumar cada uno de los elementos y partes de un origen 0, con each_with_object no lo puedes hacer:

> (1..10).each_with_object(0) {|i,sum| sum += i}

=> 0

ya que each_with_object itera sobre una colección, pasando cada elemento y el objeto al bloque, ya que no se actualiza el valor del objeto después de cada iteración,  devuelve el objeto origen. Para resolverlo tendríamos el inject:

> (1..10).inject(0) {|sum,i| sum += i}

=> 55

La misma solución con each_with_object tendríamos que hacer lo esta forma:

>(1..10).each_with_object({:sum => 0}) {|i,hash| hash[:sum] += i}

=> {:sum=>55}

Objetos mutables Strings

Para los casos en los que necesitemos actuar con Strings y componer un string a partir de elementos únicos hay diferencia en hacerlo:

>("a".."f").each_with_object("") {|i,str| str +=}

En este caso no funciona como esperamos, obtenemos un string vacío ya que devuelve un nuevo objeto y el objeto original se mantiene igual. Pero si hacemos:

>("a".."f").each_with_object("") {|i,str| str << i}

 obtenemos lo esperado, un string con todos lo elementos  “abcdef” ya que str << "a" modifica el objeto original.

Si quieres dejar cualquier comentario que podamos añadir al post, encantado!!

2 comentarios en “Trabajando con Enumerables: each_with_object

  1. Ricardo García Vega (@bigardone) dijo:

    Hola Carlos! Cuanto tiempo!😀
    Muy buen post. La verdad es que he usado each_with_object más de una vez pero nunca se me había ocurrido hacer el benchmark para compararlo con reduce o inject. Me da la sensación que each_with_object no es tan conocido o usado como reduce e inject que suelen usarse más, pero visto lo visto seguiré usándolo🙂
    Un abrazo!

    • carlossanchezperez dijo:

      Muy buenas Ricardo🙂 Pues normalmente se utiliza reduce o inject, el tema era que me llamó la atención cuando me lo comentó @otikik y me puse a ver en qué situaciones podría ser útil y hacer un benchmark para observar el comportamiento. Muchas gracias por tus comentarios, como siempre un placer!!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s