Carregando
Todos os artigos
Programação 4 min de leitura

Blocos e Iteradores em Ruby: Crie o Seu

Entenda como métodos Ruby recebem blocos, passam valores com yield e devolvem um Enumerator quando não há bloco. Crie e execute seu próprio iterador.

nbkhope
Nesta página

Blocos e Iteradores em Ruby: Crie o Seu

Você talvez já tenha usado numeros.each { |numero| puts numero }. Mas o que acontece dentro do método? Como each entrega um número ao seu bloco? E se ninguém passar um bloco?

Vamos criar um iterador pequeno para enxergar os dois lados. Se each, map e select ainda forem novidade, comece pelo guia de arrays em Ruby. Os exemplos abaixo foram executados com Ruby 3.3.

Um método Ruby entrega valores ao bloco de quem o chamou e devolve um Enumerator quando não recebe um bloco.

O método escolhe quando executar o bloco

Um bloco é um trecho de código ligado à chamada de um método. Os parâmetros entre | recebem valores do método. Dentro dele, yield chama o bloco:

1def duas_vezes
2  yield 1
3  yield 2
4end
5
6duas_vezes do |numero|
7  puts "Visita #{numero}"
8end
Visita 1
Visita 2

Quem chama fornece o bloco; duas_vezes controla quando ele roda e quais valores recebe. do ... end funciona bem para várias linhas. Para uma chamada curta, também podemos usar chaves: duas_vezes { |numero| puts numero }.

O valor de uma expressão yield é o retorno do bloco. Um método pode aproveitar esse valor, como neste pequeno transformador:

1def transformar_dois
2  [yield(1), yield(2)]
3end
4
5p transformar_dois { |numero| numero * 10 }
[10, 20]

Essa é a ideia por trás de transformações como map: o método reúne os valores devolvidos pelo bloco. Um iterador também pode apenas entregar valores e ignorar os retornos do bloco.

Crie um iterador para notas aprovadas

Imagine um relatório que precise visitar somente notas a partir de um mínimo escolhido. cada_aprovada decide quais notas entregar ao bloco:

1def cada_aprovada(notas, minimo)
2  return enum_for(__method__, notas, minimo) unless block_given?
3
4  notas.each do |nota|
5    yield nota if nota >= minimo
6  end
7end
8
9notas = [48, 82, 67, 91]
10cada_aprovada(notas, 70) { |nota| puts "Aprovada: #{nota}" }
Aprovada: 82
Aprovada: 91

Leia o método de cima para baixo:

  1. block_given? pergunta se a chamada recebeu um bloco.
  2. Com um bloco, notas.each percorre a entrada em ordem. A condição decide se a nota chega ao yield.
  3. O bloco de quem chamou imprime cada nota recebida. O método não reúne os valores retornados pelo bloco. Com um bloco, esta implementação devolve notas, pois notas.each devolve o próprio array.
Fluxo do Ruby: quem chama invoca cada_aprovada, o método testa uma nota e passa 82 ao bloco com yield; depois a execução volta ao método. Sem bloco, ele devolve um Enumerator.

A seta de volta importa: depois que o bloco roda para 82, cada_aprovada retoma o laço e pode entregar 91. O bloco não assume toda a iteração; o método continua decidindo quais valores chegam e quando.

minimo é um argumento comum do método. O bloco acrescenta o comportamento escolhido por quem o chamou. O método e o array não precisam saber se as notas serão impressas, contadas ou guardadas.

Sem bloco? Devolva algo útil

Sem a primeira linha de cada_aprovada, chegar a yield sem bloco causaria LocalJumpError. Em vez disso, podemos devolver um Enumerator, para que a pessoa escolha depois como consumir os valores:

1aprovadas = cada_aprovada([48, 82, 67, 91], 70)
2
3p aprovadas.class
4p aprovadas.to_a
5p aprovadas.first
Enumerator
[82, 91]
82

enum_for(__method__, notas, minimo) cria um enumerador para este método e guarda seus argumentos. Quando to_a ou first pede valores, o enumerador chama cada_aprovada com um bloco. Ele não monta o array filtrado no momento da atribuição a aprovadas. Cada consumo executa o método novamente; portanto, mudanças no array original podem alterar resultados futuros.

Essa interface com duas formas de uso—entregar valores a um bloco ou devolver um enumerador—é comum nos métodos de coleções do Ruby. Se o seu método exigir um bloco, também é possível retirar a verificação e documentar essa exigência. O importante é escolher conscientemente.

Experimente

Escreva cada_palavra_longa(palavras, tamanho_minimo). O método deve entregar palavras com pelo menos esse tamanho e devolver um enumerador quando for chamado sem bloco. Teste cada_palavra_longa(%w[sol lontra elefante], 6).to_a antes de abrir a resposta.

<details> <summary>Uma solução</summary>
1def cada_palavra_longa(palavras, tamanho_minimo)
2  return enum_for(__method__, palavras, tamanho_minimo) unless block_given?
3
4  palavras.each do |palavra|
5    yield palavra if palavra.length >= tamanho_minimo
6  end
7end
8
9p cada_palavra_longa(%w[sol lontra elefante], 6).to_a
["lontra", "elefante"]
</details>

A ideia central é simples: o bloco pertence a quem chama, e o método escolhe quando executá-lo. Depois de criar um método assim, os iteradores prontos do Ruby ficam muito mais fáceis de entender.

Para saber mais

Obrigado pela leitura!

Conhece alguém que vai gostar? Mande para essa pessoa.

Mais artigos

Leia a seguir

Programação 5 min de leitura

Como Tratar Erros em Go: Um Guia Prático

Aprenda a tratar erros em Go com exemplos executáveis: retorne um erro, confira o resultado, acrescente contexto com %w e reconheça um arquivo ausente com errors.Is.

nbkhope

Comentários

Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.