Curiosidades sobre Procs em Ruby

Esses dias, estava montando um código para Ruby usando Procs (blocos de código), salvando esses procs em variáveis e depois rodando-os sobre “bindings” diferentes. Talvez tenha ficado um pouco complicado de entender, mas na prática é algo mais ou menos assim:

class UmaClasse
  def self.callback_qualquer
    ...#alguns códigos aqui....
    @@bloco = proc do
      break if condicao_qualquer
      ...#códigos do meu callback
    end
    ...#mais algumas coisas
  end

  def salvar
    instance_eval(&@@bloco)
    puts "Rodei"
  end
end

Ou seja, eu posso criar um bloco definindo um callbackqualquer, e depois rodar esse bloco no contexto da “instância”, não da classe (o código que eu fiz, na verdade, é bem mais complexo). Mas, surpreeendentemente, esse código não funciona – ele lança um LocalJumpError se a condição do Proc for satisfeita. Por quê?

Para entender isso, é legal saber como os Procs funcionam. Em ruby, tudo retorna valor, então um Proc também retorna valor. Então, quando você faz um “break”, ou “next”, “redo”, você pode passar um valor também para esses comandos. Por exemplo:

array = [1, 2, 3]
a = array.each { |x| puts x } #Valor de "a" é [1, 2, 3]
a = array.each { break 2 } #Valor de "a" é 2
a = array.inject(0) { |r, v| next v + r } #Uma forma bizarra de fazer a somatória

São todos comandos válidos, e isso pode parecer estranho para quem vem de linguagens como Java ou C++ aonde o “break” não recebe parâmetros, e o “next” e o “redo” são usados apenas em “for” e outros laços semelhantes. O que significa um “redo” em um Proc que não itera? e um “next” ou “break”? Veja os exemplos a seguir:

def dez_vezes
  10.times do
    #o redo desvia o fluxo para cá
    yield
    #o "next" desvia o fluxo para cá
  end
  puts "Rodei todos os comandos."
  
  #o "break desvia o fluxo para cá
end

Ou seja, podemos definir mais claramente:

  • redo: Desvia o fluxo imediatamente antes do “yield”
  • next: Desvia o fluxo para a linha abaixo do “yield”
  • break: Desvia o fluxo para a última linha do método

Como exemplo, o que fariam esses códigos?

def ex1
  yield 1
  puts "Uma vez"
  yield 2
  puts "Outra vez"
  yield 3
  puts "Mais uma vez"

  return 10
end

a = ex1 { |x| break if x == 2 }
b = ex1 { |x| redo if x == 3 }
c = ex1 { |x| next 15 }

O primeiro exemplo: qual o valor de “a”? O que será impresso na tela?
No segundo exemplo: o que será impresso na tela?
No terceiro exemplo: o que será impresso na tela? Qual o valor de “c”?

Quando os exemplos acima forem entendidos, é razoavelmente simples entender o por que o primeiro código desse post dá um LocalJumpError – afinal, foi pedido ao Proc um “break”, mas o método aonde ele foi definido está em outro contexto (classe, ao invés da instância), além de outras complicações – não há um lugar claro aonde o “break” deveria desviar. A forma correta é:

class UmaClasse
  def self.callback_qualquer
    ...#alguns códigos aqui....
    @@bloco = proc do
      next if condicao_qualquer
      ...#códigos do meu callback
    end
    ...#mais algumas coisas
  end

  def salvar
    instance_eval(&@@bloco)
    puts "Rodei"
  end
end

Pode parecer um pouco estranho usar “next” para sair de um “proc”, mas em muitos casos é necessário (especialmente se você possui algum código depois do bloco). Adicionalmente, eu usei para criar os Procs a palavra-chave “proc”. Vale lembrar que ela é equivalente à palavra-chave “lambda”, e não ao “Proc.new” e consequentemente criará um “lambda” e não um “Proc”. Mas isso já é assunto para outra ocasião.

Advertisements
This entry was posted in Ruby and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s