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:
- A Fiber é criada com new.
- Dentro de um iterador que vai rodar 10 vezes, é chamado o método resume.
- É executado o código do início do "corpo" da Fiber até yield.
- Nesse ponto, o controle é transferido com o valor de y para onde foi chamado o resume e impresso na tela.
- 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
- Pull requests em modo raiz - sex, 22 de dezembro de 2023, 09:57:09 -0300
- Qual a idade do seu repositório? - ter, 27 de dezembro de 2022, 12:50:35 -0300
- Utilizando ctags em projetos Rails mais recentes - qui, 24 de junho de 2021, 08:23:43 -0300
- Fazendo o seu projeto brotar - seg, 15 de julho de 2019, 08:57:05 -0300
- Learn Functional Programming with Elixir - sex, 02 de março de 2018, 18:47:13 -0300
- Ambiente mínimo - Driver Driven Development - qua, 23 de agosto de 2017, 15:15:03 -0300
- Ambiente mínimo - repositórios de código - dom, 16 de abril de 2017, 13:02:14 -0300
- Ambiente mínimo - terminal e navegador - dom, 02 de abril de 2017, 21:43:29 -0300
- Utilizando muitas gems no seu projeto? - sáb, 29 de outubro de 2016, 11:57:55 -0200
- Desenvolvedores e inteligência artificial - seg, 11 de julho de 2016, 09:09:38 -0300
Carlos, após uma olhada no cont.c e conversando com o autor da "brincadeira", o Sasada Koichi, a resposta é que são coisas totalmente separadas, ou seja, mesmo sem as Continuations, as Fibers continuam vivas da silva. :-)
Interessante isso, você sabe se as coroutines foram implementadas usando call/cc (se ele estiver no 1.9)?
Algum tempo atrás eu implementei generators e coroutines (as verdadeiras :) em Scheme. Usei call/cc e dynamic-wind e é totalmente viável.
Aliás, coroutines que só retornam ao caller me parecem meros generators (a la Python). As coroutines reais tem mais utilidades.
UIA QUEM APARECEU! É um prazer a sua visita aqui, Guaracy! :-) Obrigado pela URL!
Realmente essa do caracter ficou legal:
[taq@~]irb1.9
irb(main):001:0> "Guaracy"[0]
=> "G"
Algumas outras mudanças podem ser vistas no Eigenclass (http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9), inclusive uma que consta lá (http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9/define) eu notei que eles haviam perdido em um commit e avisei a turma, como pode ser visto nessa URL: http://eustaquiorangel.com/blog/show/454. Agora está tudo certinho. :-)
Abração!
Já que dizem que uma imagem vale por mil palavras, acho que o gráfico abaixo pode servir como um bom suporte:
http://www.drones.com/images/coroutine.gif
(texto completo em: http://www.drones.com/coexp/)
Eu vejo as discussões como papo furado.
Mas o que eu mais gostei do último link que informaste foi "HELLO"[1] -> "E". Sempre detestei o retorno numérico quando deveria ser um caractere. :)