Les fibres sont quelque chose que vous ne sera probablement jamais l'utiliser directement au niveau de l'application du code. Ils sont un contrôle de flux primitifs que vous pouvez utiliser pour construire d'autres abstractions, que vous pouvez ensuite utiliser dans le code de niveau supérieur.
Probablement le #1 utilisation des fibres dans le Rubis est de mettre en oeuvre Enumerator
s, qui sont au cœur de la classe Ruby Ruby 1.9. Ce sont incroyablement utile.
En Ruby 1.9, si vous appeler presque n'importe quelle méthode d'itérateur sur les classes de base, sans le passage d'un bloc, il sera de retour un 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)>
Ces Enumerator
s sont Énumérables d'objets, et leur each
méthodes fournissent des éléments qui auraient été générés par l'original de la méthode d'itérateur, s'il avait été appelé avec un bloc. Dans l'exemple que je viens de faire, l'agent Recenseur est retourné en reverse_each
a each
méthode qui donne 3,2,1. L'agent Recenseur est retourné en chars
rendements "c","b","a" (et ainsi de suite). MAIS, à la différence de l'original de la méthode d'itérateur, l'agent Recenseur peut également retourner les éléments un par un si vous appelez next
sur à plusieurs reprises:
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"
Vous avez peut être entendu parler de "internes itérateurs" et "externe itérateurs" (une bonne description est donnée dans la "bande des Quatre" livre "Design Patterns"). L'exemple ci-dessus montre que les agents Recenseurs peuvent être utilisés pour transformer un itérateur interne en externe.
C'est une façon de faire vos propres agents recenseurs:
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
Essayons:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Attendez une minute... n'a rien d'étrange là-bas? Vous avez écrit l' yield
des déclarations en an_iterator
comme en droite ligne de code, mais l'agent Recenseur peut exécuter un à un. Entre les appels à l' next
, l'exécution d' an_iterator
est "gelé". Chaque fois que vous appelez next
, elle continue de fonctionner jusqu'à la suivante yield
déclaration, puis se fige à nouveau.
Pouvez-vous deviner comment cela est mis en œuvre? L'agent Recenseur encapsule l'appel à l' an_iterator
dans une fibre, et passe un bloc qui suspend la fibre. Donc, à chaque fois an_iterator
rendements pour le bloc, la fibre qui il est en cours d'exécution est suspendue, et l'exécution se poursuit sur le thread principal. La prochaine fois que vous appelez next
, il passe le contrôle à la fibre, le bloc retourne, et an_iterator
continue là où il l'avait laissé.
Il serait instructif de réfléchir à ce qui serait nécessaire pour ce faire sans fibres. CHAQUE classe qui voulait fournir à la fois interne et externe des itérateurs devrait contenir de code explicite de garder une trace de l'état entre les appels à l' next
. Chaque appel à la prochaine devrait vérifier que l'état, et la mise à jour avant de retourner une valeur. Avec des fibres, nous pouvons automatiquement convertir n'importe quel itérateur interne à l'externe.
Cela ne veut pas avoir à faire avec des fibres persay, mais permettez-moi de mentionner une chose que vous pouvez faire avec des agents Recenseurs: ils vous permettent d'appliquer l'ordre supérieur Énumérable méthodes à d'autres itérateurs autre que each
. Pensez-y: normalement tous les Énumérable méthodes, y compris l' map
, select
, include?
, inject
, et ainsi de suite, tous les travaux sur les éléments produits par each
. Mais que faire si un objet a d'autres itérateurs autre que 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]
L'appel de l'itérateur, pas de bloc renvoie un agent Recenseur, et puis vous pouvez appeler d'autres Énumérable méthodes.
Pour en revenir à fibres, avez-vous utilisé l' take
méthode de Énumérable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Si tout ce que les appels en each
méthode, il semble comme il se doit de ne jamais revenir, non? Check this out:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Je ne sais pas si ce utilise des fibres sous le capot, mais il pourrait l'être. Les fibres peuvent être utilisées pour mettre en œuvre infinie des listes et des paresseux évaluation d'une série. Pour un exemple de certains paresseux méthodes définies avec les agents Recenseurs, j'ai défini quelques-uns ici: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Vous pouvez également créer une coroutine installation à l'aide de fibres. Je n'ai jamais utilisé de coroutines dans un de mes programmes, mais c'est un bon concept pour le savoir.
J'espère que cela vous donne une idée des possibilités. Comme je l'ai dit au début, les fibres sont d'un faible niveau de contrôle de flux primitifs. Ils permettent de maintenir de multiples flux de contrôle "positions" dans votre programme (comme les différents "signets" dans les pages d'un livre) et de basculer entre eux comme souhaité. Depuis arbitraire de code peut s'exécuter dans une fibre, vous pouvez appeler dans la 3e partie du code sur une fibre, et puis "freeze" et continuer à faire quelque chose d'autre quand il appelle de nouveau dans le code que vous contrôlez.
Imaginez quelque chose comme ceci: vous êtes à la rédaction d'un programme serveur qui sera au service de nombreux clients. Une interaction complète avec un client implique d'aller à travers une série d'étapes, mais chaque connexion est transitoire, et vous devez vous rappeler de l'état pour chaque client entre les connexions. (Son comme de programmation web?)
Plutôt que d'être explicitement le stockage de cet état, et de les vérifier à chaque fois qu'un client se connecte (pour voir ce que la prochaine "étape" ils ont à faire est de), vous pourriez maintenir une fibre pour chaque client. Après l'identification du client, vous permettrait de récupérer leurs fibres et de re-démarrer. Puis à la fin de chaque connexion, vous suspendre la fibre et de stocker de nouveau. De cette façon, vous pouvez écrire la droite ligne de code pour implémenter la logique de l'interaction complète, y compris toutes les étapes (tout comme vous serait naturellement si votre programme a été faite pour exécuter localement).
Je suis sûr qu'il ya beaucoup de raisons pour lesquelles une telle chose peut ne pas être pratique (au moins pour l'instant), mais encore une fois, je suis juste essayer de vous montrer quelques-unes des possibilités. Qui sait, une fois que vous obtenez le concept, vous pouvez venir avec certains totalement nouvelle application qui aucun autre n'a encore pensé!