Estou tentando entender os blocos yield
e como eles funcionam no Ruby.
Como é yield
usado? Muitos dos aplicativos Rails que eu já usei de yield
maneira estranha.
Alguém pode me explicar ou me mostrar para onde ir para entendê-los?
Estou tentando entender os blocos yield
e como eles funcionam no Ruby.
Como é yield
usado? Muitos dos aplicativos Rails que eu já usei de yield
maneira estranha.
Alguém pode me explicar ou me mostrar para onde ir para entendê-los?
Respostas:
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 yield
função
Isso é muito útil, por exemplo, para percorrer uma lista ou fornecer um algoritmo personalizado.
Veja o seguinte exemplo:
Vou definir uma Person
classe inicializada com um nome e fornecer um do_with_name
método que, quando chamado, passaria o name
atributo 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, yield
ele 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
racsO
se the_name = ""
"Oscar"
(não é muito clara na resposta) #
person.do_with_name {|string| yield string, something_else }
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á yield
controlar 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 =)
É 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.
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
É 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
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.
File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" end
e rir ainda mais dos caras do Java.
IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }
(sem problemas de memória)
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 yield
ruby, o código é executado no do
bloco ou no interior {}
. Se um parâmetro for fornecido yield
, ele será fornecido como um parâmetro para o do
bloco.
Para mim, foi a primeira vez que entendi realmente o que os do
blocos 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_to
função que gera o do
bloco com o format
parâmetro (interno) . Você então chama a .html
função nessa variável interna que, por sua vez, gera o bloco de código para executar o render
comando. Observe que .html
somente produzirá se for o formato de arquivo solicitado. (tecnicidade: essas funções na verdade block.call
não são usadas yield
como 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.
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.
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
À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"}
Logger
que não deve ser executada, se o usuário não precisar. Você deve explicar o seu embora ...
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.
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.