10. Bloques y Procs

Esta es, definitivamente, una de las características más geniales de Ruby. Algunos otros lenguajes tienen esta característica, aunque pueden llamarla de otra manera (como clausuras o closures), pero muchos de los más populares no, lo cual es una lástima.

Entonces, ¿qué es esta cosa nueva y genial? Es la habilidad de tomar un bloque de código (código entre do y end), envolverlo en un objeto (llamado un proc), almacenarlo en una variable o pasarlo a un método, y ejecutar el código del bloque cuando quieras (más de una vez, si quieres). Así que es como un método, excepto que no está ligado a un objeto (esto es un objeto), y puedes almacenarlo o pasarlo como cualquier otro objeto. Creo que es hora de un ejemplo:

brindis = Proc.new do
  puts '¡Salud!'
end

brindis.call
brindis.call
brindis.call

Creé un proc (creo que es la abreviatura de "procedimiento") que contiene un bloque de código, y luego llamé (call) al proc tres veces. Como puedes ver, se parece mucho a un método.

De hecho, es aún más parecido a un método de lo que te he mostrado, porque los bloques pueden recibir parámetros:

teGusta = Proc.new do |unaCosaBuena|
  puts '¡Me gusta *realmente* '+unaCosaBuena+'!'
end

teGusta.call 'el chocolate'
teGusta.call 'ruby'

Bien, entonces vemos qué son los bloques y procs, y cómo usarlos, pero ¿cuál es el punto? ¿Por qué no usar simplemente métodos? Bueno, es porque hay algunas cosas que simplemente no puedes hacer con métodos. En particular, no puedes pasar métodos a otros métodos (pero puedes pasar procs a métodos), y los métodos no pueden devolver otros métodos (pero pueden devolver procs). Esto es simplemente porque los procs son objetos; los métodos no.

(Por cierto, ¿te resulta familiar algo de esto? Sí, has visto bloques antes... cuando aprendiste sobre iteradores. Pero hablemos más sobre eso en un momento.)

Métodos que Reciben Procs

Cuando pasamos un proc a un método, podemos controlar cómo, si, o cuántas veces llamamos al proc. Por ejemplo, digamos que hay algo que queremos hacer antes y después de que se ejecute algún código:

def hacerCosaImportante unProc
  puts '¡Todo el mundo ESPERE! Tengo algo que hacer...'
  unProc.call
  puts 'Ok todos, he terminado. Continúen.'
end

diHola = Proc.new do
  puts 'hola'
end

diAdios = Proc.new do
  puts 'adiós'
end

hacerCosaImportante diHola
hacerCosaImportante diAdios

Tal vez eso no suene tan fabuloso... pero lo es. :-) Es muy común en programación tener requisitos estrictos sobre cosas que deben hacerse. Si quieres guardar un archivo, por ejemplo, tienes que abrir el archivo, escribir la información que deseas y luego cerrar el archivo. Si olvidas cerrar el archivo, pueden suceder Cosas Malas(tm). Pero cada vez que quieras guardar o cargar un archivo, tienes que hacer lo mismo: abrir el archivo, hacer lo que realmente quieres hacer con él y luego cerrarlo. Es tedioso y fácil de olvidar. En Ruby, guardar (o cargar) archivos funciona de manera similar al código anterior, por lo que no tienes que preocuparte por nada más que lo que quieres guardar (o cargar). (En el próximo capítulo te mostraré cómo hacer cosas como guardar y cargar archivos.)

También puedes escribir métodos que determinen cuántas veces, o incluso si, llamar a un proc. Aquí hay un método que llama a un proc aproximadamente la mitad de las veces, y otro que lo llama dos veces:

def talVezHacer unProc
  if rand(2) == 0
    unProc.call
  end
end

def hacerDosVeces unProc
  unProc.call
  unProc.call
end

guino = Proc.new do
  puts '<guiño>'
end

mirada = Proc.new do
  puts '<mirada>'
end

talVezHacer guino
talVezHacer mirada
hacerDosVeces guino
hacerDosVeces mirada

(Si recargas esta página varias veces, verás que la salida cambia.) Estos son algunos de los usos más comunes de los procs, que nos permiten hacer cosas que simplemente no podríamos hacer usando solo métodos. Claro, podrías escribir un método para guiñar dos veces, ¡pero no podrías escribir uno para hacer cualquier cosa dos veces!

Antes de continuar, veamos un último ejemplo. Hasta ahora, los procs que hemos pasado han sido bastante similares entre sí. Esta vez serán bastante diferentes, para que puedas ver cuánto depende un método de los procs que se le pasan. Nuestro método tomará un objeto y un proc, y llamará al proc en ese objeto. Si el proc devuelve falso, salimos; de lo contrario, llamamos al proc con el objeto devuelto. Seguimos haciendo esto hasta que el proc devuelva falso (lo cual es mejor que haga eventualmente, o el programa se bloqueará). El método devolverá el último valor no falso devuelto por el proc.

def hacerHastaFalso primeraEntrada, unProc
  entrada = primeraEntrada
  salida  = primeraEntrada

  while salida
    entrada = salida
    salida  = unProc.call entrada
  end

  entrada
end

construirArrayDeCuadrados = Proc.new do |array|
  ultimoNumero = array.last
  if ultimoNumero <= 0
    false
  else
    array.pop                         # Quita el último número...
    array.push ultimoNumero*ultimoNumero  # ...y reemplázalo con su cuadrado...
    array.push ultimoNumero-1           # ...seguido por el siguiente número menor.
  end
end

siempreFalso = Proc.new do |soloIgnorame|
  false
end

puts hacerHastaFalso([5], construirArrayDeCuadrados).inspect
puts hacerHastaFalso('Estoy escribiendo esto a las 3:00 am; ¡alguien noquéeme!', siempreFalso)

Vale, ese fue un ejemplo bastante extraño, lo admito. Pero muestra cuán diferente actúa nuestro método cuando se le dan diferentes procs.

El método inspect es muy parecido a to_s, excepto que la cadena que devuelve intenta mostrarte el código ruby para construir el objeto que le pasaste. Aquí nos muestra todo el array devuelto por nuestra primera llamada a hacerHastaFalso. También podrías notar que nunca elevamos al cuadrado ese 0 al final de ese array, pero dado que 0 al cuadrado sigue siendo solo 0, no tuvimos que hacerlo. Y dado que siempreFalso era, ya sabes, siempre falso, hacerHastaFalso no hizo nada en absoluto la segunda vez que lo llamamos; simplemente devolvió lo que se le pasó.

Métodos que Devuelven Procs

Una de las otras cosas geniales que puedes hacer con procs es crearlos en métodos y devolverlos. Esto permite todo tipo de poder de programación loco (cosas con nombres impresionantes como evaluación perezosa, estructuras de datos infinitas y currying), pero el hecho es que casi nunca hago esto en la práctica, ni recuerdo haber visto a nadie más hacerlo en su código. Creo que es el tipo de cosa que simplemente no terminas haciendo en Ruby, o tal vez Ruby simplemente te anima a encontrar otras soluciones; no lo sé. En cualquier caso, solo tocaré esto brevemente.

En este ejemplo, componer toma dos procs y devuelve un nuevo proc que, cuando se llama, llama al primer proc y pasa su resultado al segundo proc.

def componer proc1, proc2
  Proc.new do |x|
    proc2.call(proc1.call(x))
  end
end

cuadrado = Proc.new do |x|
  x * x
end

doble = Proc.new do |x|
  x + x
end

dobleLuegoCuadrado = componer doble, cuadrado
cuadradoLuegoDoble = componer cuadrado, doble

puts dobleLuegoCuadrado.call(5)
puts cuadradoLuegoDoble.call(5)

Nota que la llamada a proc1 tuvo que estar dentro de los paréntesis para proc2 para que se hiciera primero.

Pasando Bloques (No Procs) a Métodos

Bien, esto ha sido algo académicamente interesante, pero también un poco complicado de usar. Gran parte del problema es que hay tres pasos por los que tienes que pasar (definir el método, hacer el proc y llamar al método con el proc), cuando se siente que solo debería haber dos (definir el método y pasar el bloque directamente al método, sin usar un proc en absoluto), ya que la mayoría de las veces no quieres usar el proc/bloque después de pasarlo al método. Bueno, ¡Ruby lo tiene todo resuelto para nosotros! De hecho, ya lo has estado haciendo cada vez que usas iteradores.

Te mostraré un ejemplo rápido y luego hablaremos de ello.

class Array

  def cadaPar(&eraUnBloque_ahoraUnProc)
    esPar = true  # Empezamos con "true" porque los arrays empiezan con 0, que es par.

    self.each do |objeto|
      if esPar
        eraUnBloque_ahoraUnProc.call objeto
      end

      esPar = !esPar  # Cambiar de par a impar, o de impar a par.
    end
  end

end

['manzana', 'manzana podrida', 'cereza', 'durián'].cadaPar do |fruta|
  puts '¡Yum! Me encantan los pasteles de '+fruta+', ¿a ti no?'
end

# Recuerda, estamos obteniendo los elementos numerados pares
# del array, todos los cuales resultan ser números impares,
# solo porque me gusta causar problemas así.
[1, 2, 3, 4, 5].cadaPar do |bichoRaro|
  puts bichoRaro.to_s+' ¡NO es un número par!'
end

Para pasar un bloque a cadaPar, todo lo que tuvimos que hacer fue pegar el bloque después del método. Puedes pasar un bloque a cualquier método de esta manera, aunque muchos métodos simplemente ignorarán el bloque. Para hacer que tu método no ignore el bloque, sino que lo agarre y lo convierta en un proc, pon el nombre del proc al final de la lista de parámetros de tu método, precedido por un ampersand (&). Esa parte es un poco truculenta, pero no demasiado mala, y solo tienes que hacerlo una vez (cuando defines el método). Luego puedes usar el método una y otra vez, al igual que los métodos integrados que toman bloques, como each y times. (¿Recuerdas 5.times do...?)

Si te confundes, solo recuerda lo que se supone que debe hacer cadaPar: llamar al bloque pasado para cada otro elemento en el array. Una vez que lo hayas escrito y funcione, no necesitas pensar en lo que realmente está haciendo bajo el capó ("¿¿qué bloque se llama cuándo??"); de hecho, esa es exactamente la razón por la que escribimos métodos como este: para que nunca tengamos que pensar en cómo funcionan de nuevo. Simplemente los usamos.

Recuerdo que una vez quise cronometrar cuánto tiempo tomaban diferentes secciones de mi código (esto se conoce como perfilado o profiling del código). Así que escribí un método que toma el tiempo antes de ejecutar el código, lo ejecuta, toma el tiempo nuevamente al final y devuelve la diferencia. No puedo encontrar el código ahora mismo, pero no lo necesito; probablemente se veía algo así:

def perfil descripcionDelBloque, &bloque
  horaInicio = Time.now

  bloque.call

  duracion = Time.now - horaInicio

  puts descripcionDelBloque+': '+duracion.to_s+' segundos'
end

perfil '25000 duplicaciones' do
  numero = 1

  25000.times do
    numero = numero + numero
  end

  puts numero.to_s.length.to_s+' dígitos'  # Es decir, el número de dígitos en este número ENORME.
end

perfil 'contar hasta un millón' do
  numero = 0

  1000000.times do
    numero = numero + 1
  end
end

¡Qué simple! ¡Qué elegante! Con ese pequeño método, ahora puedo cronometrar fácilmente cualquier sección de cualquier programa que quiera; simplemente lanzo el código en un bloque y lo envío a perfil. ¿Qué podría ser más simple? En la mayoría de los lenguajes, tendría que agregar explícitamente ese código de cronometraje (las cosas dentro de perfil) alrededor de cada sección que quisiera cronometrar. En Ruby, sin embargo, puedo mantenerlo todo en un solo lugar y (lo más importante) ¡fuera de mi camino!

Algunas Cosas Para Probar

  • Reloj del Abuelo. Escribe un método que tome un bloque y lo llame una vez por cada hora que ha pasado hoy. De esa manera, si pasara el bloque do puts '¡DONG!' end, sonaría (más o menos) como un reloj de abuelo. Prueba tu método con algunos bloques diferentes (incluido el que te acabo de dar). Pista: Puedes usar Time.now.hour para obtener la hora actual. Sin embargo, esto devuelve un número entre 0 y 23, por lo que tendrás que alterar esos números para obtener números de reloj ordinarios (1 a 12).
  • Logger del Programa. Escribe un método llamado log, que tome una descripción de cadena de un bloque y, por supuesto, un bloque. Similar a hacerCosaImportante, debería hacer puts de una cadena que diga que ha comenzado el bloque, y otra cadena al final que diga que ha terminado el bloque, y también decirte qué devolvió el bloque. Prueba tu método enviándole un bloque de código. Dentro del bloque, pon otra llamada a log, pasándole otro bloque. (Esto se llama anidamiento). En otras palabras, tu salida debería verse algo así:
    Comenzando "bloque exterior"...
    Comenzando "algún bloque pequeño"...
    ..."algún bloque pequeño" terminó, devolviendo:  5
    Comenzando "otro bloque más"...
    ..."otro bloque más" terminó, devolviendo:  ¡Me gusta la comida tailandesa!
    ..."bloque exterior" terminó, devolviendo:  false
    
  • Mejor Logger del Programa. La salida de ese último logger era un poco difícil de leer, y solo empeoraría cuanto más lo usaras. Sería mucho más fácil de leer si sangrara las líneas en los bloques internos. Para hacer esto, necesitarás llevar un registro de cuán profundamente anidado estás cada vez que se llama al log. Para hacer esto, usa una variable global, que es una variable que puedes ver desde cualquier lugar de tu código. Para hacer una variable global, simplemente precede el nombre de tu variable con $, como estas: $global, $profundidadDeAnidamiento, y $granPeeWee. Al final, tu logger debería generar código como este:
    Comenzando "bloque exterior"...
      Comenzando "algún bloque pequeño"...
        Comenzando "bloque pequeñito"...
        ..."bloque pequeñito" terminó, devolviendo:  mucho amor
      ..."algún bloque pequeño" terminó, devolviendo:  42
      Comenzando "otro bloque más"...
      ..."otro bloque más" terminó, devolviendo:  ¡Me encanta la comida india!
    ..."bloque exterior" terminó, devolviendo:  true
    

Bueno, eso es todo para este tutorial. ¡Felicidades! Has aprendido mucho. Tal vez sientas que no recuerdas nada de esto, o tal vez te saltaste algunas partes... Relájate. La programación no se trata de lo que sabes; se trata de lo que puedes averiguar. Mientras sepas dónde encontrar las cosas que olvidas, estás bien. ¡Espero que no pienses que escribí todo esto sin buscar cosas cada minuto! Porque lo hice. También recibí mucha ayuda con los ejemplos de código en este tutorial. Pero, ¿dónde estaba buscando todo y a quién le estaba pidiendo ayuda? Déjame presentarte...