Bloques y rendimientos en Ruby


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

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

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

 242
Author: the Tin Man, 2010-06-18

9 answers

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 función yield.

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

Tomemos el siguiente ejemplo:

Voy a definir una clase Person inicializada con un nombre, y proporcionar un método do_with_name que cuando invocado, simplemente pasaría el atributo name, 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:{[18]]}

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

Observe que el bloque recibe, como parámetro, una variable llamada name (N.B. puede llamar a esta variable como quiera, pero tiene sentido llamarla name). Cuando el código 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, invertir 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"

Usamos exactamente el mismo método (do_with_name) - es solo un bloque diferente.

Este ejemplo es trivial. 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 deberías llamarlo como:

yield(value) if block_given?

Si no es opcional, simplemente invócalo.

 334
Author: OscarRyz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-20 18:01:33

Es muy posible que alguien proporcione una respuesta verdaderamente detallada aquí, pero siempre he encontrado este post de Robert Sosinski para ser una gran explicación de las sutilezas entre bloques, procs y lambdas.

Debo añadir que creo que el post al que estoy enlazando es específico 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ías algo como lo siguiente:

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

Considerando que 1.9 daría usted:

>> 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.

 20
Author: theIV,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-07 20:13:04

En Ruby, los métodos pueden verificar si fueron llamados de tal manera que se proporcionó un bloque además de los argumentos normales. Normalmente esto se hace usando el método block_given?, pero también puede referirse al bloque como un Proc explícito prefijando un ampersand (&) antes del nombre del argumento final.

Si se invoca un método con un bloque, entonces el método puede yield controlar 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 del argumento de bloque especial:

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 =)
 19
Author: maerics,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-08 16:32:27

Quería agregar por qué harías las cosas de esa manera a las ya grandes respuestas.

No tengo idea de qué idioma vienes, pero asumiendo que es un lenguaje estático, este tipo de cosas te parecerá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 todo el asunto del encadenamiento de la corriente, la idea es esta

  1. Inicializar el recurso que necesita ser limpiado
  2. use el recurso
  3. asegúrese de limpiarlo

Así es como hazlo en ruby

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

Tremendamente diferente. Rompiendo éste abajo

  1. dile a la clase de archivo cómo inicializar el recurso
  2. dile a la clase de archivo qué hacer con él
  3. ríete de los chicos de java que todavía están escribiendo; -)

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

Ahora, no es como no se puede hacer algo similar en Java, de hecho, la gente lo ha estado haciendo durante décadas. Se llama el patrón 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 sencilla y elegante de hacerlo, que no hace ninguna sentido de 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 se puede ver en la api form_for en rails) son lo suficientemente similares como para que sea obvio lo que está pasando una vez que se envuelva en esto. Cuando ves bloques, por lo general es seguro asumir que la llamada al método es lo que quieres hacer, y el bloque está describiendo cómo quieres hacerlo.

 13
Author: Matt Briggs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-30 21:24:39

Encontré este artículo para ser 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 la siguiente salida:

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

Así que esencialmente cada vez que se haga una llamada a yield ruby ejecutará el código en el bloque do o dentro de {}. Si se proporciona un parámetro a yield, se proporcionará como parámetro al bloque do.

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

Así que cuando en rails escribes lo siguiente:

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

Esto ejecutará la función respond_to que produce el bloque do con el parámetro (interno) format. A continuación, se llama a la función .html en esta variable interna que a su vez produce el bloque de código para ejecutar el comando render. Tenga en cuenta que .html solo rendirá si es el formato de archivo solicitar. (tecnicismo: estas funciones en realidad utilizan block.call no yield como se puede ver en la fuente pero la funcionalidad es esencialmente la misma, ver esta pregunta para una discusión.) Esto proporciona una forma para que la función realice alguna inicialización, luego tome la entrada 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 javascript.

 12
Author: zelanix,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:55:10

En Ruby, un bloque es básicamente un trozo de código que puede ser pasado y ejecutado por cualquier método. Los bloques siempre se usan con métodos, que generalmente les proporcionan datos (como argumentos).

Los bloques son ampliamente utilizados en gemas Ruby (incluyendo Rails) y en código Ruby bien escrito. No son objetos, por lo tanto no se pueden asignar a variables.

Sintaxis básica

Un bloque es una pieza de código encerrada por { } o do..final. Por convención, la sintaxis de llaves debe usarse para bloques de una sola línea y el do..la sintaxis final debe usarse 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. Un bloque se ejecuta mediante la sentencia yield 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 sentencia yield, el método meditate cede el control al bloque, el código dentro del bloque se ejecuta y el control se devuelve al método, que reanuda la ejecución inmediatamente después de la sentencia yield.

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 lanzará 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 varios 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

 7
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-01 15:22:49

A veces uso "rendimiento" de esta manera:

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

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
 5
Author: smtszk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-04 01:26:31

Los rendimientos, en pocas palabras, permiten que el método que se crea para tomar y llamar a los bloques. La palabra clave yield específicamente es el lugar donde se realizarán las 'cosas' en el bloque.

 4
Author: ntarpey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-02 01:02:11

Yield 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 "Up" al que se le asigna un argumento. Ahora puede asignar este argumento a yield 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 a yield, con un argumento, se pasa a la variable block para procesar la solicitud.

 0
Author: gkstr1,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-21 15:34:27