Fibras são algo que você provavelmente nunca usará diretamente no código de nível de aplicativo. Eles são um primitivo de controle de fluxo que você pode usar para construir outras abstrações, que você usa em código de nível superior.
Provavelmente, o uso nº 1 de fibras em Ruby é implementar Enumerator
s, que são uma classe central de Ruby no Ruby 1.9. Eles são incrivelmente úteis.
No Ruby 1.9, se você chamar quase qualquer método iterador nas classes principais, sem passar um bloco, ele retornará um Enumerator
.
irb(main):001:0> [1,2,3].reverse_each
=> #<Enumerator: [1, 2, 3]:reverse_each>
irb(main):002:0> "abc".chars
=> #<Enumerator: "abc":chars>
irb(main):003:0> 1.upto(10)
=> #<Enumerator: 1:upto(10)>
Esses Enumerator
s são objetos Enumerable, e seus each
métodos geram os elementos que teriam sido produzidos pelo método iterador original, se tivesse sido chamado com um bloco. No exemplo que acabei de dar, o Enumerador retornado por reverse_each
tem um each
método que retorna 3,2,1. O Enumerador retornado por chars
retorna "c", "b", "a" (e assim por diante). MAS, ao contrário do método iterador original, o Enumerator também pode retornar os elementos um por um se você chamá next
-lo repetidamente:
irb(main):001:0> e = "abc".chars
=> #<Enumerator: "abc":chars>
irb(main):002:0> e.next
=> "a"
irb(main):003:0> e.next
=> "b"
irb(main):004:0> e.next
=> "c"
Você pode ter ouvido falar de "iteradores internos" e "iteradores externos" (uma boa descrição de ambos é fornecida no livro "Gang of Four" Design Patterns). O exemplo acima mostra que Enumeradores podem ser usados para transformar um iterador interno em um externo.
Esta é uma maneira de fazer seus próprios enumeradores:
class SomeClass
def an_iterator
# note the 'return enum_for...' pattern; it's very useful
# enum_for is an Object method
# so even for iterators which don't return an Enumerator when called
# with no block, you can easily get one by calling 'enum_for'
return enum_for(:an_iterator) if not block_given?
yield 1
yield 2
yield 3
end
end
Vamos tentar:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Espere um minuto ... alguma coisa parece estranha aí? Você escreveu as yield
instruções an_iterator
como código de linha reta, mas o Enumerador pode executá-las uma de cada vez . Entre as chamadas para next
, a execução de an_iterator
é "congelada". Cada vez que você chama next
, ele continua executando a seguinte yield
instrução e, em seguida, "congela" novamente.
Você consegue adivinhar como isso é implementado? O Enumerador envolve a chamada para an_iterator
em uma fibra e passa um bloco que suspende a fibra . Portanto, toda vez que an_iterator
cede ao bloco, a fibra na qual ele está sendo executado é suspensa e a execução continua no thread principal. Na próxima vez que você ligar next
, ele passa o controle para a fibra, o bloco retorna e an_iterator
continua de onde parou.
Seria instrutivo pensar no que seria necessário para fazer isso sem fibras. CADA classe que quisesse fornecer iteradores internos e externos teria que conter código explícito para controlar o estado entre as chamadas para next
. Cada chamada para next teria que verificar esse estado e atualizá-lo antes de retornar um valor. Com fibras, podemos converter automaticamente qualquer iterador interno em externo.
Isso não tem a ver com fibras persay, mas deixe-me mencionar mais uma coisa que você pode fazer com Enumerators: eles permitem que você aplique métodos Enumerable de ordem superior a outros iteradores diferentes de each
. Pense nisso: normalmente todos os métodos Enumerable, incluindo map
, select
, include?
, inject
, e assim por diante, todos os trabalhos sobre os elementos gerados pelo each
. Mas e se um objeto tiver outros iteradores diferentes de each
?
irb(main):001:0> "Hello".chars.select { |c| c =~ /[A-Z]/ }
=> ["H"]
irb(main):002:0> "Hello".bytes.sort
=> [72, 101, 108, 108, 111]
Chamar o iterador sem bloco retorna um Enumerator, e então você pode chamar outros métodos Enumerable nele.
Voltando às fibras, você usou o take
método de Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Se alguma coisa chamar esse each
método, parece que nunca deve retornar, certo? Veja isso:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Não sei se isso usa fibras sob o capô, mas poderia. As fibras podem ser usadas para implementar listas infinitas e avaliação preguiçosa de uma série. Para obter um exemplo de alguns métodos preguiçosos definidos com Enumeradores, defini alguns aqui: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Você também pode construir uma instalação de co-rotina de uso geral usando fibras. Eu nunca usei corrotinas em nenhum dos meus programas ainda, mas é um bom conceito de saber.
Espero que isso lhe dê uma ideia das possibilidades. Como eu disse no início, as fibras são um primitivo de controle de fluxo de baixo nível. Eles tornam possível manter várias "posições" de fluxo de controle dentro de seu programa (como diferentes "marcadores" nas páginas de um livro) e alternar entre eles conforme desejado. Como o código arbitrário pode ser executado em uma fibra, você pode chamar o código de terceiros em uma fibra e, em seguida, "congelá-lo" e continuar fazendo outra coisa quando ele retornar ao código que você controla.
Imagine algo assim: você está escrevendo um programa de servidor que atenderá a muitos clientes. Uma interação completa com um cliente envolve passar por uma série de etapas, mas cada conexão é transitória e você deve lembrar o estado de cada cliente entre as conexões. (Parece programação da web?)
Em vez de armazenar explicitamente esse estado e verificá-lo cada vez que um cliente se conecta (para ver qual é a próxima "etapa" que eles precisam fazer), você pode manter uma fibra para cada cliente. Depois de identificar o cliente, você deve recuperar sua fibra e reiniciá-lo. Então, ao final de cada conexão, você suspenderia a fibra e a armazenaria novamente. Dessa forma, você poderia escrever código em linha reta para implementar toda a lógica para uma interação completa, incluindo todas as etapas (exatamente como você faria naturalmente se seu programa fosse feito para ser executado localmente).
Tenho certeza de que há muitos motivos pelos quais tal coisa pode não ser prática (pelo menos por agora), mas, novamente, estou apenas tentando mostrar a você algumas das possibilidades. Quem sabe; depois de entender o conceito, você pode criar um aplicativo totalmente novo no qual ninguém mais pensou ainda!