Ruby Language
Bloques y Procs y Lambdas
Buscar..
Sintaxis
- Proc.nuevo ( bloque )
- lambda {| args | código}
- -> (arg1, arg2) {código}
- object.to_proc
- {| single_arg | código}
- do | arg, (clave, valor) | código final
Observaciones
Tenga cuidado con la prioridad del operador cuando tenga una línea con múltiples métodos encadenados, como:
str = "abcdefg"
puts str.gsub(/./) do |match|
rand(2).zero? ? match.upcase : match.downcase
end
En lugar de imprimir algo como abCDeFg
, como es de esperar, imprime algo como #<Enumerator:0x00000000af42b28>
- esto es porque do ... end
tiene menor precedencia que los métodos, lo que significa que gsub
solo ve el argumento /./
, y no el argumento del bloque. Devuelve un enumerador. El bloque termina pasado a las puts
, lo que lo ignora y solo muestra el resultado de gsub(/./)
.
Para solucionar este problema, envuelva la llamada gsub
entre paréntesis o use { ... }
lugar.
Proc
def call_the_block(&calling); calling.call; end
its_a = proc do |*args|
puts "It's a..." unless args.empty?
"beautiful day"
end
puts its_a #=> "beautiful day"
puts its_a.call #=> "beautiful day"
puts its_a[1, 2] #=> "It's a..." "beautiful day"
Hemos copiado el método call_the_block
del último ejemplo. Aquí, puede ver que un proceso se realiza llamando al método proc
con un bloque. También puede ver que los bloques, como los métodos, tienen retornos implícitos, lo que significa que procs (y lambdas) también lo tienen. En la definición de its_a
, puede ver que los bloques pueden tomar argumentos splat así como los normales; también son capaces de tomar argumentos predeterminados, pero no se me ocurre una forma de trabajar en eso. Por último, puede ver que es posible usar varias sintaxis para llamar a un método, ya sea el método de call
o el []
operador.
Lambdas
# lambda using the arrow syntax
hello_world = -> { 'Hello World!' }
hello_world[]
# 'Hello World!'
# lambda using the arrow syntax accepting 1 argument
hello_world = ->(name) { "Hello #{name}!" }
hello_world['Sven']
# "Hello Sven!"
the_thing = lambda do |magic, ohai, dere|
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
end
the_thing.call(1, 2, 3)
# magic! 1
# ohai 3
# 2 means hello
the_thing.call(1, 2)
# ArgumentError: wrong number of arguments (2 for 3)
the_thing[1, 2, 3, 4]
# ArgumentError: wrong number of arguments (4 for 3)
También puede usar ->
para crear y .()
Para llamar a lambda
the_thing = ->(magic, ohai, dere) {
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
}
the_thing.(1, 2, 3)
# => magic! 1
# => ohai 3
# => 2 means hello
Aquí puedes ver que un lambda es casi lo mismo que un proc. Sin embargo, hay varias advertencias:
La aridad de los argumentos de un lambda se hace cumplir; pasar el número incorrecto de argumentos a un lambda, generará un
ArgumentError
. Todavía pueden tener parámetros predeterminados, parámetros splat, etc.return
ing desde una lambda devuelve desde la lambda, mientrasreturn
ing desde una proc devuelve fuera del alcance adjunto:def try_proc x = Proc.new { return # Return from try_proc } x.call puts "After x.call" # this line is never reached end def try_lambda y = -> { return # return from y } y.call puts "After y.call" # this line is not skipped end try_proc # No output try_lambda # Outputs "After y.call"
Objetos como argumentos de bloque a métodos.
Poner un &
(ampersand) delante de un argumento lo pasará como el bloque del método. Los objetos se convertirán en un Proc
utilizando el método to_proc
.
class Greeter
def to_proc
Proc.new do |item|
puts "Hello, #{item}"
end
end
end
greet = Greeter.new
%w(world life).each(&greet)
Este es un patrón común en Ruby y muchas clases estándar lo proporcionan.
Por ejemplo, Symbol
s implementa to_proc
enviándose ellos mismos al argumento:
# Example implementation
class Symbol
def to_proc
Proc.new do |receiver|
receiver.send self
end
end
end
Esto habilita el lenguaje útil &:symbol
, comúnmente utilizado con objetos Enumerable
:
letter_counts = %w(just some words).map(&:length) # [4, 4, 5]
Bloques
Los bloques son fragmentos de código encerrados entre llaves {}
(generalmente para bloques de una sola línea) o do..end
(utilizados para bloques de varias líneas).
5.times { puts "Hello world" } # recommended style for single line blocks
5.times do
print "Hello "
puts "world"
end # recommended style for multi-line blocks
5.times {
print "hello "
puts "world" } # does not throw an error but is not recommended
Nota: los frenos tienen mayor prioridad que do..end
Flexible
Los bloques se pueden usar dentro de los métodos y funciones usando la palabra yield
:
def block_caller
puts "some code"
yield
puts "other code"
end
block_caller { puts "My own block" } # the block is passed as an argument to the method.
#some code
#My own block
#other code
Tenga cuidado, si se llama a yield
sin un bloque, se generará un LocalJumpError
. Para este propósito ruby proporciona otro método llamado block_given?
esto le permite verificar si se pasó un bloqueo antes de llamar a rendimiento
def block_caller
puts "some code"
if block_given?
yield
else
puts "default"
end
puts "other code"
end
block_caller
# some code
# default
# other code
block_caller { puts "not defaulted"}
# some code
# not defaulted
# other code
yield
puede ofrecer argumentos al bloque también.
def yield_n(n)
p = yield n if block_given?
p || n
end
yield_n(12) {|n| n + 7 }
#=> 19
yield_n(4)
#=> 4
Si bien este es un ejemplo simple, el yield
puede ser muy útil para permitir el acceso directo a las variables de la instancia o evaluaciones dentro del contexto de otro objeto. Por ejemplo:
class Application
def configuration
@configuration ||= Configuration.new
block_given? ? yield(@configuration) : @configuration
end
end
class Configuration; end
app = Application.new
app.configuration do |config|
puts config.class.name
end
# Configuration
#=> nil
app.configuration
#=> #<Configuration:0x2bf1d30>
Como puede ver, usar el yield
de esta manera hace que el código sea más legible que llamar continuamente a app.configuration.#method_name
. En su lugar, puede realizar toda la configuración dentro del bloque manteniendo el código contenido.
Variables
Las variables para bloques son locales al bloque (similares a las variables de funciones), mueren cuando se ejecuta el bloque.
my_variable = 8
3.times do |x|
my_variable = x
puts my_variable
end
puts my_variable
#=> 0
# 1
# 2
# 8
Los bloques no se pueden salvar, mueren una vez ejecutados. Para guardar bloques necesitas usar procs
y lambdas
.
Convertir a Proc
Los objetos que responden a to_proc
se pueden convertir a procs con el operador &
(lo que también permitirá que se pasen como bloques).
El símbolo de la clase define #to_proc
por lo que intenta llamar al método correspondiente en el objeto que recibe como parámetro.
p [ 'rabbit', 'grass' ].map( &:upcase ) # => ["RABBIT", "GRASS"]
Los objetos de método también definen #to_proc
.
output = method( :p )
[ 'rabbit', 'grass' ].map( &output ) # => "rabbit\ngrass"
Aplicación parcial y curry
Técnicamente, Ruby no tiene funciones, sino métodos. Sin embargo, un método de Ruby se comporta de manera casi idéntica a las funciones en otro idioma:
def double(n)
n * 2
end
Este método / función normal toma un parámetro n
, lo duplica y devuelve el valor. Ahora definamos una función de orden superior (o método):
def triple(n)
lambda {3 * n}
end
En lugar de devolver un número, el triple
devuelve un método. Puedes probarlo usando el Ruby Interactive Shell :
$ irb --simple-prompt
>> def double(n)
>> n * 2
>> end
=> :double
>> def triple(n)
>> lambda {3 * n}
>> end
=> :triple
>> double(2)
=> 4
>> triple(2)
=> #<Proc:0x007fd07f07bdc0@(irb):7 (lambda)>
Si realmente desea obtener el número triplicado, debe llamar (o "reducir") la lambda:
triple_two = triple(2)
triple_two.call # => 6
O más concisamente:
triple(2).call
Aplicaciones al curry y parciales.
Esto no es útil en términos de definir una funcionalidad muy básica, pero es útil si desea tener métodos / funciones que no sean llamados o reducidos instantáneamente. Por ejemplo, supongamos que desea definir métodos que agreguen un número por un número específico (por ejemplo, add_one(2) = 3
). Si tuvieras que definir una tonelada de estos, podrías hacer:
def add_one(n)
n + 1
end
def add_two(n)
n + 2
end
Sin embargo, también podría hacer esto:
add = -> (a, b) { a + b }
add_one = add.curry.(1)
add_two = add.curry.(2)
Usando el cálculo lambda podemos decir que add
es (λa.(λb.(a+b)))
. Currying es una forma de aplicar parcialmente add
. Entonces add.curry.(1)
, es (λa.(λb.(a+b)))(1)
que puede reducirse a (λb.(1+b))
. La aplicación parcial significa que pasamos un argumento para add
pero dejamos el otro argumento para proporcionarlo más adelante. La salida es un método especializado.
Ejemplos más útiles de curry
Digamos que tenemos una fórmula general realmente grande, que si le especificamos ciertos argumentos, podemos obtener fórmulas específicas de ella. Considera esta fórmula:
f(x, y, z) = sin(x\*y)*sin(y\*z)*sin(z\*x)
Esta fórmula está hecha para trabajar en tres dimensiones, pero digamos que solo queremos esta fórmula con respecto a y y z. Digamos también que para ignorar x, queremos establecer su valor en pi / 2. Primero hagamos la fórmula general:
f = ->(x, y, z) {Math.sin(x*y) * Math.sin(y*z) * Math.sin(z*x)}
Ahora, usemos el curry para obtener nuestra fórmula yz
:
f_yz = f.curry.(Math::PI/2)
Luego para llamar a la lambda almacenada en f_yz
:
f_xy.call(some_value_x, some_value_y)
Esto es bastante simple, pero digamos que queremos obtener la fórmula para xz
. ¿Cómo podemos configurar y
en Math::PI/2
si no es el último argumento? Bueno, es un poco más complicado:
f_xz = -> (x,z) {f.curry.(x, Math::PI/2, z)}
En este caso, debemos proporcionar marcadores de posición para el parámetro que no estamos rellenando previamente. Por coherencia podríamos escribir f_xy
así:
f_xy = -> (x,y) {f.curry.(x, y, Math::PI/2)}
Así es como funciona el cálculo lambda para f_yz
:
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_yz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (π/2) # Reduce =>
f_yz = (λy.(λz.(sin((π/2)*y) * sin(y*z) * sin(z*(π/2))))
Ahora veamos f_xz
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_xz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (λt.t) (π/2) # Reduce =>
f_xz = (λt.(λz.(sin(t*(π/2)) * sin((π/2)*z) * sin(z*t))))
Para más información sobre el cálculo lambda intente esto .