Iteradores idiomáticos para código mais expressivo
Quando você já domina each, map e select, o próximo passo é escrever iterações que carregam mais intenção no próprio código. Em Ruby, isso costuma significar: (1) escolher o iterador que “diz” o que você quer fazer, (2) usar blocos curtos e legíveis, e (3) extrair regras para métodos com nomes claros quando a lógica cresce.
Quando usar cada iterador (visão prática)
each_with_index: quando você precisa do elemento e também da posição (índice) para formatar, numerar, validar ou cruzar com outra estrutura.each_with_object(obj): quando você quer construir/acumular um objeto (hash, array, string, set) sem depender de variável externa e sem retornar um array intermediário.inject/reduce: quando você quer reduzir uma coleção a um único valor (soma, produto, hash agregado, string final, estatística). O acumulador é explícito e o retorno é o valor final.
each_with_index: índice sem “gambiarras”
Evite manter um contador manual fora do bloco quando o índice é parte do problema. each_with_index entrega o elemento e o índice de forma direta.
Exemplo: numerar itens para exibição
Estilo imperativo (contador manual):
itens = ["Arroz", "Feijão", "Macarrão"]
linha = 1
itens.each do |item|
puts "#{linha}. #{item}"
linha += 1
endEstilo idiomático com each_with_index:
itens = ["Arroz", "Feijão", "Macarrão"]
itens.each_with_index do |item, index|
puts "#{index + 1}. #{item}"
endGanho de clareza: não existe estado externo (linha) para manter consistente; o índice vem do próprio iterador.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Passo a passo: validando posições específicas
Imagine que você precisa validar que nenhum item em posição par (2, 4, 6...) esteja vazio.
- Use
each_with_indexpara obter a posição. - Converta o índice (0-based) para posição humana (1-based) quando necessário.
- Faça a regra ficar explícita no bloco.
campos = ["Ana", "", "Carlos", ""]
campos.each_with_index do |valor, index|
posicao = index + 1
proxima_se_par = (posicao % 2).zero?
if proxima_se_par && valor.strip.empty?
raise "Campo na posição #{posicao} não pode ser vazio"
end
endeach_with_object: acumular sem variável externa
Um padrão comum é construir um hash/array a partir de uma coleção. Você pode fazer isso com each e uma variável externa, mas each_with_object torna o “objeto acumulador” parte do iterador, reduzindo efeitos colaterais e deixando a intenção mais evidente.
Exemplo: indexar usuários por e-mail
Imperativo (variável externa):
usuarios = [
{ nome: "Ana", email: "ana@ex.com" },
{ nome: "Bia", email: "bia@ex.com" }
]
por_email = {}
usuarios.each do |u|
por_email[u[:email]] = u
endIdiomaticamente com each_with_object:
usuarios = [
{ nome: "Ana", email: "ana@ex.com" },
{ nome: "Bia", email: "bia@ex.com" }
]
por_email = usuarios.each_with_object({}) do |u, acc|
acc[u[:email]] = u
endGanho de clareza: o acumulador (acc) é explícito, e o método retorna o próprio objeto acumulado (não um array de retornos do bloco).
Exemplo: construir uma lista filtrada e transformada em um único passo
Às vezes você quer filtrar e transformar, mas sem criar arrays intermediários (ou sem encadear muitos métodos). each_with_object é útil quando a regra fica mais clara assim.
entradas = [" ana ", "", "bia", " ", "carlos"]
nomes = entradas.each_with_object([]) do |texto, acc|
nome = texto.strip
next if nome.empty?
acc << nome.capitalize
end
# nomes => ["Ana", "Bia", "Carlos"]inject/reduce: reduzindo para um único valor
inject (também conhecido como reduce) percorre a coleção carregando um acumulador. A cada elemento, você devolve o novo valor do acumulador. No final, o retorno é o acumulador final.
Modelo mental do reduce
| Parte | Significado |
|---|---|
| Valor inicial | O acumulador começa com esse valor (ex.: 0 para soma, 1 para produto, {} para hash). |
|acc, item| | acc é o acumulador atual; item é o elemento da vez. |
| Retorno do bloco | Vira o próximo acc. |
Exemplo: soma (comparando estilos)
Imperativo:
valores = [10, 20, 30]
total = 0
valores.each do |v|
total += v
endCom reduce:
valores = [10, 20, 30]
total = valores.reduce(0) do |acc, v|
acc + v
endCom atalho de símbolo (quando for simples):
total = valores.reduce(0, :+)Passo a passo: total de carrinho com regra
Vamos calcular o total de um carrinho, aplicando desconto de 10% apenas em itens acima de 100.
- Defina o acumulador inicial como
0.0(evita divisão inteira e deixa o tipo claro). - Para cada item, calcule o subtotal com a regra.
- Some no acumulador e retorne o novo acumulador.
itens = [
{ nome: "Teclado", preco: 120.0 },
{ nome: "Mouse", preco: 80.0 },
{ nome: "Monitor", preco: 900.0 }
]
total = itens.reduce(0.0) do |acc, item|
preco = item[:preco]
subtotal = preco > 100 ? (preco * 0.9) : preco
acc + subtotal
endreduce para construir hashes (com cuidado)
É possível usar reduce para construir um hash, mas muitas vezes each_with_object fica mais legível. Se usar reduce, deixe o valor inicial explícito e retorne o acumulador.
pares = [[:a, 1], [:b, 2], [:c, 3]]
hash = pares.reduce({}) do |acc, (chave, valor)|
acc[chave] = valor
acc
endNote a linha acc no final: ela garante que o bloco devolve o acumulador, não o valor atribuído.
Padrões de legibilidade em blocos
1) Nomes claros para variáveis de bloco
Blocos com |x| e |y| funcionam, mas custam leitura. Prefira nomes que expliquem o papel do valor.
# Menos legível
pedidos.each do |p|
puts p[:id]
end
# Mais legível
pedidos.each do |pedido|
puts pedido[:id]
endPara acumuladores, use nomes como acc, total, resultado, por_email, contagem, dependendo do contexto.
2) Evitar blocos longos (regra prática)
Se o bloco começa a ter muitas variáveis temporárias, múltiplos if e comentários para explicar, é um sinal de que a lógica merece um método com nome.
Bloco longo (difícil de escanear):
linhas.each_with_object([]) do |linha, acc|
texto = linha.strip
next if texto.empty?
colunas = texto.split(",")
next if colunas.size < 3
nome = colunas[0].strip
email = colunas[1].strip.downcase
idade = colunas[2].to_i
next if nome.empty? || email.empty?
next if idade < 18
acc << { nome: nome, email: email, idade: idade }
end3) Extração para métodos: bloco curto, intenção alta
Extraia regras para métodos com nomes que descrevem o “porquê”, não o “como”. O iterador fica responsável apenas por orquestrar.
def parse_linha_csv(linha)
texto = linha.strip
return nil if texto.empty?
colunas = texto.split(",")
return nil if colunas.size < 3
nome = colunas[0].strip
email = colunas[1].strip.downcase
idade = colunas[2].to_i
return nil if nome.empty? || email.empty?
return nil if idade < 18
{ nome: nome, email: email, idade: idade }
end
usuarios = linhas.each_with_object([]) do |linha, acc|
usuario = parse_linha_csv(linha)
acc << usuario if usuario
endAgora o bloco tem uma única responsabilidade: “tentar parsear e acumular quando válido”. A regra de parsing/validação está encapsulada e pode ser testada isoladamente.
Reescrevendo loops imperativos para estilo iterativo/funcional
Padrão 1: construir um hash de contagem
Imperativo:
palavras = %w[ruby ruby bloco iterador ruby]
contagem = {}
palavras.each do |palavra|
contagem[palavra] ||= 0
contagem[palavra] += 1
endCom each_with_object (mesma ideia, menos estado externo):
palavras = %w[ruby ruby bloco iterador ruby]
contagem = palavras.each_with_object(Hash.new(0)) do |palavra, acc|
acc[palavra] += 1
endPadrão 2: encontrar o “melhor” elemento (máximo por critério)
Às vezes você vê um loop que mantém “o melhor até agora”. Dá para expressar com reduce deixando o critério explícito.
Imperativo:
produtos = [
{ nome: "A", preco: 10 },
{ nome: "B", preco: 25 },
{ nome: "C", preco: 20 }
]
mais_caro = nil
produtos.each do |p|
if mais_caro.nil? || p[:preco] > mais_caro[:preco]
mais_caro = p
end
endCom reduce:
mais_caro = produtos.reduce do |melhor, atual|
atual[:preco] > melhor[:preco] ? atual : melhor
endGanho de clareza: a variável melhor comunica a intenção; o bloco descreve a regra de comparação.
Padrão 3: transformar e acumular em string final
Imperativo:
nomes = ["ana", "bia", "carlos"]
saida = ""
nomes.each_with_index do |nome, i|
saida += "- "
saida += (i + 1).to_s
saida += ": "
saida += nome.capitalize
saida += "\n"
endCom reduce (acumulando texto):
nomes = ["ana", "bia", "carlos"]
saida = nomes.each_with_index.reduce("") do |acc, (nome, i)|
acc + "- #{i + 1}: #{nome.capitalize}\n"
endObservação: para grandes volumes de texto, acumular com String pode ser custoso; nesse caso, prefira acumular em array e depois join. A escolha do iterador deve equilibrar clareza e custo.
Checklist rápido para blocos expressivos
- O iterador escolhido descreve a intenção? (
each_with_index,each_with_object,reduce) - Os nomes do bloco explicam o papel dos valores? (
pedido,usuario,acc,total) - O bloco cabe “na tela” sem rolagem mental? Se não, extraia para método.
- O bloco retorna o que você espera? Em
reduce, o retorno do bloco vira o próximo acumulador. - Evite misturar muitas responsabilidades no mesmo bloco (parse + validar + persistir + logar).