apenas uma figura bonitinha

10.  Blocos e Procs

 

Este é, definitivamente, um dos recursos mais legais de Ruby. Algumas outras linguagens têm esse recurso, porém elas podem chamar isso de formas diferentes (como closures), mas muitas das mais populares não, o que é uma pena.

Então o que é essa nova coisa legal? É a habilidade de pegar um bloco de código (código entre do e end), amarrar tudo em um objeto (chamado de proc), armazenar isso em uma variável e passar isso para um método, e rodar o código do bloco quando você quiser (mais de uma vez, se você quiser). Então, é como se fosse um método, exceto pelo fato de que isso não está em um objeto (isso é um objeto), e você pode armazená-lo ou passá-lo adiante, como com qualquer outro objeto. Acho que é hora de um exemplo:

saudacao = Proc.new do
  puts 'Olá!'
end

saudacao.call
saudacao.call
saudacao.call
Olá!
Olá!
Olá!

Eu criei uma proc (eu acho que é abreviatura para "procedimento", mas o que importa é que rima com "block") que tem um bloco de código, então eu chamei (call) a proc três vezes. Como você pode ver, parece, em muito, com um método.

Atualmente, é muito mais parecido com um método do que eu mostrei para você, porque blocos podem receber parâmetros:

VoceGostade = Proc.new do |umaBoaCoisa|
  puts 'Eu *realmente* gosto de '+umaBoaCoisa+'!'
end

VoceGostade.call 'chocolate'
VoceGostade.call 'ruby'
Eu *realmente* gosto de chocolate!
Eu *realmente* gosto de ruby!

Certo, então nós vimos o que blocos e procs são e como os usar, mas e daí? Por que não usar apenas métodos? Bem, isso é porque existem algumas coisas que você não pode fazer com métodos. Particularmente, você não pode passar métodos para outros métodos (mas você pode passar procs para métodos), e métodos não podem retornar outros métodos (mas podem retornar procs). É apenas por isso que procs são objetos; métodos não.

(De qualquer forma, isso parece familiar? Sim, você já viu blocos antes... quando você aprendeu sobre iteradores. Mas vamos voltar a falar disso daqui a pouco.)

Métodos que Recebem Procs

Quando passamos uma proc em um método, nós podemos controlar como, se ou quantas vezes nós vamos chamar a proc. Por exemplo, posso dizer que há uma coisa que nós queremos fazer antes e depois que um código é executado:

def FacaUmaCoisaImportante umaProc
  puts 'Todo mundo apenas ESPERE! Eu tenho uma coisa a fazer...'
  umaProc.call
  puts 'Certo pessoal, Eu terminei. Voltem a fazer o que estavam fazendo.'
end

digaOla = Proc.new do
  puts 'olá'
end

digaTchau = Proc.new do
  puts 'tchau'
end

FacaUmaCoisaImportante digaOla
FacaUmaCoisaImportante digaTchau
Todo mundo apenas ESPERE! Eu tenho uma coisa a fazer...
olá
Certo pessoal, Eu terminei. Voltem a fazer o que estavam fazendo.
Todo mundo apenas ESPERE! Eu tenho uma coisa a fazer...
tchau
Certo pessoal, Eu terminei. Voltem a fazer o que estavam fazendo.

Talvez isso não pareça tão fabuloso... mas é. :-) É muito comum em programação que alguns requesitos críticos sejam executados. Se você grava um arquivo, por exemplo, você deve abrir o arquivo, escrever o que quiser lá dentro e então fechar o arquivo. Se você se esquecer de fechar o arquivo, Coisas Ruins(tm) podem acontecer. Mas toda a vez que você quiser salvar ou carregar um arquivo, você deve fazer a mesma coisa: abrir o arquivo, fazer o que você realmente quiser com ele e então fechar o arquivo. Isso é entediante e fácil de esquecer. Em Ruby, salvar (ou carregar) arquivos funciona similarmente com o código anterior, então você não precisa se preocupar com nada além de o que você quer salvar (ou carregar) (No próximo capítulo eu vou lhe mostrar como fazer coisas como salvar e carregar arquivos).

Você pode também escrever métodos que vão determinar quantas vezes, ou mesmo se uma proc será chamada. Aqui está um método que irá chamar uma proc metade do tempo, e outra que irá chamar a proc duas vezes.

def talvezFaca umaProc
  if rand(2) == 0
    umaProc.call
  end
end

def FacaDuasVezes umaProc
  umaProc.call
  umaProc.call
end

piscar = Proc.new do
  puts '<piscada>'
end

olhandofixamente = Proc.new do
  puts '<olhando fixamente>'
end

talvezFaca piscar
talvezFaca olhandofixamente
FacaDuasVezes piscar
FacaDuasVezes olhandofixamente
<olhando fixamente>
<piscada>
<piscada>
<olhando fixamente>
<olhando fixamente>

(Se você recarregar essa página algumas vezes, você verá que a saída muda) Esses são alguns dos usos mais comuns de procs, que nos possibilita fazer coisas que nós simplesmente não poderíamos fazer usando apenas métodos. Claro, você pode escrever um método que "pisque" duas vezes, mas você não pode escrever um que apenas faça qualquer coisa duas vezes!

Antes de seguirmos adiante, vamos olhar um último exemplo. Até agora, as procs que nós usamos foram muito similares umas às outras. Agora, elas serão um pouco diferentes, assim você poderá ver como um método depende das procs que lhe são passadas. Nosso método irá receber um objeto e uma proc e irá chamar essa proc naquele objeto. Se a proc retornar falso, nós saímos; caso contrário, nós chamamos a proc com o objeto retornado. Nós vamos continuar fazendo isso até que a proc retorne falso (o que é o melhor a fazer eventualmente, ou o programá irá travar). O método irá retornar o último valor não falso retornado pela proc.

def facaAteFalso primeiraEntrada, umaProc
  entrada = primeiraEntrada
  saida   = primeiraEntrada

  while saida
    entrada = saida
    saida   = umaProc.call entrada
  end

  entrada
end

construindoVetorDeQuadrados = Proc.new do |vetor|
  ultimoNumero = vetor.last
  if ultimoNumero <= 0
    false
  else
    vetor.pop                            # Jogue fora o último número...
    vetor.push ultimoNumero*ultimoNumero # ... e o substitua com esse quadrado...
    vetor.push ultimoNumero-1            # ... seguido pelo número imediatamente anterior.
  end
end

sempreFalso = Proc.new do |apenasIgnoreme|
  false
end

puts facaAteFalso([5], construindoVetorDeQuadrados).inspect
puts facaAteFalso('Estou escrevendo isso às 3:00; alguém me derrube!', sempreFalso)
[25, 16, 9, 4, 1, 0]
Estou escrevendo isso às 3:00; alguém me derrube!

Certo, esse foi um exemplo estranho, eu admito. Mas isso mostra como nosso método age diferentemente quando recebe diferentes procs.

O método inspect é muito parecido com o to_s, exceto pelo fato de que a string retornada tenta mostrar para você o código em ruby que está construindo o objeto que você passou. Aqui ele nos mostra todo o vetor retornado pela nossa primeira chamada a facaAteFalso. Você também deve ter notado que nós nunca elevamos aquele 0 ao quadrado, no fim do vetor. Já que 0 elevado ao quadrado continua apenas 0, nós não precisamos fazer isso. E já que sempreFalso foi, você sabe, sempre falso, facaAteFalso não fez nada na segunda vez que a chamamos; apenas retornou o que lhe foi passada.

Métodos que Retornam Procs

Uma das outras coisas legais que você pode fazer com procs é criá-las em métodos e retorná-las. Isso permite toda uma variedade de poderes de programação malucos (coisas com nomes impressionantes, como avaliação preguiçosa, estrutura de dados infinita, e temperando com curry), mas o fato é de que eu nunca faço isso na prática e eu não me lembro de ter visto ninguém fazendo isso. Eu acho que isso é o tipo de coisa que você acaba não fazendo em Ruby, ou talvez Ruby apenas encoraje-o a achar outras soluções: eu não sei. De qualquer forma, eu vou tocar no assunto apenas brevemente.

Nesse exemplo, compor recebe duas procs e retorna uma nova proc que, quando chamada, chama uma terceira proc e passa seu resultado para a segunda proc.

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

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

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

dobreeEleve = compor dobre, quadrado
eleveeDobre = compor quadrado, dobre

puts dobreeEleve.call(5)
puts eleveeDobre.call(5)
100
50

Note que a chamada para proc1 deve ser inserida entre parenteses dentro de proc2, para que seja executada primeiro.

Passando Blocos (E Não Procs) para Métodos

Certo, isso foi muito interessante academicamente, mas de pouca utilidade prática. Uma porção desse problema é que há três passos que você deve seguir (definir o método, construir a proc e chamar o método com a proc), quando eles podem ser resumidos em apenas dois (definir o método e passar o bloco diretamente ao método, sem usar uma proc), uma vez que na maior parte das vezes você não vai querer usar a proc ou o bloco depois que o passar para um método. Bem, você não sabe, mas Ruby tem isso para nós! Na verdade, você já estava fazendo isso todas as vezes que usou iteradores.

Eu vou mostrar a você um exemplo rápido, então nós vamos falar sobre isso.

class Array

    def cadaComparacao(&eraUmBloco_agoraUmaProc)
    eIgual = true  # Nós começamos com "verdadeiro" porque vetores começam com 0, mesmo se iguais.

    self.each do |objeto|
      if eIgual
        eraUmBloco_agoraUmaProc.call objeto
      end

      eIgual = (not eIgual)  # Comutando entre igual para diferente, ou de diferente para igual.
    end
  end

end

['maçã', 'maçã podre', 'cereja', 'mamona'].cadaComparacao do |fruta|
  puts 'Hum! Eu adoro tortas de '+fruta+', você não?'
end

#  Lembre-se, nós estamos pegando os mesmos elementos numerados
#  do array, todos que se relacionam com os outros números,
#  apenas porque gosto de causar esse tipo de problema.
[1, 2, 3, 4, 5].cadaComparacao do |bola_estranha|
  puts bola_estranha.to_s+' não é um número!'
end
Hum! Eu adoro tortas de maçã, você não?
Hum! Eu adoro tortas de cereja, você não?
1 não é um número!
3 não é um número!
5 não é um número!

Para passar um bloco para cadaComparacao, tudo o que temos que fazer é anexar o bloco após o método. Você pode passar um bloco para qualquer método dessa maneira, apesar de que muitos métodos vão apenas ignorar o bloco. Para fazer seu método não ignorar o bloco, mas pegá-lo e transformá-lo em uma proc, ponha o nome da proc no começo da lista dos parâmetros do seu método, precedido por um 'e' comercial (&). Essa parte é um pequeno truque, mas não é tão ruim, e você apenas precisa fazer isso uma vez (quando você define o método). Então você pode usar o método de novo, e de novo e de novo, assim como os métodos da linguagem que aceitam blocos, como o each e o times (Lembra-se do 5.times do...?).

Se você estiver confuso, apenas lembre-se do que supostamente o método cadaComparacao faz: chama o bloco passado como parâmetro para cada elemento no vetor. Depois que você o escrever e ele estiver funcionando, você não vai precisar pensar sobre o que está acontecendo realmente por baixo dos panos ("qual bloco é chamado quando??"); na verdade, é exatamente por isso que escrevemos métodos assim: nós nunca mais vamos precisar pensar sobre como eles funcionam novamente. Nós vamos apenas usar-los.

Eu lembro que uma vez eu quis cronometrar quanto tempo cada seção do meu código estava demorando (Isso é algo conhecido como sumarizar o código). Então, eu escrevi um método que pegava o tempo antes de executar o código, o executava e então fazia uma nova medição do tempo e me retornava a diferença. Eu não estou conseguindo achar o código agora, mas eu não preciso disso: provavelmente é um código parecido com esse:

def sumario descricaoDoBloco, &bloco
  tempoInicial = Time.now

  bloco.call

  duracao = Time.now - tempoInicial

  puts descricaoDoBloco+': '+duracao.to_s+' segundos'
end

sumario 'dobrando 25000 vezes' do
  numero = 1

  25000.times do
    numero = numero + numero
  end

  puts numero.to_s.length.to_s+' dígitos'  #  É isso mesmo: o número de dígitos nesse número ENORME.
end

sumario 'contando até um milhão' do
  numero = 0

  1000000.times do
    numero = numero + 1
  end
end
7526 dígitos
dobrando 25000 vezes: 0.079249 segundos
contando até um milhão: 0.134337 segundos

Que simplicidade! Que elegância! Com aquele pequeno método eu posso, agora, facilmente cronometrar qualquer seção, de qualquer programa, que eu queira, eu apenas preciso jogar o código para um bloco e enviar ele para o sumario. O que poderia ser mais simples? Em muitas linguagens, eu teria que adicionar explicitamente o código de cronometragem (tudo o que está em sumario) em volta de qualquer seção que eu queira medir. Em Ruby, porém, eu deixo tudo em um só lugar, e (o mais importante) fora do meu caminho!

Algumas Coisinhas Para Tentar

Relógio Cuco. Escreva um método que pegue um bloco e o chame de hora em hora. Assim, se eu passar o bloco do puts 'DONG!end, ele deve tocar como um relógio cuco. Teste seu método com diferentes blocos (inclusive o que eu mostrei para você). Dica: Você pode usar Time.now.hour para pegar a hora atual. Porém, isso retorna um número entre 0 e 23, então você deve alterar esses números para os números de um relógio normal, entre (1 e 12).

Logger do programa. Escreva um método chamado log, que pegue uma string como descrição de um bloco e, é claro, um bloco. Similarmente ao FacaUmaCoisaImportante, essa deve retornar (puts) uma string dizendo que o bloco foi iniciado e outra string ao fim, dizendo que é o fim da execução do bloco, e também dizendo o que o bloco retornou. Teste seu método enviando um bloco de código. Dentro do bloco, coloque outra chamada para log, passando outro bloco para o mesmo (isto é chamado de nesting). Em outras palavras, sua saída deve se parecer com isso:

Começando "bloco externo"...
Começando "um bloco um pouco menor"...
..."um bloco um pouco menor" terminou retornando:  5
Começando "um outro bloco"...
..."um outro bloco" terminou retornando: Eu gosto de comida tailandesa!
..."bloco externo" terminou retornando: false

Um Logger aperfeiçoado. A saída do último logger é muito difícil de ler, e fica muito pior a medida que você for usando. Seria muito mais fácil de ler se você identasse as linhas para os blocos internos. Para fazer isso, você vai precisar saber quão profundamente aninhado você está toda vez que for escrever algo no log. Para fazer isso, use uma variável global, uma variável que você possa ver de qualquer lugar de seu código. Para instânciar uma variável global, você deve precedê-la com um $, assim: $global, $nestingDepth, e $bigTopPeeWee. Enfim, seu logger deve ter uma saída parecida com essa:

Começando "bloco externo"...
  Começando "um pequeno bloco"...
    Começando "pequenino bloco"...
    ..."pequenino bloco" terminou retornando: muito amor
  ..."um pequeno bloco" terminou retornando: 42
  Começando "um outro bloco"...
  ..."um outro bloco" terminou retornando: Eu adoro comida indiana!
..."bloco externo" terminou retornando: true

Bem, isso é tudo que você aprendeu com esse tutorial. Parabéns! Você aprendeu muito. Talvez você sinta como se não lembrasse de nada, ou talvez você tenha pulado algumas partes... Relaxe. Programação não é o que você sabe, e sim o que você faz. À medida que você for aprendendo onde procurar as coisas que você esquecer, você estará indo bem. Eu espero que você não ache que eu escrevi tudo isso sem ficar conferindo a cada minuto! Porque eu fiz isso. Eu também tive muita ajuda com os códigos que rodam em todos os exemplos desse tutorial. Mas onde eu estava pesquisando tudo e a quem eu estava pedindo ajuda? Deixe-me conhecê-lo...

 

© 2003-2015 Chris Pine