Blocos e rendimentos em Ruby


275

Estou tentando entender os blocos yielde como eles funcionam no Ruby.

Como é yieldusado? Muitos dos aplicativos Rails que eu já usei de yieldmaneira estranha.

Alguém pode me explicar ou me mostrar para onde ir para entendê-los?


2
Você pode estar interessado na resposta ao recurso de rendimento de Ruby em relação à ciência da computação . Embora seja uma pergunta um pouco diferente da sua, ela pode lançar alguma luz sobre o assunto.
Ken Bloom

Respostas:


393

Sim, é um pouco intrigante a princípio.

No Ruby, os métodos podem receber um bloco de código para executar segmentos arbitrários de código.

Quando um método espera um bloco, ele o chama chamando a yieldfunção

Isso é muito útil, por exemplo, para percorrer uma lista ou fornecer um algoritmo personalizado.

Veja o seguinte exemplo:

Vou definir uma Personclasse inicializada com um nome e fornecer um do_with_namemétodo que, quando chamado, passaria o nameatributo para o bloco recebido.

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

    def do_with_name 
        yield( @name ) 
    end
end

Isso nos permitiria chamar esse método e passar um bloco de código arbitrário.

Por exemplo, para imprimir o nome, faríamos:

person = Person.new("Oscar")

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

Imprimiria:

Hey, his name is Oscar

Observe que o bloco recebe, como parâmetro, uma variável chamada name(NB, você pode chamar essa variável como quiser, mas faz sentido chamá-la name). Quando o código chama, yieldele preenche esse parâmetro com o valor de @name.

yield( @name )

Poderíamos fornecer outro bloco para executar uma ação diferente. Por exemplo, inverta o nome:

#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 exatamente o mesmo método ( do_with_name) - é apenas um bloco diferente.

Este exemplo é trivial. Usos mais interessantes são filtrar todos os elementos em uma matriz:

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

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

=> ["tuesday", "thursday"]

Ou também podemos fornecer um algoritmo de classificação personalizado, por exemplo, com base no tamanho da string:

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

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

Espero que isso ajude você a entender melhor.

BTW, se o bloco for opcional, você deve chamá-lo como:

yield(value) if block_given?

Se não for opcional, basta invocá-lo.

EDITAR

@hmak criou um repl.it para estes exemplos: https://repl.it/@makstaks/blocksandyieldsrubyexample


como ele imprime racsOse the_name = ""
Paritosh Piplewar

2
Desculpe, o nome é uma variável de instância inicializada com "Oscar" (não é muito clara na resposta) #
OscarRyz 21/11/12

E o código assim? person.do_with_name {|string| yield string, something_else }
f.ardelian

7
Portanto, em termos de Javascripty, é uma maneira padronizada de passar um retorno de chamada para um determinado método e chamá-lo. Obrigada pelo esclarecimento!
precisa saber é o seguinte

De uma maneira mais geral - um bloco é um açúcar de sintaxe "aprimorado" em ruby ​​para o padrão Strategy. porque o uso típico é fornecer um código para fazer algo no contexto de outra operação. Mas melhorias rubi abrir um caminho para essas coisas legais como escrever DSLs usando bloco para passar contexto ao redor
Roman Bulgakov

25

No Ruby, os métodos podem verificar se foram chamados de tal maneira que um bloco foi fornecido além dos argumentos normais. Normalmente, isso é feito usando o block_given?método, mas você também pode se referir ao bloco como um Proc explícito, prefixando um e comercial ( &) antes do nome do argumento final.

Se um método for chamado com um bloco, ele poderá yieldcontrolar o bloco (chamar o bloco) com alguns argumentos, se necessário. Considere este método de exemplo que demonstra:

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, usando a sintaxe especial do argumento de bloco:

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

É bom conhecer maneiras diferentes de acionar um bloco.
LPing

22

É bem possível que alguém forneça uma resposta verdadeiramente detalhada aqui, mas eu sempre achei este post de Robert Sosinski uma ótima explicação das sutilezas entre blocos, procs e lambdas.

Devo acrescentar que acredito que o post ao qual estou vinculando é específico do ruby ​​1.8. Algumas coisas mudaram no ruby ​​1.9, como variáveis ​​de bloco sendo locais no bloco. No 1.8, você obteria algo como o seguinte:

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

Considerando que 1.9 daria a você:

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

Eu não tenho 1.9 nesta máquina, portanto, o acima pode ter um erro.


Grande descrição nesse artigo, ele me levou meses para descobrir isso tudo para fora em minhas próprias =)
maerics

Concordo. Acho que não sabia metade das coisas explicadas até a ler.
theIV 18/06/10

O link atualizado também é 404 agora. Aqui está o link Wayback Machine .
klenwell

@klenwell obrigado pelo aviso, atualizei o link novamente.
theIV 07/07

13

Eu queria acrescentar por que você faria as coisas dessa maneira com as já excelentes respostas.

Não faço ideia de que idioma você é, mas, supondo que seja um idioma estático, esse tipo de coisa parecerá familiar. É assim que você lê um arquivo em 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 o encadeamento do fluxo, a ideia é essa

  1. Inicialize o recurso que precisa ser limpo
  2. usar recurso
  3. certifique-se de limpá-lo

É assim que você faz em rubi

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

Descontroladamente diferente. Quebrando este aqui

  1. diga à classe File como inicializar o recurso
  2. diga à classe do arquivo o que fazer com ela
  3. rir dos caras de java que ainda estão digitando ;-)

Aqui, em vez de lidar com as etapas um e dois, você basicamente delega isso para outra classe. Como você pode ver, isso reduz drasticamente a quantidade de código que você precisa escrever, o que facilita a leitura e reduz as chances de coisas como vazamentos de memória ou bloqueios de arquivos que não são limpos.

Agora, não é como se você não pudesse fazer algo semelhante em java; de fato, as pessoas fazem isso há décadas. É chamado de padrão de estratégia . A diferença é que, sem blocos, para algo simples como o exemplo de arquivo, a estratégia se torna um exagero devido à quantidade de classes e métodos que você precisa escrever. Com blocos, é uma maneira tão simples e elegante de fazê-lo, que não faz sentido NÃO estruturar seu código dessa maneira.

Esta não é a única maneira pela qual os blocos são usados, mas os outros (como o padrão Builder, que você pode ver no formulário_para API nos trilhos) são semelhantes o suficiente para que seja óbvio o que está acontecendo quando você entender isso. Quando você vê blocos, geralmente é seguro assumir que a chamada do método é o que você deseja fazer, e o bloco está descrevendo como você deseja fazê-lo.


5
Vamos simplificar um pouco: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" ende rir ainda mais dos caras do Java.
Michael Hampton

1
@ MichaelHampton, ria depois de ler um arquivo com alguns gigabytes.
akostadinov 17/02/2015

@akostadinov Não ... isso me faz querer chorar!
Michael Hampton

3
@MichaelHampton Ou, melhor ainda: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(sem problemas de memória)
Fund Monica's Lawsuit

12

Eu achei este artigo muito útil. Em particular, o seguinte exemplo:

#!/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 deve fornecer a seguinte saída:

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

Então, essencialmente, toda vez que uma chamada é feita para yieldruby, o código é executado no dobloco ou no interior {}. Se um parâmetro for fornecido yield, ele será fornecido como um parâmetro para o dobloco.

Para mim, foi a primeira vez que entendi realmente o que os doblocos estavam fazendo. É basicamente uma maneira de a função conceder acesso a estruturas de dados internas, seja para iteração ou para configuração da função.

Então, quando nos trilhos, você escreve o seguinte:

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

Isso executará a respond_tofunção que gera o dobloco com o formatparâmetro (interno) . Você então chama a .htmlfunção nessa variável interna que, por sua vez, gera o bloco de código para executar o rendercomando. Observe que .htmlsomente produzirá se for o formato de arquivo solicitado. (tecnicidade: essas funções na verdade block.callnão são usadas yieldcomo você pode ver na fonte, mas a funcionalidade é essencialmente a mesma; consulte esta pergunta para uma discussão.) Isso fornece um meio para a função executar alguma inicialização, depois receber informações do código de chamada e Em seguida, continue o processamento, se necessário.

Em outras palavras, é semelhante a uma função que usa uma função anônima como argumento e a chama em javascript.


8

No Ruby, um bloco é basicamente um pedaço de código que pode ser passado e executado por qualquer método. Os blocos são sempre usados ​​com métodos, que geralmente alimentam dados para eles (como argumentos).

Os blocos são amplamente utilizados em gemas Ruby (incluindo Rails) e em código Ruby bem escrito. Eles não são objetos, portanto, não podem ser atribuídos a variáveis.

Sintaxe básica

Um bloco é um pedaço de código delimitado por {} ou do..end. Por convenção, a sintaxe entre chaves deve ser usada para blocos de linha única e a sintaxe do..end deve ser usada para blocos de várias linhas.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Qualquer método pode receber um bloco como argumento implícito. Um bloco é executado pela declaração de rendimento dentro de um método. A sintaxe básica é:

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.

Quando a declaração de rendimento é alcançada, o método meditate gera controle para o bloco, o código dentro do bloco é executado e o controle é retornado ao método, que retoma a execução imediatamente após a declaração de rendimento.

Quando um método contém uma declaração de rendimento, espera receber um bloco no momento da chamada. Se um bloco não for fornecido, uma exceção será lançada assim que a declaração de rendimento for alcançada. Podemos tornar o bloco opcional e evitar que uma exceção seja gerada:

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

Output:
Today we will practice zazen. 

Não é possível passar vários blocos para um método. Cada método pode receber apenas um bloco.

Veja mais em: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


Esta é a (única) resposta que realmente me faz entender o que é bloqueio e rendimento e como usá-los.
Eric Wang

5

Às vezes eu uso "yield" assim:

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

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}

OK mas porquê ? Existem várias razões, como a Loggerque não deve ser executada, se o usuário não precisar. Você deve explicar o seu embora ...
Ulysse BN

4

Rendimentos, para simplificar, permitem que o método que você cria receba e chame blocos. A palavra-chave yield é especificamente o local onde as 'coisas' no bloco serão executadas.


1

Há dois pontos que quero destacar sobre o rendimento aqui. Primeiro, embora muitas respostas aqui falem sobre diferentes maneiras de passar um bloco para um método que usa yield, vamos também falar sobre o fluxo de controle. Isso é especialmente relevante, pois você pode gerar vários tempos em um bloco. Vamos dar uma olhada em um exemplo:

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

Quando o método each é invocado, ele executa linha por linha. Agora, quando chegarmos ao bloco 3. vezes, esse bloco será chamado 3 vezes. Cada vez que invoca rendimento. Esse rendimento está vinculado ao bloco associado ao método que chamou o método each. É importante observar que cada vez que o rendimento é invocado, ele retorna o controle de volta ao bloco de cada método no código do cliente. Quando o bloco termina de executar, ele volta ao bloco 3.x vezes. E isso acontece 3 vezes. Portanto, esse bloco no código do cliente é chamado em 3 ocasiões separadas, pois o rendimento é explicitamente chamado 3 vezes em separado.

Meu segundo ponto é sobre enum_for e yield. enum_for instancia a classe Enumerator e esse objeto Enumerator também responde ao rendimento.

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" 

Portanto, observe que toda vez que invocamos tipos com o iterador externo, ele invocará yield apenas uma vez. A próxima vez que chamarmos, ele invocará o próximo rendimento e assim por diante.

Há um boato interessante em relação a enum_for. A documentação online declara o seguinte:

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

Se você não especificar um símbolo como argumento para enum_for, o ruby ​​conectará o enumerador a cada método do destinatário. Algumas classes não possuem um método para cada, como a classe String.

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

Portanto, no caso de alguns objetos invocados com enum_for, você deve ser explícito quanto ao método de enumeração.


0

O rendimento pode ser usado como bloco sem nome para retornar um valor no método Considere o seguinte código:

Def Up(anarg)
  yield(anarg)
end

Você pode criar um método "Up" ao qual é atribuído um argumento. Agora você pode atribuir esse argumento ao rendimento que chamará e executará um bloco associado. Você pode atribuir o bloco após a lista de parâmetros.

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

Quando o método Up chama yield, com um argumento, ele é passado para a variável de bloco para processar a solicitação.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.