Bloques y rendimientos en rubí

275

Estoy tratando de entender los bloques yieldy cómo funcionan en Ruby.

¿Cómo se yieldusa? Muchas de las aplicaciones de Rails que he visto usan yieldde una manera extraña.

¿Puede alguien explicarme o mostrarme a dónde ir para entenderlos?

Matt Elhotiby
fuente
2
Quizás le interese la respuesta a la función de rendimiento de Ruby en relación con la informática . Aunque es una pregunta algo diferente a la suya, puede arrojar algo de luz sobre el asunto.
Ken Bloom

Respuestas:

393

Sí, es un poco desconcertante al principio.

En Ruby, los métodos pueden recibir un bloque de código para realizar segmentos arbitrarios de código.

Cuando un método espera un bloque, lo invoca llamando a la yieldfunción.

Esto es muy útil, por ejemplo, para iterar sobre una lista o para proporcionar un algoritmo personalizado.

Tome el siguiente ejemplo:

Definiré una Personclase inicializada con un nombre y proporcionaré un do_with_namemétodo que, cuando se invoque, simplemente pasaría el nameatributo al bloque recibido.

class Person 
    def initialize( name ) 
         @name = name
    end

    def do_with_name 
        yield( @name ) 
    end
end

Esto nos permitiría llamar a ese método y pasar un bloque de código arbitrario.

Por ejemplo, para imprimir el nombre haríamos:

person = Person.new("Oscar")

#invoking the method passing a block
person.do_with_name do |name|
    puts "Hey, his name is #{name}"
end

Imprimiría:

Hey, his name is Oscar

Tenga en cuenta que el bloque recibe, como parámetro, una variable llamada name(NB puede llamar a esta variable lo que quiera, pero tiene sentido llamarla name). Cuando el código lo invoca yield, llena este parámetro con el valor de @name.

yield( @name )

Podríamos proporcionar otro bloque para realizar una acción diferente. Por ejemplo, invierta el nombre:

#variable to hold the name reversed
reversed_name = ""

#invoke the method passing a different block
person.do_with_name do |name| 
    reversed_name = name.reverse
end

puts reversed_name

=> "racsO"

Utilizamos exactamente el mismo método ( do_with_name): es solo un bloque diferente.

Este ejemplo es trivial. Los usos más interesantes son filtrar todos los elementos en una matriz:

 days = ["monday", "tuesday", "wednesday", "thursday", "friday"]  

 # select those which start with 't' 
 days.select do | item |
     item.match /^t/
 end

=> ["tuesday", "thursday"]

O también podemos proporcionar un algoritmo de ordenación personalizado, por ejemplo, basado en el tamaño de la cadena:

 days.sort do |x,y|
    x.size <=> y.size
 end

=> ["monday", "friday", "tuesday", "thursday", "wednesday"]

Espero que esto te ayude a entenderlo mejor.

Por cierto, si el bloque es opcional, debe llamarlo así:

yield(value) if block_given?

Si no es opcional, simplemente invoca.

EDITAR

@hmak creó un repl.it para estos ejemplos: https://repl.it/@makstaks/blocksandyieldsrubyexample

OscarRyz
fuente
cómo se imprime racsOsi the_name = ""
Paritosh Piplewar
2
Lo sentimos, el nombre es una variable de instancia inicializada con "Oscar" (no está muy claro en la respuesta)
OscarRyz
¿Qué pasa con un código como este? person.do_with_name {|string| yield string, something_else }
f.ardelian
77
Entonces, en términos de Javascripty, es una forma estandarizada de pasar una devolución de llamada a un método determinado y llamarlo. ¡Gracias por la explicación!
yitznewton
De una manera más general: un bloque es un azúcar de sintaxis "mejorado" de rubí para el patrón de Estrategia. porque el uso típico es proporcionar un código para hacer algo en el contexto de otra operación. Pero las mejoras de ruby ​​abren un camino a cosas tan geniales como escribir DSLs usando block para pasar el contexto
Roman Bulgakov
25

En Ruby, los métodos pueden verificar si se llamaron de tal manera que se proporcionó un bloque además de los argumentos normales. Por lo general, esto se hace utilizando el block_given?método, pero también puede referirse al bloque como un Proc explícito al prefijar un signo de y comercial ( &) antes del nombre del argumento final.

Si se invoca un método con un bloque, el método puede yieldcontrolar el bloque (llamar al bloque) con algunos argumentos, si es necesario. Considere este método de ejemplo que demuestra:

def foo(x)
  puts "OK: called as foo(#{x.inspect})"
  yield("A gift from foo!") if block_given?
end

foo(10)
# OK: called as foo(10)
foo(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as foo(123)
# BLOCK: A gift from foo! How nice =)

O, usando la sintaxis especial de argumento de bloque:

def bar(x, &block)
  puts "OK: called as bar(#{x.inspect})"
  block.call("A gift from bar!") if block
end

bar(10)
# OK: called as bar(10)
bar(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as bar(123)
# BLOCK: A gift from bar! How nice =)
maerics
fuente
Es bueno saber diferentes formas de activar un bloqueo.
LPing
22

Es muy posible que alguien proporcione una respuesta verdaderamente detallada aquí, pero siempre he encontrado que esta publicación de Robert Sosinski es una gran explicación de las sutilezas entre bloques, procesos y lambdas.

Debo agregar que creo que la publicación con la que estoy enlazando es específica de ruby ​​1.8. Algunas cosas han cambiado en ruby ​​1.9, como que las variables de bloque sean locales al bloque. En 1.8, obtendría algo como lo siguiente:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Goodbye"

Mientras que 1.9 te daría:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Hello"

No tengo 1.9 en esta máquina, por lo que lo anterior podría tener un error.

theIV
fuente
Gran descripción en ese artículo, me tomó meses darme cuenta por mi cuenta =)
maerics
Estoy de acuerdo. No creo que supiera la mitad de las cosas explicadas hasta que las leí.
IV
El enlace actualizado también es 404 ahora. Aquí está el enlace de Wayback Machine .
klenwell
@klenwell gracias por el aviso, he actualizado el enlace nuevamente.
theIV
13

Quería agregar un poco por qué harías las cosas de esa manera a las excelentes respuestas.

No tengo idea de qué idioma vienes, pero suponiendo que sea un lenguaje estático, este tipo de cosas te resultarán familiares. Así es como se lee un archivo en Java

public class FileInput {

  public static void main(String[] args) {

    File file = new File("C:\\MyFile.txt");
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    DataInputStream dis = null;

    try {
      fis = new FileInputStream(file);

      // Here BufferedInputStream is added for fast reading.
      bis = new BufferedInputStream(fis);
      dis = new DataInputStream(bis);

      // dis.available() returns 0 if the file does not have more lines.
      while (dis.available() != 0) {

      // this statement reads the line from the file and print it to
        // the console.
        System.out.println(dis.readLine());
      }

      // dispose all the resources after using them.
      fis.close();
      bis.close();
      dis.close();

    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Ignorando toda la secuencia de encadenamiento, la idea es esta

  1. Inicialice el recurso que necesita ser limpiado
  2. usar recurso
  3. asegúrate de limpiarlo

Así es como lo haces en rubí

File.open("readfile.rb", "r") do |infile|
    while (line = infile.gets)
        puts "#{counter}: #{line}"
        counter = counter + 1
    end
end

Muy diferente Desglosando este

  1. decirle a la clase File cómo inicializar el recurso
  2. decirle a la clase de archivo qué hacer con ella
  3. reírse de los chicos de Java que todavía están escribiendo ;-)

Aquí, en lugar de manejar los pasos uno y dos, básicamente delega eso en otra clase. Como puede ver, eso reduce drásticamente la cantidad de código que tiene que escribir, lo que hace que las cosas sean más fáciles de leer y reduce las posibilidades de que fugas de memoria o bloqueos de archivos no se borren.

Ahora, no es que no puedas hacer algo similar en Java, de hecho, la gente lo ha estado haciendo durante décadas. Se llama el patrón de estrategia . La diferencia es que sin bloques, para algo simple como el ejemplo de archivo, la estrategia se vuelve excesiva debido a la cantidad de clases y métodos que necesita escribir. Con los bloques, es una forma tan simple y elegante de hacerlo, que no tiene ningún sentido NO estructurar su código de esa manera.

Esta no es la única forma en que se usan los bloques, pero los otros (como el patrón Builder, que puede ver en el formulario_para la api en los rieles) son lo suficientemente similares como para que sea obvio lo que sucede una vez que comprende esto. Cuando ve bloques, generalmente es seguro asumir que la llamada al método es lo que desea hacer, y el bloque describe cómo desea hacerlo.

Matt Briggs
fuente
55
Vamos a simplificar eso un poco: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endy reírnos aún más de los chicos de Java.
Michael Hampton
1
@MichaelHampton, ríe después de leer un archivo de un par de gigabytes de largo.
akostadinov
@akostadinov No ... ¡eso me da ganas de llorar!
Michael Hampton
3
@MichaelHampton O, mejor aún: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(más sin problemas de memoria)
Financia la demanda de Mónica el
12

Este artículo me pareció muy útil. En particular, el siguiente ejemplo:

#!/usr/bin/ruby

def test
  yield 5
  puts "You are in the method test"
  yield 100
end

test {|i| puts "You are in the block #{i}"}

test do |i|
    puts "You are in the block #{i}"
end

que debería dar el siguiente resultado:

You are in the block 5
You are in the method test
You are in the block 100
You are in the block 5
You are in the method test
You are in the block 100

Entonces, esencialmente cada vez que se realiza una llamada a yieldruby, se ejecutará el código en el dobloque o en el interior {}. Si se proporciona un parámetro, yieldentonces se proporcionará como un parámetro para el dobloque.

Para mí, esta fue la primera vez que entendí realmente lo que doestaban haciendo los bloques. Básicamente es una forma para que la función dé acceso a estructuras de datos internas, ya sea para la iteración o para la configuración de la función.

Entonces, cuando estás en rieles, escribes lo siguiente:

respond_to do |format|
  format.html { render template: "my/view", layout: 'my_layout' }
end

Esto ejecutará la respond_tofunción que produce el dobloque con el formatparámetro (interno) . Luego llama a la .htmlfunción en esta variable interna que a su vez produce el bloque de código para ejecutar el rendercomando. Tenga en cuenta que .htmlsolo rendirá si es el formato de archivo solicitado. (tecnicismo: estas funciones en realidad block.callno se usan yieldcomo se puede ver en la fuente, pero la funcionalidad es esencialmente la misma, vea esta pregunta para una discusión). Esto proporciona una forma para que la función realice un poco de inicialización y luego reciba información del código de llamada y luego continúe con el procesamiento si es necesario.

O dicho de otra manera, es similar a una función que toma una función anónima como argumento y luego la llama en javascript.

zelanix
fuente
8

En Ruby, un bloque es básicamente una porción de código que puede pasarse y ejecutarse por cualquier método. Los bloques siempre se usan con métodos, que generalmente les envían datos (como argumentos).

Los bloques se usan ampliamente en las gemas de Ruby (incluidos Rails) y en el código de Ruby bien escrito. No son objetos, por lo tanto, no pueden asignarse a variables.

Sintaxis Básica

Un bloque es un fragmento de código encerrado por {} o do..end. Por convención, la sintaxis de llaves se debe usar para bloques de una sola línea y la sintaxis do..end se debe usar para bloques de varias líneas.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Cualquier método puede recibir un bloque como argumento implícito. La declaración de rendimiento ejecuta un bloque dentro de un método. La sintaxis básica es:

def meditate
  print "Today we will practice zazen"
  yield # This indicates the method is expecting a block
end 

# We are passing a block as an argument to the meditate method
meditate { print " for 40 minutes." }

Output:
Today we will practice zazen for 40 minutes.

Cuando se alcanza la declaración de rendimiento, el método de meditación cede el control al bloque, se ejecuta el código dentro del bloque y se devuelve el control al método, que reanuda la ejecución inmediatamente después de la declaración de rendimiento.

Cuando un método contiene una declaración de rendimiento, espera recibir un bloque en el momento de la llamada. Si no se proporciona un bloque, se generará una excepción una vez que se alcance la declaración de rendimiento. Podemos hacer que el bloque sea opcional y evitar que se genere una excepción:

def meditate
  puts "Today we will practice zazen."
  yield if block_given? 
end meditate

Output:
Today we will practice zazen. 

No es posible pasar múltiples bloques a un método. Cada método puede recibir solo un bloque.

Ver más en: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


fuente
Esta es la (única) respuesta que realmente me hace comprender qué es el bloqueo y el rendimiento, y cómo usarlos.
Eric Wang
5

A veces uso "rendimiento" como este:

def add_to_http
   "http://#{yield}"
end

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
Samet Sazak
fuente
Está bien, pero por qué ? Hay muchas razones, como la Loggerque no debe realizar alguna tarea si el usuario no lo necesita. Aunque deberías explicar el tuyo ...
Ulysse BN
4

Los rendimientos, para decirlo simplemente, permiten que el método que cree tome y llame a los bloques. La palabra clave de rendimiento específicamente es el lugar donde se realizarán las 'cosas' en el bloque.

ntarpey
fuente
1

Hay dos puntos que quiero destacar sobre el rendimiento aquí. Primero, aunque muchas respuestas aquí hablan sobre diferentes formas de pasar un bloque a un método que usa rendimiento, también hablemos sobre el flujo de control. Esto es especialmente relevante ya que puede producir MÚLTIPLES veces a un bloque. Echemos un vistazo a un ejemplo:

class Fruit
  attr_accessor :kinds

  def initialize 
    @kinds = %w(orange apple pear banana)
  end

  def each 
    puts 'inside each'
    3.times { yield (@kinds.tap {|kinds| puts "selecting from #{kinds}"} ).sample }
  end  
end

f = Fruit.new
f.each do |kind|
  puts 'inside block'
end    

=> inside each
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block

Cuando se invoca cada método, se ejecuta línea por línea. Ahora, cuando lleguemos al bloque 3.times, este bloque se invocará 3 veces. Cada vez que invoca rendimiento. Ese rendimiento está vinculado al bloque asociado con el método que llamó a cada método. Es importante tener en cuenta que cada vez que se invoca el rendimiento, devuelve el control al bloque de cada método en el código del cliente. Una vez que el bloque termina de ejecutarse, vuelve al bloque 3.times. Y esto sucede 3 veces. Por lo tanto, ese bloque en el código del cliente se invoca en 3 ocasiones separadas, ya que el rendimiento se llama explícitamente 3 veces separadas.

Mi segundo punto es sobre enum_for y yield. enum_for crea una instancia de la clase Enumerator y este objeto Enumerator también responde al rendimiento.

class Fruit
  def initialize
    @kinds = %w(orange apple)
  end

  def kinds
    yield @kinds.shift
    yield @kinds.shift
  end
end

f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
 => "orange" 
enum.next
 => "apple" 

Por lo tanto, observe que cada vez que invoquemos tipos con el iterador externo, invocará el rendimiento solo una vez. La próxima vez que lo llamemos, invocará el próximo rendimiento y así sucesivamente.

Hay un dato interesante con respecto a enum_for. La documentación en línea establece lo siguiente:

enum_for(method = :each, *args)  enum
Creates a new Enumerator which will enumerate by calling method on obj, passing args if any.

str = "xyz"
enum = str.enum_for(:each_byte)
enum.each { |b| puts b }    
# => 120
# => 121
# => 122

Si no especifica un símbolo como argumento para enum_for, ruby ​​conectará el enumerador a cada método del receptor. Algunas clases no tienen cada método, como la clase String.

str = "I like fruit"
enum = str.to_enum
enum.next
=> NoMethodError: undefined method `each' for "I like fruit":String

Por lo tanto, en el caso de algunos objetos invocados con enum_for, debe ser explícito sobre cuál será su método de enumeración.

Donato
fuente
0

El rendimiento se puede usar como bloque sin nombre para devolver un valor en el método. Considere el siguiente código:

Def Up(anarg)
  yield(anarg)
end

Puede crear un método "Arriba" al que se le asigna un argumento. Ahora puede asignar este argumento para obtener lo que llamará y ejecutará un bloque asociado. Puede asignar el bloque después de la lista de parámetros.

Up("Here is a string"){|x| x.reverse!; puts(x)}

Cuando el método Up llama rendimiento, con un argumento, se pasa a la variable de bloque para procesar la solicitud.

gkstr1
fuente