Ruby do Zero: Blocos, yield, Proc e Lambda

Capítulo 13

Tempo estimado de leitura: 7 minutos

+ Exercício

O que é um bloco em Ruby

Blocos são trechos de código que você pode “anexar” a uma chamada de método. Eles não são objetos por padrão, mas podem ser capturados e transformados em objetos (Proc/lambda). Um bloco pode ser escrito com { ... } (mais comum em uma linha) ou com do ... end (mais legível em múltiplas linhas).

# { ... } costuma ser usado para blocos curtos
[1, 2, 3].map { |n| n * 2 }

# do ... end costuma ser usado para blocos maiores
[1, 2, 3].map do |n|
  n * 2
end

O bloco pode receber parâmetros (os “parâmetros do bloco”), declarados entre barras verticais | |. Esses parâmetros são preenchidos por quem “chama” o bloco (normalmente o método que faz yield).

Passando blocos para métodos: yield e block_given?

Um método pode executar o bloco associado usando yield. Se nenhum bloco foi passado, chamar yield levanta erro. Para evitar isso, use block_given?.

Executando um bloco com yield

def executar
  yield
end

executar { puts "rodou" }

Passando argumentos para o bloco

def com_valor
  yield(10)
end

com_valor { |n| puts n * 3 } # 30

Protegendo com block_given?

def executar_se_tiver_bloco
  return :sem_bloco unless block_given?
  yield
  :ok
end

executar_se_tiver_bloco # :sem_bloco
executar_se_tiver_bloco { puts "agora sim" } # imprime e retorna :ok

yield múltiplas vezes

Você pode chamar o bloco várias vezes, o que é útil para APIs de repetição, tentativas, medições, etc.

def duas_vezes
  yield
  yield
end

duas_vezes { puts "oi" }

Capturando o bloco como parâmetro (&block)

Além de usar yield, você pode capturar o bloco como um objeto do tipo Proc usando um parâmetro especial com &. Isso permite armazenar, passar adiante e chamar o bloco com call.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

def executar_com_call(&block)
  return :sem_bloco unless block
  block.call
  :ok
end

executar_com_call { puts "via call" }

Quando capturado, o bloco vira um Proc. Isso é útil quando você quer repassar o bloco para outro método, ou quando quer chamar em momentos diferentes.

Repassando o bloco para outro método

def camada_externa(&block)
  camada_interna(&block)
end

def camada_interna
  yield("mensagem")
end

camada_externa { |txt| puts txt.upcase }

Convertendo Proc em bloco e bloco em Proc com &

O operador & faz conversões importantes:

  • Bloco → Proc: ao capturar com &block no parâmetro do método.
  • Proc → bloco: ao passar um Proc precedido de & para um método que espera bloco.
dobrar = Proc.new { |n| n * 2 }

resultado = [1, 2, 3].map(&dobrar)
# resultado: [2, 4, 6]

Esse padrão é muito comum para reutilizar lógica de transformação/validação.

Proc e lambda: diferenças práticas

Em Ruby, tanto Proc.new quanto lambda criam objetos “chamáveis” (callables). As diferenças mais importantes no dia a dia são: aridade (como lidam com número de argumentos) e retorno (como o return se comporta).

Aridade (quantidade de argumentos)

lambda é mais rígida: exige a quantidade correta de argumentos. Proc é mais flexível: aceita menos (preenche com nil) e ignora extras em muitos casos.

p1 = Proc.new { |a, b| [a, b] }
l1 = lambda { |a, b| [a, b] }

p1.call(1)      # [1, nil]
# p1.call(1, 2, 3) geralmente não falha e ignora extra (depende do caso)

l1.call(1)      # ArgumentError: wrong number of arguments
l1.call(1, 2)   # [1, 2]

return dentro de Proc vs lambda

lambda: return retorna da própria lambda. Proc: return tenta retornar do método onde o Proc foi definido (o que pode encerrar o método “por fora” ou até gerar erro se não houver contexto adequado).

def com_lambda
  l = lambda { return :da_lambda }
  x = l.call
  :do_metodo
end

com_lambda # :do_metodo
def com_proc
  p = Proc.new { return :do_metodo }
  p.call
  :nao_chega_aqui
end

com_proc # :do_metodo

Na prática: use lambda quando quiser comportamento mais “parecido com método” (aridade rígida e retorno local). Use Proc quando quiser flexibilidade e estiver ciente do impacto do return.

Checagem rápida: lambda?

l = lambda { }
p = Proc.new { }

l.lambda? # true
p.lambda? # false

APIs simples baseadas em blocos (padrões úteis)

1) Medindo tempo de execução com bloco

Um padrão clássico é criar um método que recebe um bloco, executa e mede duração. O passo a passo é: (1) capturar o tempo inicial, (2) executar o bloco, (3) capturar o tempo final, (4) retornar duração e/ou resultado.

def medir_tempo(rotulo = "execução")
  inicio = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  resultado = yield
  fim = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  duracao = fim - inicio
  { rotulo: rotulo, segundos: duracao, resultado: resultado }
end

info = medir_tempo("soma") do
  (1..1_000_000).reduce(0) { |acc, n| acc + n }
end

# info[:segundos] -> duração
# info[:resultado] -> valor calculado

Note que o método retorna um Hash com metadados e o resultado do bloco, o que facilita uso em logs e testes.

2) Garantindo limpeza com ensure (padrão “around”)

Quando você quer garantir que algo aconteça mesmo se o bloco falhar, use ensure. Passo a passo: (1) preparar recurso, (2) executar bloco, (3) garantir liberação/limpeza.

def com_recurso(nome)
  puts "abrindo: #{nome}"
  yield
ensure
  puts "fechando: #{nome}"
end

com_recurso("arquivo.txt") do
  puts "processando..."
end

3) Validações com bloco (mini DSL)

Você pode criar uma API onde o bloco descreve regras. Um jeito simples é: (1) criar um objeto validador, (2) executar o bloco passando esse objeto, (3) coletar erros.

class Validador
  attr_reader :erros

  def initialize
    @erros = []
  end

  def exigir(condicao, mensagem)
    @erros << mensagem unless condicao
  end
end

def validar
  v = Validador.new
  yield(v)
  v
end

usuario = { nome: "", idade: 15 }

v = validar do |val|
  val.exigir(!usuario[:nome].strip.empty?, "nome é obrigatório")
  val.exigir(usuario[:idade] >= 18, "idade mínima é 18")
end

# v.erros -> ["nome é obrigatório", "idade mínima é 18"]

Esse padrão é a base para criar “mini linguagens” internas (DSLs) com blocos, mantendo o código de chamada expressivo.

4) Transformações configuráveis (pipeline com bloco opcional)

Às vezes você quer um comportamento padrão, mas permitir customização via bloco. Use block_given? para escolher entre o padrão e o customizado.

def formatar_lista(itens)
  if block_given?
    itens.map { |item| yield(item) }
  else
    itens.map { |item| item.to_s }
  end
end

formatar_lista([1, 2, 3])
# ["1", "2", "3"]

formatar_lista([1, 2, 3]) { |n| "n=#{n}" }
# ["n=1", "n=2", "n=3"]

Exercícios práticos (para treinar blocos, yield, Proc e lambda)

Exercício 1: timer com retorno do bloco

Crie um método timer que receba um rótulo e um bloco. Ele deve retornar um Hash com: :rotulo, :ms (milissegundos) e :resultado (o retorno do bloco). Requisitos: usar Process.clock_gettime e yield.

# Assinatura sugerida
# def timer(rotulo = "tarefa")
#   ...
# end

Exercício 2: método retry simples baseado em bloco

Implemente tentar(vezes) que executa o bloco e, se ocorrer uma exceção, tenta novamente até acabar. Ao final, se não conseguir, relança a última exceção. Dica: use begin/rescue e um contador.

# Exemplo de uso esperado:
# tentar(3) do
#   pode_falhar
# end

Exercício 3: validação com DSL

Expanda o Validador para ter métodos: exigir_presenca(valor, campo) e exigir_minimo(numero, minimo, campo). Faça o bloco ficar assim:

# validar do |v|
#   v.exigir_presenca(usuario[:nome], :nome)
#   v.exigir_minimo(usuario[:idade], 18, :idade)
# end

Exercício 4: Proc vs lambda na prática

Crie dois callables: um Proc e uma lambda, ambos esperando dois argumentos. Teste chamadas com 1 e com 3 argumentos e registre o comportamento. Depois, crie um método executar_callable(callable) que chama o objeto com call e trate o caso de ArgumentError para a lambda.

# Sugestão:
# def executar_callable(callable)
#   callable.call(1)
# rescue ArgumentError => e
#   e.message
# end

Exercício 5: aceitando Proc como bloco

Crie um método aplicar(itens, &transformacao) que aplica a transformação em cada item. Depois, crie uma transformação como Proc e passe com &.

# Exemplo:
# up = Proc.new { |s| s.upcase }
# aplicar(["a", "b"], &up) # ["A", "B"]

Agora responda o exercício sobre o conteúdo:

Ao transformar um Proc em bloco usando & (por exemplo, em um método como map), qual comportamento está correto?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O operador & permite converter um Proc em bloco ao passá-lo para um método que espera bloco, possibilitando reutilizar a transformação definida e executá-la dentro do método.

Próximo capitúlo

Ruby do Zero: Iteradores e Código Expressivo com Blocos

Arrow Right Icon
Capa do Ebook gratuito Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código
62%

Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.