Eustáquio Rangel

Desenvolvedor, pai, metalhead, ciclista

Rubis, fibras e geradores

Publicado em Developer


Dando uma olhada nas features novas do Ruby, achei uma bem interessante esses dias. Vamos dar uma olhada nesse código:

3.times {|item| puts item}

Até aí tudo bem, é código normal para quem conhece Ruby, mas vamos dar uma olhada nesse aqui:

 1 enum1 = 3.times
 2 enum2 = %w(zero um dois).each
 3 puts enum1.class
 4 
 5 loop do
 6    puts enum1.next
 7    puts enum2.next
 8 end
Enumerable::Enumerator
0
zero
1
um
2
dois

Opa! Aqui já tem coisa diferente! Dando uma olhada no nome da classe de enum1, podemos ver que agora podemos criar um Enumerator com vários dos iteradores à que já estávamos acostumados, e foi o que eu fiz ali alternando entre os elementos dos dois Enumerators, até finalizar quando foi gerada uma exceção quando os elementos terminaram.

O segredo ali nos Enumerators é que eles estão utilizando internamente um recurso novo que vai vir com o 1.9: Fibers (esse nome pode mudar depois). Para um exemplinho básico de Fibers, podemos ver como calcular os números de Fibonacci:

 1 require "fiber"
 2 
 3 fib = Fiber.new do
 4    x, y = 0, 1
 5    loop do
 6       Fiber.yield y
 7       x,y = y,x+y
 8    end
 9 end
10 10.times { puts fib.resume }
=> 1 1 2 3 5 8 13 21 34 55

O segredo ali é que Fibers são corrotinas (é assim que se traduz coroutine?) e não subrotinas. Em uma subrotina o controle é retornado para o contexto de onde ela foi chamada geralmente com um return, e continua a partir dali liberando todos os recursos alocados dentro da rotina, como variáveis locais etc. Em uma corrotina, o controle é desviado para outro ponto mas mantendo o contexto onde ele se encontra atualmente, de modo similar à uma closure. O exemplo acima funciona dessa maneira:

  1. A Fiber é criada com new.
  2. Dentro de um iterador que vai rodar 10 vezes, é chamado o método resume.
  3. É executado o código do início do "corpo" da Fiber até yield.
  4. Nesse ponto, o controle é transferido com o valor de y para onde foi chamado o resume e impresso na tela.
  5. A partir do próximo resume, o código da Fiber é executado do ponto onde parou para baixo, ou seja, da próxima linha após o yield (linha 7, mostrando outra característica das corrotinas que é ter mais de um ponto de entrada) processando os valores das variáveis e retornando para o começo do loop, retornando o controle novamente com yield.

Pudemos comprovar que x e y tiveram seus valores preservados entre as trocas de controle. Código parecido seria feito com uma Proc dessa maneira:

1 def create_fib
2    x, y = 0, 1
3    lambda do
4       t, x, y = y, y, x+y
5       return t
6    end
7 end
8 proc = create_fib
9 10.times { puts proc.call }

Nesse caso podemos ver o comportamento da Proc como uma subrotina, pois o valor que estamos interessados foi retornado com um return explícito ali (lembrem-se que em Ruby a última expressão avaliada é a retornada, eu inseri o return explicitamente apenas para efeitos didáticos). Há algumas divergências entre Fibers serem corrotinas ou semi-corrotinas, (argh!) mas acho que já deu para entender as diferenças das subrotinas. As semi-corrotinas são diferentes das corrotinas pois só podem transferir o controle para quem as chamou, enquanto corrotinas podem transferir o controle para outra corrotina. A diferença básica era que havia Fiber (semi-corrotinas) e Fiber::Core (corrotinas) em algumas versões do Ruby 1.9, mas pelo menos na que eu estou rodando aqui de 30/08/2007, a Fiber::Core já não existe mais. Para algumas definições de como vai ficar todos esses nomes e definições aguardem os próximos capítulos do desenvolvimento da linguagem. o Sasada Koichi mesmo disse que "Fiber::Core and Fiber::Core#transfer is black magic". ;-)

Para jogar um pouco de lenha na fogueira enquanto isso, deêm uma olhada nesse código:

 1 require "fiber"
 2 
 3 f2 = Fiber.new do |value|
 4    puts "Estou em f2, transferindo para onde vai resumir ..."
 5    Fiber.yield value + 40
 6    puts "Cheguei aqui?"
 7 end
 8 
 9 f1 = Fiber.new do
10    puts "Comecei f1, transferindo para f2 ..."
11    f2.resume 10
12 end
13 
14 puts "Resumindo fiber 1: #{f1.resume}"
=> Comecei f1, transferindo para f2 ...
   Estou em f2, transferindo para onde vai resumir ...
   Resumindo fiber 1: 50

Comportamento parecido com as semi-corrotinas, correto (atenção: o "Cheguei aqui?" nunca vai ser mostrado ali)? Mas e se fizermos isso:

 1 require "fiber"
 2 
 3 f1 = Fiber.new do |other|
 4    puts "Comecei f1, transferindo para f2 ..."
 5    other.transfer Fiber.current, 10
 6 end
 7 
 8 f2 = Fiber.new do |caller,value|
 9    puts "Estou em f2, transferindo para f1 ..."
10    caller.transfer value + 40
11    puts "Cheguei aqui?"
12 end
13 
14 puts "Resumindo fiber 1: #{f1.resume(f2)}"

Nesse caso, f1 está transferindo o controle para f2 (que não é quem a chamou!), que transfere de volta para f1 que retorna o resultado em resume. Discussões teóricas à parte, as Fibers são um recurso interessante que vai dar para especularmos mais coisas assim que "fechar" o assunto oficialmente na implementação da próxima versão do Ruby. Para finalizar, um bate-bola rápido no esquema de "produtor-consumidor" usando Fibers:

 1 require "fiber"
 2 
 3 produtor = Fiber.new do |cons|
 4    5.times do
 5       items = Array.new((rand*5).to_i+1,"oi!")
 6       puts "Produzidos #{items} ..."
 7       cons.transfer Fiber.current, items
 8    end
 9 end
10 
11 consumidor = Fiber.new do |prod,items|
12    loop do
13       puts "Consumidos #{items}"
14       prod, items = prod.transfer
15    end
16 end
17 
18 produtor.resume consumidor

Este post foi baseado nos artigos citados abaixo e em várias discussões vistas na Ruby-talk. Vale a lida do primeiro como especulação das Fibers como opção para concorrência "peso-leve" e do blog do David Flanagan que está bem por dentro do assunto. Como o assunto é bem novo me corrijam se eu estiver errado em algum ponto por favor e vamos esperar para ver como "fecha" na versão oficial, quando for lançada.

Atualizado: Ah, esqueci de dizer, as continuations por enquanto estão no 1.9, a única diferença é ter que usar require para utilizá-las:

 1 require "continuation"
 2 
 3 def cria_continuation
 4    puts "Criando a continuation e retornando ..."
 5    callcc {|obj| return obj}
 6    puts "Ei, olha eu aqui de volta na continuation!"
 7 end
 8 
 9 puts "Vou criar a continuation."
10 cont = cria_continuation()
11 if cont
12    puts "Criada, vamos voltar para ela?"
13    cont.call
14 end
15 puts "Agora terminei, tchau."



Comentários

Comentários fechados.

Artigos anteriores