Algunos temas jugosos sobre métodos de Ruby – Proc, Blocks y Lambdas


HamburgerSegún vas profundizando y fijándote en los detalles, Ruby (Mi guía de Ruby – Las clases y curiosidades OOP) es uno de esos lenguajes que te va capturando hasta el punto no dejan de sorprenderte cada día. En alguna ocasión se ha discutido sobre el cómo funciona cada uno de ellos, pero creo que merece la pena hacer una presentación. Vamos a echarle un vistazo:

Los bloques es una de esas cosas que tiene Ruby y que es muy potente, pero además es que es muy necesario tener muy claro cómo funciona. Veamos un ejemplo de bloque, vamos a pensar en ¿cómo podríamos poner código Ruby en un objeto para que pueda pasar  y llamarlo después?, la respuesta es un bloque.

¿cómo definimos un bloque?

Primero antes de nada, hay que saber que un bloque no es un objeto, veamos un ejemplo que es la mejor forma de ver su sintaxis. Lo podemos hacer de dos formas:

 > [1,2,3].each { |x| puts x*2 }
2
4
6
=> [1, 2, 3]

> [1,2,3].each do |x|
>         puts x*2  # block is everything between the do and end
>  end
2
4
6
=> [1, 2, 3]

Pero cuidado podemos estar tentados de hacer  lo siguiente, para más tarde utilizarlo:

myBlock = { |x| puts x }

Esto así no es válido, pero ¿un bloque no se define dentro de llaves {…….} o dentro un do…. end? si. entonces ¿por qué no sería correcto lo que hemos definido? la respuesta es. que no dispone de parámetros en la definición del bloque, por lo que tendríamos que definirlo así:

myBlock = lambda { |x| puts x }

para llamarlo:

myBlock.call “Hello My Block!”

La verdad es que cuando lo descubrí y ver la posibilidad de asignar un trozo de código Ruby a una variable, pensé en lo poderoso que esto podría ser, por tanto llegados a este punto, podemos comenzar a dejar libre nuestra imaginación y definirnos nuestro propio método:

def my_lambda(&myBlock)

myBlock

end

b = my_lambda { puts “Hello Block My Way!” } estamos construyendo nuestro bloque para pasarlo como parámetro al método

b.call  y como resultado al llamar al método, obtenemos un Hello Block My Way!

Ahora veamos el siguiente codigo Proc.new, proc y lambda:

myBlock = lambda { |x| puts x }

myBlock = proc { |x| puts x }

myBlock = Proc.new { |x| puts x }

Pero ¿no es todo lo mismo?, o me lo parece a mi. En cierta forma y visto así, si, entonces ¿cuál es la diferencia en hacerlo de las tres formas? vamos a despejar la duda. Si definimos un bloque como lambda, su comportamiento es como un método y el número de argumentos debe ser el mismo que recibe, es decir, si lo llamamos con el mismo número de argumentos que hemos definido, no tenemos error y podemos llamar al bloque, pero por el contrario, llamamos con más argumentos de los definidos, entonces obtenemos un error como en los siguientes casos

 > add_lambda = lambda { |x,y| x + y } ——> Definimos nuestro bloque con lambda

> add_lambda.call(4)
ArgumentError: wrong number of arguments (1 for 2)

> add_lambda.call(4,5) —–> Llamada correcta al bloque con el mismo número de parámetros en la definición del bloque
=> 9

> add_lambda.call(4,5,6)
ArgumentError: wrong number of arguments (3 for 2)

Por el contrario, si definimos un bloque como Proc.new, actúa como el bloque de código anónimo se pasa a un método como Enumerable#each:

 > add_procnew = Proc.new { |x,y| x + y }  ——> Definimos nuestro bloque con Proc.new

> add_procnew.call(4)  ——> Le pasamos un único parámetro
TypeError: nil can’t be coerced into Fixnum

> add_procnew.call(4,5,6)  ——> Le pasamos tres parámetros a pesar de tener definido 2 y no tenemos error
=> 9

Corrección a lo que puse inicialmente, gracias a Diego Rodríguez (@diec123) :

En un principio puse que para myBlock = proc { |x| puts x } el comportamiento es igual que lambda y no es correcto, el comportamiento es como un Proc.new, veamos:

> proc_prod = proc { |x, y| x * y }

> proc_prod.call(2, 3, 4)
=> 6
> proc_prod = Proc.new { |x, y| x * y }

> proc_prod.call(2, 3, 4)
=> 6
> lambda_prod = lambda { |x, y| x * y }

> lambda_prod.call(2, 3,4)
ArgumentError: wrong number of arguments (3 for 2)

Podemos echar un vistazo al codigo Ruby fuente de proc: https://github.com/ruby/ruby/blob/8bf9fe27645b3bf58c3b6213dd517cfe28733808/proc.c

Dicho esto,  veamos ahora algunas curiosidades más al respecto:

def comeback

mylambda = lambda { return “Carlos” }
mylambda.call

return “Carmen”

end

comeback
=> “Carmen” ————–> Valor devuelto

Pero si ahora definimos el método con Proc.new o proc, el resultado es bien distinto:

def comeback

mylambda = Proc.new { return “Carlos” }
mylambda.call

return “Carmen”

end

comeback
=> “Carlos” ————–> Valor devuelto

La definición del método con lambda y Proc.new con un return, en el caso de lambda continua y con Proc.new no continua la secuencia de código.

Escribiendo un método que acepta bloques

Esta es una de las cosas que me gusta de los bloques, la palabra reservada yield, veamos en que consiste:

def call_my_block

puts “I’m about to call my block. First time”
yield
puts “I’m about to call my block one more time.”
yield

end

> call_my_block { puts “Hi folks, I’m a talking my code block.” } —-> esta parte es la que dentro del método se captura como yield

I’m about to call my block. First time
Hi folks, I’m a talking my code block. —> YIELD
I’m about to call my block one more time.
Hi folks, I’m a talking my code block. —> YIELD

También es algo común encontrase los yield en los templates de Rails, por ejemplo en un layout de  application.html.erb:

………….</head>
<body>

<%= render “shared/menu” %>
<%= welcome_user %>

<section id=”content”>
    <%= yield %>
</section>

</body>
</html>

Para un mayor detalle puedes ver el post de Abriendo una Web App Ruby On Rails- Welcome aboard 2ª Parte, en dónde ves el sentido de yield en estos casos. También podríamos definir el siguiente método, controlando si disponemos de un bloque cuando se llame al método o no y dar un error cuando no se cumpla la condición:

def repeat(n)

if block_given?

n.times { yield }

else

raise ArgumentError.new(“I can’t repeat a block you don’t give me!”)

end

end

 > repeat(4) { puts “Hello.” }
Hello.
Hello.
Hello.
Hello.
=> 4
> repeat(4)————–> En este caso nos falta el bloque y por esa razón nos devuelve un error, que ya hemos controlado en nuestro método.
ArgumentError: I can’t repeat a block you don’t give me!

Vemos algún otro ejemplo más, que me parece atractivo de mostrar:

def call_twice

puts “Calling your block.”
ret1 = yield(“very first“)
puts “The value of your block: #{ret1}
puts “Calling your block again.”
ret2 = yield(“second“)
puts “The value of your block: #{ret2}

end

> call_twice do |which_time| ——> Definimos un bloque con yield dentro de la llamada al método call_twice
>          puts “I’m a code block, called for the #{which_time} time.”
>         which_time == “very first” ? 1 : 2
> end

RESULTADO:

Calling your block.
I’m a code block, called for the very first time. —> en esta parte tenemos lo interesante, primero llegamos al yield y cambia #{which_time} por  yield(“very first”)
The value of your block: 1 —–> cuando llegamos a esta sección, comprueba que tenemos y en  which_time == “very first” ? 1 : 2 nos dice 1 o 2 y lo cambia en  #{ret1}
Calling your block again.
I’m a code block, called for the second time.
The value of your block: 2

Hay una curiosidad muy interesante, pero esto ya es cuestión de gustos y la connotación de lamba en la versión de 1.9 de Ruby, fíjate en el siguiente código:

 > ->(x,y){puts x}
 > lambda{|x,y| puts x,y}

Ambas son lo mismo, pero hay preferencias de utilización y de claridad, la primera quizá sea mucho más clara (si conoces CoffeeScript puede ser que sea más interesante) y te da una visión de lo que quiere hacer, esto ya te lo dejo a gusto del desarrollador.

También he preguntado a Xavi Noria (@fxn), si es mejor utilizar Proc.new o proc, su respuesta es de lo más interesante, me dijo, hoy en día la flecha. Me puse a investigar y en la versión 1.9 de Ruby se pueden hacer cosas como las siguientes:

f =->n {[:hello, n]}

podemos ejecutarlas de estas cuatro formas diferentes:

> f[:ruby] ——–> cuando ejecutemos en consola obtenemos como resultado  => [:hello, :ruby]

> f.call(:ruby) ——–> cuando ejecutemos en consola obtenemos como resultado  => [:hello, :ruby]

> f.(:ruby) ——–> cuando ejecutemos en consola obtenemos como resultado  => [:hello, :ruby]

> f === :ruby ——–> cuando ejecutemos en consola obtenemos como resultado => [:hello, :ruby]

También permite parámetros por defecto

f =-> a,b=1,*c { p [a,b,c] }

> f.call(1) ——–> cuando ejecutemos en consola obtenemos como resultado => [1, 1, []]

> f.(1,2) ——–> cuando ejecutemos en consola obtenemos como resultado => [1, 2, []]

> f[1,2,3] ——–> cuando ejecutemos en consola obtenemos como resultado => [1, 2, [3]]

Y ¿cómo podríamos hacer un scope con esta forma? 

scope :by_org, ->(org_id){ where(org_id: org_id.to_i) }
scope :is_public, -> { where(‘is_public = true’) }

en mi opinión “la verdad es que es mucho más limpia y clara”

Como resumen final podemos decir que:

  • Los proc y los lambdas son objetos y lo bloques no lo son, es sintaxis
  • Los proc y los lambdas son invocados mediante llamadas call y los bloques lo hacen mediante la palabra reservada yield
  • Los proc y lambda difieren en cómo se manejan las sentencias de control
  • El control de los parámetros es más estricto en lambda que deben ser los definidos, en proc, pueden ser mayor de los definidos.

Seguiremos hablando de Ruby en próximos post.

Un comentario en “Algunos temas jugosos sobre métodos de Ruby – Proc, Blocks y Lambdas

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