Blocs et rendements en Ruby

275

J'essaie de comprendre les blocs yieldet comment ils fonctionnent dans Ruby.

Comment est-il yieldutilisé? Beaucoup d'applications Rails que j'ai examinées sont utilisées yieldde manière étrange.

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

Matt Elhotiby
la source
2
Vous pourriez être intéressé par la réponse à la fonction de rendement de Ruby par rapport à l'informatique . Bien qu'il s'agisse d'une question quelque peu différente de la vôtre, elle peut éclairer la question.
Ken Bloom

Réponses:

393

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

Dans Ruby, les méthodes peuvent recevoir un bloc de code afin d'effectuer des segments de code arbitraires.

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

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 Personclasse initialisée avec un nom et fournir une do_with_nameméthode qui, lorsqu'elle est invoquée, passerait simplement l' nameattribut 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

Imprime:

Hey, his name is Oscar

Remarquez, le bloc reçoit, en 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 invoque, yieldil remplit ce paramètre avec la valeur de @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) - c'est juste 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"]

Ou, nous pouvons également fournir un algorithme de tri personnalisé, par exemple basé 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 le comprendre.

BTW, si le bloc est facultatif, vous devez l'appeler comme:

yield(value) if block_given?

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

ÉDITER

@hmak a créé un repl.it pour ces exemples: https://repl.it/@makstaks/blocksandyieldsrubyexample

OscarRyz
la source
comment il imprime racsOsi the_name = ""
Paritosh Piplewar
2
Désolé, le nom est une variable d'instance initialisée avec "Oscar" (n'est pas très clair dans la réponse)
OscarRyz
Et le code comme ça? person.do_with_name {|string| yield string, something_else }
f.ardelian
7
Donc, en termes Javascripty, c'est un moyen standardisé de passer un rappel à une méthode donnée et de l'appeler. Merci pour l'explication!
yitznewton
D'une manière plus générale - un bloc est un sucre de syntaxe rubis "amélioré" pour le modèle de stratégie. parce que l'usage typique est de fournir un code pour faire quelque chose dans le contexte d'une autre opération. Mais les améliorations de rubis ouvrent la voie à des choses aussi cool que l'écriture de DSL en utilisant un bloc pour passer le contexte
Roman Bulgakov
25

Dans Ruby, les méthodes peuvent vérifier si elles ont été appelées de telle manière qu'un bloc a été fourni en plus des arguments normaux. En règle générale, cela se fait à l'aide de la block_given?méthode, 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, la méthode peut yieldcontrôler le bloc (appeler le bloc) avec quelques arguments, si nécessaire. Considérez cet exemple de méthode qui illustre:

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 spéciale des arguments de bloc:

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
la source
Bon à savoir différentes façons de déclencher un blocage.
LPing
22

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

Je dois ajouter que je pense que le post auquel je fais un lien 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 pourrait avoir une erreur.

theIV
la source
Grande description dans cet article, il m'a fallu des mois pour comprendre cela tout seul =)
maerics
Je suis d'accord. Je ne pense pas que je connaissais la moitié des choses expliquées jusqu'à ce que je les lise.
theIV
Le lien mis à jour est désormais le 404. Voici le lien Wayback Machine .
klenwell
@klenwell merci pour l'avertissement, j'ai de nouveau mis à jour le lien.
theIV
13

Je voulais en quelque sorte ajouter pourquoi vous feriez les choses de cette façon aux réponses déjà excellentes.

Je ne sais pas de quelle langue vous venez, mais en supposant qu'il s'agit 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();
    }
  }
}

Ignorant tout le chaînage de flux, l'idée est la suivante

  1. Initialiser la ressource qui doit être nettoyée
  2. utiliser la ressource
  3. assurez-vous de le nettoyer

Voici comment vous le faites en rubis

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

Extrêmement 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 un et deux, vous déléguez essentiellement cela dans une autre classe. Comme vous pouvez le voir, cela réduit considérablement la quantité de code que vous devez écrire, ce qui facilite 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 maintenant. Cela s'appelle le modèle de stratégie . La différence est que sans blocs, pour quelque chose de simple comme 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 manière si 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 dont les blocs sont utilisés, mais les autres (comme le modèle Builder, que vous pouvez voir dans l'api form_for dans les rails) sont suffisamment similaires pour qu'il soit évident de savoir ce qui se passe une fois que vous avez tourné la tête autour de cela. Lorsque vous voyez des blocs, il est généralement sûr de supposer que l'appel de méthode est ce que vous voulez faire, et le bloc décrit comment vous voulez le faire.

Matt Briggs
la source
5
Simplifions cela un peu: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endet rions encore plus des gars Java.
Michael Hampton
1
@MichaelHampton, riez après avoir lu un fichier de quelques gigaoctets.
akostadinov
@akostadinov Non ... ça me donne envie de pleurer!
Michael Hampton
3
@MichaelHampton Ou, mieux encore: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(plus aucun problème de mémoire)
Fund Monica's Lawsuit
12

J'ai trouvé cet article 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

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, essentiellement, à chaque appel à yieldruby, le code sera exécuté dans le dobloc ou à l'intérieur {}. Si un paramètre est fourni à yieldalors il sera fourni comme paramètre à lado bloc.

Pour moi, c'était la première fois que je comprenais vraiment ce que do faisaient blocs. C'est fondamentalement 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.

Donc, dans les rails, vous écrivez ce qui suit:

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

Cela exécutera la respond_tofonction qui produit le dobloc avec le formatparamètre (interne) . Vous appelez ensuite la .htmlfonction sur cette variable interne qui à son tour donne le bloc de code pour exécuter la rendercommande. Notez que .htmlcela ne donnera que si c'est le format de fichier demandé. (technicité: ces fonctions n'utilisent block.callpas réellement yieldcomme vous pouvez le voir à la source mais la fonctionnalité est essentiellement la même, voir cette question pour une discussion.) Cela permet à la fonction d'effectuer une initialisation puis de saisir le code appelant et puis poursuivez le traitement si nécessaire.

Ou, autrement dit, c'est semblable à une fonction prenant une fonction anonyme comme argument et l'appelle ensuite en javascript.

zelanix
la source
8

Dans Ruby, un bloc est essentiellement un morceau de code qui peut ê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. Ce 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 entouré par {} ou do..end. Par convention, la syntaxe d'accolade doit être utilisée pour les blocs à une seule ligne et la syntaxe do..end doit être utilisée pour les blocs à plusieurs lignes.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Toute méthode peut recevoir un bloc comme argument implicite. Un bloc est exécuté par l'instruction yield dans une méthode. La syntaxe de base est:

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 yield est atteinte, la méthode meditate cède le contrôle au bloc, le code dans le bloc est exécuté et le contrôle est retourné à la méthode, qui reprend l'exécution immédiatement après l'instruction yield.

Lorsqu'une méthode contient une déclaration de rendement, elle s'attend à recevoir un bloc au moment de l'appel. Si aucun bloc n'est fourni, une exception sera levée une fois la déclaration de rendement 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 passer plusieurs blocs à une méthode. Chaque méthode ne peut recevoir qu'un seul bloc.

Voir plus à: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


la source
C'est la (seule) réponse qui me fait vraiment comprendre ce qu'est le bloc et le rendement, et comment les utiliser.
Eric Wang
5

J'utilise parfois "yield" comme ceci:

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

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
Samet Sazak
la source
D'accord mais pourquoi ? Il existe de nombreuses raisons, telles que celle-ci Loggerne doit pas effectuer de tâche si l'utilisateur n'en a pas besoin. Vous devriez cependant expliquer les vôtres ...
Ulysse BN
4

Les rendements, pour le dire simplement, permettent à la méthode que vous créez de prendre et d'appeler des blocs. Le mot-clé yield est spécifiquement l'endroit où les «trucs» du bloc seront exécutés.

ntarpey
la source
1

Il y a deux points que je veux faire valoir au sujet du rendement ici. Tout d'abord, alors que de nombreuses réponses parlent ici de différentes façons de passer un bloc à une méthode qui utilise le rendement, parlons également du flux de contrôle. Ceci est particulièrement pertinent car vous pouvez générer plusieurs fois un bloc. Prenons 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 la méthode each est invoquée, elle s'exécute ligne par ligne. Maintenant, lorsque nous arrivons au bloc 3.times, ce bloc sera invoqué 3 fois. Chaque fois, il invoque le 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 yield est invoqué, il renvoie le contrôle au bloc de chaque méthode dans le code client. Une fois l'exécution du bloc terminée, il revient au bloc 3 fois. Et cela se produit 3 fois. Donc, ce bloc dans le code client est invoqué à 3 reprises distinctes puisque le rendement est explicitement appelé 3 fois distinctes.

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

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" 

Notez donc que chaque fois que nous invoquons des types avec l'itérateur externe, il n'appellera le rendement qu'une seule fois. La prochaine fois que nous l'appellerons, il invoquera le prochain rendement et ainsi de suite.

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 comme argument pour enum_for, ruby ​​raccordera l'énumérateur à chaque méthode du récepteur. Certaines classes n'ont pas de méthode each, 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 invoqués avec enum_for, vous devez être explicite quant à votre méthode d'énumération.

Donato
la source
0

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

Def Up(anarg)
  yield(anarg)
end

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

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

Lorsque la méthode Up appelle yield, avec un argument, elle est transmise à la variable de bloc pour traiter la demande.

gkstr1
la source