Função Anônima ou Closure? Aprenda como blocos no Ruby funcionam

em Artigos, Desenvolvimento, Ruby.

Para começar, podemos dizer que blocos são funções anônimas, ou seja, um trecho de código destinado a realizar algo, mas que não possui um nome ou definição. Tal recurso é geralmente utilizado quando escrevemos algum código que será utilizado apenas uma vez, poucas vezes ou em um contexto específico.

É um código que não será reutilizado, então não vemos sentido em criar uma função nomeada, definir um método, incluí-lo em uma classe, ocupar espaço em memória com a sua definição e coisas do tipo.

Para entender melhor, vamos explorar o que blocos são na prática.

Como Funcionam os Blocos no Ruby

Blocos são uma forma de passar pedaços de código durante a execução de algo. É muito comum ver, por exemplo:

numbers = [1, 2, 3, 4]

numbers.each do |number|
  puts number
end

Tal código escreverá na tela:

1
2
3
4

Para quem está acostumado com outras linguagens, uma comparação para ajudar a entender o que está acontecendo, seria este exemplo em JavaScript:

var numbers = [1, 2, 3, 4];

numbers.forEach(function(number) {
  console.log(number);
});

Uma característica comum em funções anônimas, é que elas podem ser escritas em uma única linha, o que no Ruby ficaria:

[1, 2, 3, 4].each { |number| puts number }

Um exemplo bem simples de utilização seria somar todos os números dentro de uma array:

total = 0

numbers = [20, 10, 30]

numbers.each { |number| total += number }

puts total

Que escreverá na tela:

60

Isto, fazendo uma última comparação, seria em JavaScript o equivalente a:

var total = 0;

var numbers = [20, 10, 30];

numbers.forEach(function(number) { total += number; });

console.log(total);

Algo interessante é que podemos passar blocos como variáveis de um método. Se quisermos por exemplo um método que ordene uma array alfabéticamente e depois faça algo customizado com cada item da array de acordo com o bloco passado, podemos utilizar o yield:

def sort_and_then(my_list)
  my_list = my_list.sort

  my_list = my_list.map do |value|
    yield(value)
  end

  puts my_list
end

fruits = ['Uva', 'Abacate', 'Banana']

sort_and_then(fruits) do |value|
  value.upcase
end

Tal código escreverá:

ABACATE
BANANA
UVA

Neste caso, o método sort_and_then espera que um bloco sempre seja passado. Se quisermos deixar isto opcional, podemos adicionar uma verificação com o block_given?, ficando assim:

def sort_and_then(my_list)
  my_list = my_list.sort

  if block_given?
    my_list.each { |value| puts yield(value) }
  end
end

fruits = ['Uva', 'Abacate', 'Banana']

sort_and_then(fruits)

Perceba que quando o método é chamado sem o bloco não haverá problemas, resultando em:

Abacate
Banana
Uva

Com este código, podemos sempre mudar o bloco passado. Se quisermos ver apenas a primeira letra de cada fruta na array, poderíamos chamá-lo da seguinte maneira:

sort_and_then(fruits) do |fruit|
  fruit[0]
end

Resultando em:

A
B
U

Uma outra forma de utilizar blocos como parâmetros além do yield é nomeando o parâmetro com o identificador &. Desta forma utilizamos o método .call para executar o bloco, deixando o código assim:

def sort_and_then(my_list, &custom_block)
  my_list = my_list.sort

  if block_given?
    my_list = my_list.map do |value|
      custom_block.call(value)
    end
  end

  puts my_list
end

Um detalhe importante desta abordagem, é que o parâmetro com & (que poderá receber o bloco) deve ser sempre o último do método. Isto é necessário, pois caso contrário seria muito estranho tentar utilizar o do para passar o bloco antes do final da chamada, não daria muito certo tentar fazer algo como:

# bad idea!
def sort_and_then(my_list, &custom_block, other_param)
  # ...
end

fruits = ['Uva', 'Abacate', 'Banana']

sort_and_then(fruits) do |fruit|
  fruit.upcase
end, 'param 3'

sort_and_then(fruits) { |fruit| fruit.upcase}, 'param 3'

Por isso a regra é clara, se quiser nomear o bloco recebido com &, ele será sempre o último parâmetro do método e podemos receber apenas um bloco por método.

Resumindo, podemos chamar o nosso método que recebe blocos com trechos de códigos das seguintes maneiras:

fruits = ['Uva', 'Abacate', 'Banana']

sort_and_then(fruits) do |fruit|
  fruit.upcase
end

sort_and_then(fruits) { |fruit| fruit.upcase}

Se você utiliza Ruby on Rails, provavelmente já precisou criar helpers para manipular o HTML de suas views.

Uma forma interessante de usar blocos neste cenário seria criar um método para envolver um trecho de HTML com a utilização do capture.

Você poderia criar em seu helper um método como:

def my_custom_box(id, &block)
  html  = "<div class='my-box' id='#{id}'>"
  html += capture(&block)
  html += '</div>'
  html.html_safe
end

E na sua view utilizá-lo da seguinte maneira:

<%= my_custom_box('my-id') do %>
  <h1>My Title</h1>
<% end %>

Tal código resultaria no seguinte HTML:

<div class='my-box' id='my-id'>
  <h1>My Title</h1>
</div>

E por aí vai. Existem infinitas aplicações para blocos e com certeza você vai usá-los muito sempre que programar no Ruby.

Então Blocos não são Closures?

Como vimos, blocos são funções anônimas. Porém, isto não quer dizer que blocos não podem ser closures também. Closures são funções que conseguem acessar e manipular variáveis externas, presentes em sua função exterior.

Vamos analisar o seguinte exemplo:

total = 0

def add_to_total(value)
  total += value
end

add_to_total(20)

puts total

Tal código gera o erro “undefined method `+' for nil:NilClass”. O motivo deste erro é que o método add_to_total não possui acesso à variável total. Ou seja, neste cenário, add_to_total não é uma closure.

E se passarmos a variável total para ele? Vejamos:

total = 0

def add_to_total(total, value)
  total += value
end

add_to_total(total, 20)

add_to_total(total, 10)

puts total

O erro não acontecerá mais, porém o resultado final ao imprimir total será 0. Ele recebeu a variável, mas a manipulou dentro do seu contexto, sem influenciar a variável externa. Novamente, isto mostra que não podemos chamar o add_to_total de closure.

Closures conseguem manipular variáveis externas presentes nas funções que a executam. E blocos conseguem fazer isso? Vamos alterar o código para utilizar um bloco e descobrir:

def add_to_total(value, &my_block)
  my_block.call(value)
end

add_to_total(20) { |value| total += value }

add_to_total(10) { |value| total += value }

puts total

O resultado? 30. Então sim, blocos conseguem acessar e manipular variáveis fora do seu contexto, logo, blocos são Closures também.

Concluindo

Blocos são Funções Anônimas e Closures, pois atendem à definição de ambas as coisas.

Eles são amplamente utilizados no Ruby e se bem aplicados podem colaborar muito para deixar o seu código mais versátil e legível.

Blocos são simples e algo que você não pode fazer com eles por exemplo é associá-los à uma variável:

my_block = { puts 'test' }

Tal código irá gerar um erro "(syntax error, unexpected tSTRING_BEG)".

Porém, isto é possível quando passamos a utilizar Procs ou Lambdas que são outras formas de se trabalhar com blocos no Ruby. Podemos falar sobre elas em um outro post. =)

E é isso, esperto ter ajudado a esclarecer este assunto.

Um último aviso: Utilize blocos com moderação, de forma ponderada. Não é legal ver por aí códigos assim, com infinitos blocos dentro de outros blocos:

fruits = ['Uva é Roxa', 'Abacate é Verde', 'Banana é Amarela']

def check_fruits(fruits)
  fruits.each do |fruit|
    words = fruit.split(' ')
    words.each do |word|
      letters = word.split('')

      letters.each do |letter|
        ('a'..'z').each do |letter_to_check|
          if letter.casecmp(letter_to_check) == 0
            puts "#{letter} is eql #{letter_to_check}!"
          end
        end
      end
    end
  end
end

check_fruits(fruits)

O rubocop e talvez algumas pessoas podem ficar ofendidas. ;)

Você também pode gostar