web-dev-qa-db-fra.com

Blocs et rendements en rubis

J'essaie de comprendre les blocs et yield et leur fonctionnement en Ruby.

Comment yield est-il utilisé? La plupart des applications Rails que j'ai examinées utilisent yield de manière étrange.

Quelqu'un peut-il m'expliquer ou me montrer où aller pour les comprendre?

251
Matt Elhotiby

Oui, c'est un peu déroutant au début. 

En Ruby, les méthodes peuvent recevoir un bloc de code afin d’exécuter des segments de code arbitraires. 

Lorsqu'une méthode attend un bloc, elle l'invoque en appelant la fonction yield

C'est très pratique, par exemple, pour parcourir une liste ou pour fournir un algorithme personnalisé. 

Prenons l'exemple suivant:

Je vais définir une classe Person initialisée avec un nom et fournir une méthode do_with_name qui, une fois appelée, transmettrait simplement l'attribut name au bloc reçu.

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

    def do_with_name 
        yield( @name ) 
    end
end

Cela nous permettrait d'appeler cette méthode et de passer un bloc de code arbitraire.

Par exemple, pour imprimer le nom, nous ferions:

person = Person.new("Oscar")

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

Souhaitez imprimer:

Hey, his name is Oscar

Notez que le bloc reçoit, en tant que paramètre, une variable appelée name (NB: vous pouvez appeler cette variable comme bon vous semble, mais il est logique de l’appeler name). Lorsque le code appelle yield, il remplit ce paramètre avec la valeur @name

yield( @name )

Nous pourrions fournir un autre bloc pour effectuer une action différente. Par exemple, inversez le nom:

#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"

Nous avons utilisé exactement la même méthode (do_with_name) - il s’agit simplement d’un bloc différent.

Cet exemple est trivial. Les utilisations les plus intéressantes sont de filtrer tous les éléments d’un tableau:

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

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

=> ["tuesday", "thursday"]

Nous pouvons également fournir un algorithme de tri personnalisé, basé par exemple sur la taille de la chaîne:

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

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

J'espère que cela vous aide à mieux comprendre.

BTW, si le bloc est optionnel, vous devriez l'appeler comme suit:

yield(value) if block_given?

Si n'est pas facultatif, il suffit de l'invoquer. 

354
OscarRyz

En Ruby, les méthodes peuvent vérifier si elles ont été appelées de manière à fournir un bloc en plus des arguments normaux. Cela se fait généralement à l'aide de la méthode block_given?, mais vous pouvez également faire référence au bloc en tant que Proc explicite en préfixant une esperluette (&) avant le nom de l'argument final.

Si une méthode est invoquée avec un bloc, elle peut yield contrôler le bloc (appeler le bloc) avec des arguments, si nécessaire. Considérez cet exemple de méthode qui montre:

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 =)

Ou, en utilisant la syntaxe d'argument de bloc spéciale:

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 =)
22
maerics

Il est fort possible que quelqu'un fournisse une réponse vraiment détaillée ici, mais j'ai toujours trouvé cet article de Robert Sosinski une excellente explication des subtilités entre blocs, procs et lambdas.

Je devrais ajouter que je pense que le message auquel je fais référence est spécifique à Ruby 1.8. Certaines choses ont changé dans Ruby 1.9, telles que les variables de bloc étant locales au bloc. En 1.8, vous obtiendrez quelque chose comme ceci:

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

Alors que 1.9 vous donnerait:

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

Je n'ai pas 1.9 sur cette machine, donc ce qui précède peut contenir une erreur.

22
theIV

Je voulais en quelque sorte ajouter pourquoi vous feriez de cette manière aux réponses déjà excellentes.

Vous ne savez pas de quelle langue vous venez, mais en supposant qu'il s'agisse d'un langage statique, ce genre de chose vous semblera familier. Voici comment lire un fichier 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();
    }
  }
}

Ignorer tout ce qui enchaîne les flux, L'idée est la suivante

  1. Initialiser la ressource à nettoyer
  2. utiliser la ressource
  3. assurez-vous de le nettoyer

C'est comme ça qu'on fait en rubis

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

Follement différent. Briser celui-ci

  1. indiquer à la classe File comment initialiser la ressource
  2. dire à la classe de fichiers quoi en faire
  3. rire des gars de Java qui tapent encore ;-)

Ici, au lieu de gérer les étapes 1 et 2, vous déléguez cette opération à une autre classe. Comme vous pouvez le constater, cela réduit considérablement la quantité de code que vous devez écrire, ce qui simplifie la lecture et réduit les risques de fuites de mémoire ou de verrous de fichiers non effacés. 

Maintenant, ce n'est pas comme si vous ne pouviez pas faire quelque chose de similaire en Java, en fait, les gens le font depuis des décennies. C'est ce qu'on appelle le modèle Strategy . La différence est que sans blocs, pour quelque chose d'aussi simple que l'exemple de fichier, la stratégie devient excessive en raison de la quantité de classes et de méthodes que vous devez écrire. Avec les blocs, c’est une façon tellement simple et élégante de le faire que cela n’a aucun sens de NE PAS structurer votre code de cette façon.

Ce n'est pas la seule façon d'utiliser les blocs, mais les autres (comme le motif Builder, que vous pouvez voir dans l'onglet form_for d'API dans Rails) sont suffisamment similaires pour qu'il soit évident de savoir ce qui se passe une fois que vous comprenez cela. Lorsque vous voyez des blocs, il est généralement prudent de supposer que l'appel de la méthode correspond à ce que vous voulez faire et qu'il décrit comment vous voulez le faire.

13
Matt Briggs

J'ai trouvé cet article être très utile. En particulier, l'exemple suivant:

#!/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

ce qui devrait donner la sortie suivante:

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

Donc, chaque fois qu'un appel est fait à yield, Ruby exécutera le code dans le bloc do ou à l'intérieur de {}. Si un paramètre est fourni à yield, il le sera en tant que paramètre du bloc do.

Pour moi, c’était la première fois que je comprenais vraiment ce que faisaient les blocs do. C'est en gros un moyen pour la fonction de donner accès aux structures de données internes, que ce soit pour l'itération ou pour la configuration de la fonction.

Ainsi, lorsque vous êtes dans Rails, vous écrivez ce qui suit:

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

Ceci exécutera la fonction respond_to qui produit le bloc do avec le paramètre (interne) format. Vous appelez ensuite la fonction .html sur cette variable interne, ce qui génère à son tour le bloc de code pour exécuter la commande render. Notez que .html ne cédera que s’il s’agit du format de fichier demandé. (technicité: ces fonctions utilisent en réalité block.call et non pas yield comme vous pouvez le voir dans source mais la fonctionnalité est essentiellement la même, voir cette question pour une discussion.) Ceci fournit à la fonction un moyen de fonctionner une certaine initialisation prend alors une entrée du code appelant et continue ensuite le traitement si nécessaire.

Autrement dit, cela ressemble à une fonction prenant une fonction anonyme comme argument puis l'appelant en javascript.

12
zelanix

En Ruby, un bloc est essentiellement un bloc de code pouvant être transmis et exécuté par n’importe quelle méthode. Les blocs sont toujours utilisés avec des méthodes, qui leur fournissent généralement des données (sous forme d'arguments). 

Les blocs sont largement utilisés dans les gemmes Ruby (y compris Rails) et dans le code Ruby bien écrit. Ils ne sont pas des objets et ne peuvent donc pas être affectés à des variables. 

Syntaxe de base

Un bloc est un morceau de code compris entre {} ou do..end. Par convention, la syntaxe entre accolades doit être utilisée pour les blocs d'une seule ligne et la syntaxe do..end doit être utilisée pour les blocs de plusieurs lignes. 

{ # This is a single line block }

do
  # This is a multi-line block
end 

Toute méthode peut recevoir un bloc en tant qu'argument implicite. Un bloc est exécuté par la déclaration de rendement dans une méthode. La syntaxe de base est la suivante: 

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.

Lorsque l'instruction de rendement est atteinte, la méthode meditate donne le contrôle au bloc, le code dans le bloc est exécuté et le contrôle est renvoyé à la méthode, qui reprend l'exécution immédiatement après l'instruction de rendement. 

Lorsqu'une méthode contient une instruction de rendement, elle s'attend à recevoir un bloc au moment de l'appel. Si un bloc n'est pas fourni, une exception sera levée une fois que la déclaration de rendement est atteinte. Nous pouvons rendre le bloc facultatif et éviter qu'une exception ne soit déclenchée: 

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

Output:
Today we will practice zazen. 

Il n'est pas possible de transmettre plusieurs blocs à une méthode. Chaque méthode ne peut recevoir qu'un seul bloc. 

Plus d’informations sur: http://www.zenruby.info/2016/04/introduction-to-blocks-in-Ruby.html

7
user3847220

J'utilise parfois "rendement" comme ceci:

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

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
5
Samet Sazak

En termes simples, les rendements permettent à la méthode que vous créez de prendre et d'appeler des blocs. Le mot-clé de rendement est précisément l'endroit où les "éléments" du bloc seront exécutés.

4
ntarpey

Je veux faire deux remarques sur le rendement. Premièrement, bien que beaucoup de réponses évoquent différentes manières de passer d'un bloc à une méthode utilisant le rendement, parlons également du flux de contrôle. Ceci est particulièrement important car vous pouvez céder plusieurs fois à un bloc. Jetons un coup d'oeil à un exemple:

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

Lorsque chaque méthode est invoquée, elle s'exécute ligne par ligne. Maintenant, quand nous arriverons au bloc 3.times, ce bloc sera invoqué 3 fois. Chaque fois qu'il appelle un rendement. Ce rendement est lié au bloc associé à la méthode qui a appelé chaque méthode. Il est important de noter que chaque fois que le rendement est appelé, il renvoie le contrôle au bloc de chaque méthode dans le code client. Une fois que le bloc est terminé, il revient au bloc 3.times. Et cela se produit 3 fois. Donc, ce bloc dans le code client est invoqué à 3 reprises car le rendement est appelé explicitement 3 fois. 

Mon deuxième point concerne l'énumération et le rendement. enum_for instancie la classe Enumerator et cet objet Enumerator répond également au rendement. 

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" 

Donc, notez que chaque fois que nous appelons des types avec l’itérateur externe, il n’invoquera le rendement qu’une fois. La prochaine fois que nous l'appellerons, il invoquera le prochain rendement, etc. 

Il y a une friandise intéressante en ce qui concerne enum_for. La documentation en ligne indique ce qui suit:

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 vous ne spécifiez pas de symbole en tant qu'argument pour enum_for, Ruby connecte l'énumérateur à chaque méthode du destinataire. Certaines classes n'ont pas de méthode, comme la classe String. 

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

Ainsi, dans le cas de certains objets appelés avec enum_for, vous devez être explicite sur ce que sera votre méthode d'énumération. 

1
Donato

Yield peut être utilisé comme bloc sans nom pour renvoyer une valeur dans la méthode. Considérons le code suivant:

Def Up(anarg)
  yield(anarg)
end

Vous pouvez créer une méthode "Up" à laquelle un argument est attribué. Vous pouvez maintenant affecter cet argument à renvoyer qui appellera et exécutera un bloc associé. Vous pouvez affecter le bloc après la liste de paramètres.

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

Lorsque la méthode Up appelle les commandes renvoyer, avec un argument, il est passé à la variable de bloc pour traiter la demande.

0
gkstr1