Conceitos na prática: Ruby block (yield)

Olá! Esta será uma série de posts que vão apresentar alguns conceitos bem legais da linguagem Ruby e situações onde poderemos aplicá-los para obter um código bem melhor. E o primeiro post será sobre blocos em ruby e a famosa palavra chave yield.

O que são blocos

Em Ruby existem várias definições diferentes de coisas bem semelhantes. Blocos, Lambda e Procs são referenciados de maneira geral como “closure”. Além de dar nome a uma linguagem, closure é uma funcionalidade que permite escrever um pedaço de código que pode ser [1]:

  • Utilizado como um valor (podendo ser atribuído, passado como parâmetro, etc.)
  • Pode ser executado em qualquer lugar
  • Referenciam variáveis no contexto onde são criados

Nos próximos posts eu falo um pouco mais sobre os benefícios da utilização de closure, agora vamos focar na parte prática da coisa.

Passando blocos como argumento

Blocos em ruby são pedaços de código que estão entre “do end” ou entre “{}”. Veja um exemplo:

array.each do |e|
  puts e
end

O código “puts e” que está entre o “do end” é um bloco que foi passado como parâmetro para o each.

Recebendo blocos como argumento

Para exemplificar, vamos tentar criar o nosso próprio método “for_each”, que vai receber um array um bloco que será executado em cada um dos elementos do array:

def for_each(array)
  array.each do |e|
    yield e
  end
end

for_each [1,2,3,4,5] do |e|
  puts e
end

A saída desse código serão os números dentro do array. O que o “for_each” fez foi executar um yield em cada elemento do array. O yield para a execução do método (no caso “for_each”) e passa o controle para o bloco que foi passado como parâmetro pelo método.

Generalização com blocos

Vamos para mais um exemplo. Suponha que você precisa implementar uma lógica para tentar executar um mesmo método (ou qualquer pedaço de código). Digamos que você precise se conectar em um host, mas caso a conexão não possa ser estabelecida vamos tentar 3 vezes.

Ruby já tem um “retry” que pode ser utilizado dentro de um “begin/rescue”, mas não vamos utilizá-lo neste exemplo. Um código para resolver esse problema seria:

retry_count = 0
successful = false
while(!successful or (retry_count < 3))
  begin
    page = Nokogiri::HTML(open("brizeno.wordpress.com"))
    successful = true
  rescue Errno::ECONNRESET => e
    retry_count += 1
  end
end

Com esse código podemos tentar a conexão por 3 vezes. Agora, caso seja necessário aplicar essa lógica em vários lugares, será necessário quebrar um pouco mais a cabeça e garantir que o código é reutilizável.

Graças aos blocos, podemos extrair esse comportamento para um método, que vai receber como parâmetro o número de tentativas a serem realizadas e o que deve ser feito. Seria algo do tipo:

def retry_this times
  retry_count = 0
  successful = false
  while(!successful or (retry_count < times))
    begin
      yield
      successful = true
    rescue Errno::ECONNRESET => e
      retry_count += 1
    end
  end
  raise "Couldn't establish connection" unless sucessful
end

A utilização desse método seria:

retry_this 3 do
  page = Nokogiri::HTML(open("brizeno.wordpress.com"))
end

Um pouco mais sobre blocos

Repare que não é necessário explicitar que o bloco é um parâmetro do método “retry_this”. Em ruby, todo método pode executar um yield e passar o controle para um bloco, caso ele exista. E o que acontece quando um bloco não é passado?

No exemplo anterior, uma exceção seria lançada “no block given (yield) (LocalJumpError)”. Podemos resolver esse problema facilmente, basta adicionar uma outra palavra chave: “block_given?”.

def retry_this times
...
  yield if block_given?
...
end

Agora o bloco só será chamado se existir. Essa é uma boa prática para desenvolver métodos que precisam de blocos, a não ser que o objetivo realmente seja avisar que o bloco não foi passado.

Uma outra curiosidade sobre o blocos é que, no primeiro exemplo, nós fizemos uma chamada ao yield passando um parâmetro. Um bloco pode receber parâmetros utilizando os caracteres “| |”. Ao chamar o “each” nós precisamos receber um argumento: o elemento do array que está sendo iterado atualmente.

Para iterar sobre uma hash por exemplo, precisamos de dois argumentos: a chave e o valor.

my_hash.each do |key, value|
  puts "#{key} => #{value}"
end

O yield dentro do metodo each de hashes provavelmente se parece com o seguinte:

yield key, value if block_given?

Ruby possui vários métodos para iterar em um conjunto de valores, todos eles se utilizam de blocos. O “inject” por exemplo utiliza dois argumentos, um contador e o elemento do conjunto, e adiciona o retorno do bloco ao contador:

acc = [1,2,3,4,5].inject do |accumulator, number|
  accumulator +  number
end
puts acc # => 15 (1 + 2 + 3 + 4 + 5)

Martin Fowler escreveu um artigo [2] bem interessante (na categoria old but gold), sobre a facilidade que os método de iteração proporcionam e os benefícios de se utilizar lambdas. E lambdas serão o tema do próximo post!

Referências

[1] Ahsan Sharafuddin: Closures in Ruby, http://technicalecstasy.net/closures-in-ruby-blocks-procs-and-lambdas/
[2] Martin Fowler: Lambdas, http://martinfowler.com/bliki/Lambda.html

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s