Nivel inicial14 min de lectura · Publicado el

Bloques en Ruby: yield, do…end, proc y lambda

Bloques en Ruby: alguien pide «haz también esto», que es el bloque, a quien riega las plantas, el método con yield. yield: cuándo; bloque: quérubentejera.com «haz también esto»{ bloque }dice quémétodo con yieldsabe cuándo
Quien cuida tu casa es el método: con yield sabe cuándo toca. Lo que le pides, «haz también esto», es el bloque: le dice qué hacer en ese momento.

En Ruby, yield es la palabra con la que un método ejecuta el bloque que le han pasado. Un bloque es código que le das a un método para que lo ejecute cuando le convenga. Es como pedirle a quien cuida de tu casa: «cuando riegues las plantas, haz también esto». El método sabe cuándo; tú le dices qué.

Lo usas constantemente sin darte cuenta: en each, times, map o loop, lo que va entre llaves o entre do y end es un bloque.

Todo el código de este artículo se ha ejecutado con Ruby 4.0.9 (imagen oficial ruby:latest de Docker). Cada ejemplo va seguido de su salida real.

yield: cómo un método ejecuta tu bloque

yield significa «ahora le toca al bloque»: el método hace su parte y, en el punto donde escribe yield, cede el control al código que le has pasado.

def saludar
  puts "¡Hola!"
  yield
end

saludar do
  puts "Hola desde el bloque"
end
¡Hola!
Hola desde el bloque

El método puede tener sus propios parámetros y además recibir el bloque. El bloque no cuenta como parámetro: va aparte, detrás de los paréntesis.

def saludar_a(nombre)
  puts "¡Hola, #{nombre}!"
  yield
end

saludar_a("Pepe") { puts "Hola desde el bloque" }
¡Hola, Pepe!
Hola desde el bloque

Darle datos al bloque

Lo que pongas detrás de yield llega a los parámetros del bloque, los que van entre barras (|hora, nombre|), en el mismo orden:

def preparar_cita
  yield 4, "Pepe"
end

preparar_cita do |hora, nombre|
  puts "¿Qué pasó, #{nombre}? Son las #{hora}"
end
¿Qué pasó, Pepe? Son las 4

Recibir lo que devuelve el bloque

yield devuelve el valor de la última línea del bloque, así que el método puede usarlo:

def doble
  yield(21) * 2
end

p doble { |n| n }
42
Ida y vuelta de yield con el método doble: yield(21) cede el control al bloque { |n| n } y le pasa 21; en el bloque, n vale 21 y su última línea, n, devuelve 21; el método sigue con 21 * 2 y da 42.
Con yield, el control va del método al bloque con un valor y vuelve con el de su última línea.

yield varias veces

Un método puede llamar a yield tantas veces como quiera. Así funcionan por dentro each y times: recorren algo y, en cada vuelta, ceden el control al bloque con un valor distinto.

def tres_veces
  yield 1
  yield 2
  yield 3
end

tres_veces { |n| puts "Vuelta #{n}" }
Vuelta 1
Vuelta 2
Vuelta 3

Dos formas de escribir un bloque: llaves y do … end

Llaves, para una línea

contactos = { "Ana" => "600111222", "Luis" => "600333444" }
contactos.each { |nombre, telefono| puts "#{nombre}: #{telefono}" }
Ana: 600111222
Luis: 600333444

do … end, para varias líneas

loop do
  puts "¿Quieres otra vuelta? s/n"
  respuesta = gets.chomp
  break if respuesta == "n"
  puts "Volvemos a empezar"
end

Este ejemplo lee del teclado. Ejecutado respondiendo s y después n:

¿Quieres otra vuelta? s/n
Volvemos a empezar
¿Quieres otra vuelta? s/n

Los parámetros del bloque funcionan igual con las dos formas:

3.times do |vuelta|
  puts "Empiezo la vuelta #{vuelta}"
  puts "Termino la vuelta #{vuelta}"
end
Empiezo la vuelta 0
Termino la vuelta 0
Empiezo la vuelta 1
Termino la vuelta 1
Empiezo la vuelta 2
Termino la vuelta 2

La convención es usar llaves para una línea y do … end para varias. La diferencia es de estilo salvo en un detalle: las llaves se unen con más fuerza a lo que tienen al lado. Si llamas a un método sin paréntesis, do … end va al método de fuera:

puts [1, 2, 3].map { |n| n * 2 }

puts [1, 2, 3].map do |n|
  n * 2
end
2
4
6
#<Enumerator:0x00007b6fb18bdb60>

En la primera línea, las llaves van con map. En la segunda, el bloque do … end se lo lleva puts, que lo ignora, y map sin bloque devuelve un enumerador (la dirección que acompaña a Enumerator cambia en cada ejecución). Ante la duda, usa paréntesis: puts([1, 2, 3].map do |n| n * 2 end).

¿Me han pasado un bloque? block_given?

Si llamas a yield y nadie ha pasado un bloque, Ruby lanza un error:

def saludar
  yield
end

begin
  saludar
rescue LocalJumpError => error
  puts "LocalJumpError: #{error.message}"
end
LocalJumpError: no block given (yield)

Para evitarlo, pregunta antes con block_given?, que devuelve true si al método que se está ejecutando le han pasado un bloque:

def hablar(nombre)
  if block_given?
    yield(nombre)
  else
    "Soy #{nombre} y estoy hablando"
  end
end

puts hablar("Pepe")
puts hablar("Pepe") { |texto| "#{texto} habla lo que yo diga" }
Soy Pepe y estoy hablando
Pepe habla lo que yo diga

El patrón de la librería estándar: sin bloque, un enumerador

Muchos métodos de Ruby, como each o map, hacen algo útil aunque no les pases bloque: devuelven un enumerador, un objeto que recorre los valores cuando se lo pidas. Puedes hacer lo mismo en tus métodos combinando block_given? con to_enum:

def cada_par(numeros)
  return to_enum(:cada_par, numeros) unless block_given?

  numeros.each { |n| yield n if n.even? }
end

cada_par([1, 2, 3, 4]) { |n| puts "Par: #{n}" }
p cada_par([1, 2, 3, 4]).to_a
Par: 2
Par: 4
[2, 4]

Recibir el bloque como objeto: &block

Si pones & delante del último parámetro, Ruby convierte el bloque en un objeto Proc que puedes guardar, pasar a otro método o ejecutar con call. Por convención se suele llamar &block, pero el nombre es libre. Si no te han pasado bloque, vale nil:

def suma(a, b, &bloque)
  resultado = a + b
  if bloque
    bloque.call(resultado)
  else
    puts "El cálculo es #{resultado}"
  end
end

suma(4, 8) { |r| puts "El resultado es #{r}" }
suma(4, 8)
El resultado es 12
El cálculo es 12

Pasar el bloque a otro método

El uso más frecuente de &block es reenviar el bloque. Dentro del método, &bloque vuelve a convertir el objeto en bloque para el siguiente:

def registrar(mensaje, &bloque)
  puts "Antes: #{mensaje}"
  resultado = medir(&bloque)
  puts "Después: #{mensaje}"
  resultado
end

def medir
  inicio = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  valor = yield
  duracion = Process.clock_gettime(Process::CLOCK_MONOTONIC) - inicio
  puts "Ha tardado menos de un segundo: #{duracion < 1}"
  valor
end

p registrar("sumar") { 2 + 2 }
Antes: sumar
Ha tardado menos de un segundo: true
Después: sumar
4

Desde Ruby 3.1, si solo vas a reenviar el bloque, puedes dejarlo sin nombre con un & suelto:

def pasar_bloque(&)
  [1, 2, 3].map(&)
end

p pasar_bloque { |n| n * 2 }
[2, 4, 6]

Cuándo yield y cuándo &block

yield

  • Ejecuta el bloque directamente
  • Es la opción que recomienda la documentación cuando solo vas a llamarlo
  • Para la mayoría de los casos

&block

  • Convierte el bloque en un objeto Proc
  • Puedes guardarlo o pasarlo a otro método
  • Cuando el bloque tiene que viajar
El método decide cuándo; el bloque decide qué.

Bloques, procs y lambdas: la diferencia

Un bloque no es un objeto: solo existe pegado a la llamada de un método. Cuando lo recibes con &block, Ruby lo convierte en un Proc. Pero también puedes crear ese objeto directamente, sin método de por medio, y guardarlo en una variable. Hay dos sabores: proc y lambda. Los dos son de la clase Proc; lambda? te dice cuál tienes.

saludo = proc { |nombre| "Hola, #{nombre}" }
doble  = lambda { |n| n * 2 }
triple = ->(n) { n * 3 }

puts saludo.call("Ana")
puts doble.call(21)
puts triple.(5)
puts triple[5]
p saludo.lambda?
p doble.lambda?
p triple.class
Hola, Ana
42
15
15
false
true
Proc

->(n) { … } es la forma corta de escribir una lambda. Para ejecutarlos, call, .() y [] hacen lo mismo; call es la más explícita.

Según la documentación de Proc, las diferencias entre los dos sabores son dos: cómo tratan los argumentos y qué hace return.

Diferencia 1: los argumentos (la aridad)

La aridad es el número de argumentos que espera algo. Una lambda la exige como un método: si le faltan o le sobran, lanza ArgumentError. Un proc es tolerante: rellena con nil lo que falta, ignora lo que sobra y, si recibe un único array y tiene varios parámetros, lo reparte entre ellos.

mi_proc   = proc { |a, b| "a=#{a.inspect} b=#{b.inspect}" }
mi_lambda = lambda { |a, b| "a=#{a.inspect} b=#{b.inspect}" }

puts mi_proc.call(1)
puts mi_proc.call(1, 2, 3)
puts mi_proc.call([1, 2])
p mi_proc.arity
p mi_lambda.arity

begin
  mi_lambda.call(1)
rescue ArgumentError => error
  puts "ArgumentError: #{error.message}"
end
a=1 b=nil
a=1 b=2
a=1 b=2
2
2
ArgumentError: wrong number of arguments (given 1, expected 2)

Los dos dicen que esperan dos argumentos (arity es 2), pero solo la lambda lo hace cumplir. Esa tolerancia del proc es la misma que tienen los bloques: por eso contactos.each { |nombre, telefono| … } funciona aunque each le entregue cada par como un array.

Diferencia 2: qué hace return

En una lambda, return sale de la lambda y el método que la llamó sigue. En un proc, return sale del método en el que se escribió el proc, y lo que quedaba del método no se ejecuta:

def con_lambda
  comprobar = lambda { return "sale de la lambda" }
  resultado = comprobar.call
  "el método sigue (la lambda devolvió: #{resultado})"
end

def con_proc
  comprobar = proc { return "sale del método" }
  comprobar.call
  "esta línea no se ejecuta"
end

puts con_lambda
puts con_proc
el método sigue (la lambda devolvió: sale de la lambda)
sale del método

Si quieres terminar un proc o un bloque sin salir del método, usa next:

def con_proc_y_next
  comprobar = proc { next "next sale solo del proc" }
  resultado = comprobar.call
  "el método sigue (#{resultado})"
end

puts con_proc_y_next
el método sigue (next sale solo del proc)

Y si el método donde nació el proc ya ha terminado, su return no tiene adónde volver y Ruby lanza LocalJumpError:

def fabricar_proc
  proc { return "demasiado tarde" }
end

huerfano = fabricar_proc
begin
  huerfano.call
rescue LocalJumpError => error
  puts "LocalJumpError: #{error.message}"
end
LocalJumpError: unexpected return

Resumen: bloque, proc y lambda

La tabla es un resumen nuestro de la documentación de Proc y de los ejemplos de arriba:

Bloque proc lambda
Es un objeto No; se convierte en Proc con &block Sí, Proc Sí, Proc
lambda? false al convertirlo false true
Argumentos que faltan o sobran Tolerante Tolerante ArgumentError
return Sale del método donde se escribió Sale del método donde se escribió Sale solo de la lambda
Se ejecuta con yield o block.call call call

Convertir un proc o una lambda en bloque

El & también funciona al revés: delante de un proc o una lambda en una llamada, lo pasa como bloque. Y como todos son objetos que recuerdan las variables que había donde se crearon (lo que se llama un closure), pueden modificarlas:

def ejecutar_dos_veces
  yield
  yield
end

contador = 0
sumar = proc { contador += 1 }
ejecutar_dos_veces(&sumar)
p contador

p [1, 2, 3].map(&->(n) { n * 10 })
2
[10, 20, 30]

Ese «recordar» permite fabricar funciones con memoria propia:

def crear_contador
  cuenta = 0
  -> { cuenta += 1 }
end

siguiente = crear_contador
siguiente.call
siguiente.call
p siguiente.call
3

Cuándo usar cada uno

Esto es una pauta nuestra, basada en las diferencias de arriba:

  • Un bloque con yield para el caso habitual: un método que hace algo y deja que quien lo llama decida un paso.
  • &block cuando el método tiene que guardar el bloque o pasárselo a otro.
  • Una lambda cuando quieres guardar una función en una variable o en un hash y que se comporte como un método: con los argumentos comprobados y un return que no te sorprenda.
  • Un proc cuando quieres la tolerancia de un bloque. Fuera de eso, la lambda da menos sorpresas.

Bloques en métodos de Ruby que ya usas

Muchos métodos de la librería estándar aceptan un bloque opcional. Un ejemplo curioso es Array#delete: si el elemento no existe devuelve nil, pero si le pasas un bloque, devuelve lo que diga el bloque:

numeros = [1, 2, 3]
p numeros.delete(5) { "no existe" }
p numeros.delete(5)
p numeros.delete(2)
p numeros
"no existe"
nil
2
[1, 3]

Otros que verás a diario:

%w[a b c].each_index { |i| puts "#{i}.-" }
p [1, 2, 3].map { |n| n * 10 }
p [1, 2, 3].map(&:to_s)
0.-
1.-
2.-
[10, 20, 30]
["1", "2", "3"]

&:to_s es un atajo: el & convierte el símbolo :to_s en un proc que llama a ese método, así que equivale a { |n| n.to_s }.

Parámetro implícito: it (Ruby 3.4 y posteriores)

Cuando el bloque tiene un solo parámetro, desde Ruby 3.4 puedes no nombrarlo y usar it:

p [1, 2, 3].map { it * 10 }
p [1, 2, 3].map { _1 * 10 }
[10, 20, 30]
[10, 20, 30]

Desde Ruby 2.7 existe _1, que sigue funcionando. Úsalos en bloques cortos; si el bloque tiene varias líneas, un nombre claro se lee mejor.

Errores frecuentes con bloques

  • LocalJumpError: no block given (yield). Has llamado a yield sin que nadie pasara un bloque. Comprueba con block_given? o devuelve un enumerador.
  • El bloque do … end no llega a tu método. Pasa cuando la llamada no lleva paréntesis y hay otro método delante, como puts. Usa paréntesis o llaves.
  • Un return dentro de un bloque o un proc que corta el método. Si solo querías dar un valor y seguir, usa next o una lambda.
  • ArgumentError al llamar a una lambda. La lambda exige el número exacto de argumentos; un proc no.
  • LocalJumpError: unexpected return. Un proc con return que se ejecuta cuando su método ya ha terminado.

Sigue aprendiendo

Para más ejemplos de bloques con each, times y compañía, mira Bucles e iteradores en Ruby. Los métodos que reciben bloques, con sus parámetros y su return, están en Métodos en Ruby. Y map con bloque es la forma habitual de transformar listas en Arrays en Ruby.

Fuentes

Consultadas el 9 de noviembre de 2026, en la documentación oficial de Ruby 4.0:

Todos los ejemplos se han ejecutado con Ruby 4.0.9 (imagen oficial ruby:latest de Docker) y las salidas son las reales. El de loop y gets se ha ejecutado pasándole s y n por la entrada estándar.