Les fibres sont quelque chose que vous n'utiliserez probablement jamais directement dans le code au niveau de l'application. Il s'agit d'une primitive de contrôle de flux que vous pouvez utiliser pour créer d'autres abstractions, que vous utilisez ensuite dans un code de niveau supérieur.
L'utilisation n ° 1 des fibres dans Ruby est probablement d'implémenter Enumerator
s, qui sont une classe Ruby de base dans Ruby 1.9. Ce sont incroyablement utiles.
Dans Ruby 1.9, si vous appelez presque n'importe quelle méthode d'itérateur sur les classes principales, sans passer un bloc, elle renverra 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)>
Ce Enumerator
sont des objets Enumerable, et leurs each
méthodes donnent les éléments qui auraient été générés par la méthode iterator originale, si elle avait été appelée avec un bloc. Dans l'exemple que je viens de donner, l'énumérateur renvoyé par reverse_each
a une each
méthode qui donne 3,2,1. L'énumérateur renvoyé par renvoie chars
"c", "b", "a" (et ainsi de suite). MAIS, contrairement à la méthode iterator d'origine, l'énumérateur peut également retourner les éléments un par un si vous l'appelez next
à 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 des «itérateurs internes» et des «itérateurs externes» (une bonne description des deux est donnée dans le livre «Gang of Four» Design Patterns). L'exemple ci-dessus montre que les énumérateurs peuvent être utilisés pour transformer un itérateur interne en un itérateur externe.
Voici une façon de créer vos propres 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 ... est-ce que quelque chose vous semble étrange? Vous avez écrit les yield
instructions sous an_iterator
forme de code linéaire, mais l'énumérateur peut les exécuter une par une . Entre les appels à next
, l'exécution de an_iterator
est "gelée". Chaque fois que vous appelez next
, il continue de fonctionner jusqu'à l' yield
instruction suivante , puis "se fige" à nouveau.
Pouvez-vous deviner comment cela est mis en œuvre? L'énumérateur encapsule l'appel an_iterator
dans une fibre et transmet un bloc qui suspend la fibre . Ainsi, chaque fois que an_iterator
le bloc cède, la fibre sur laquelle il fonctionne 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 revient et an_iterator
continue là où il s'était arrêté.
Il serait instructif de penser à ce qu'il faudrait pour faire cela sans fibres. CHAQUE classe qui voulait fournir des itérateurs internes et externes devrait contenir du code explicite pour garder une trace de l'état entre les appels à next
. Chaque appel à next devrait vérifier cet état et le mettre à jour avant de renvoyer une valeur. Avec les fibres, nous pouvons convertir automatiquement n'importe quel itérateur interne en un itérateur externe.
Cela n'a pas à voir avec les fibres persay, mais permettez-moi de mentionner une autre chose que vous pouvez faire avec les Enumerators: ils vous permettent d'appliquer des méthodes Enumerable d'ordre supérieur à d'autres itérateurs autres que each
. Pensez-y: normalement toutes les méthodes, y compris Enumerable 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 autres 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 sans bloc renvoie un Enumerator, puis vous pouvez appeler d'autres méthodes Enumerable à ce sujet.
Pour en revenir aux fibres, avez-vous utilisé la take
méthode d'Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Si quelque chose appelle cette each
méthode, il semble qu'elle ne devrait jamais revenir, non? Regarde ça:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Je ne sais pas si cela utilise des fibres sous le capot, mais c'est possible. Les fibres peuvent être utilisées pour implémenter des listes infinies et une évaluation paresseuse d'une série. Pour un exemple de certaines méthodes paresseuses définies avec Enumerators, j'en ai défini quelques-unes ici: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Vous pouvez également créer une installation de coroutine à usage général en utilisant des fibres. Je n'ai encore jamais utilisé de coroutines dans aucun de mes programmes, mais c'est un bon concept à connaître.
J'espère que cela vous donne une idée des possibilités. Comme je l'ai dit au début, les fibres sont une primitive de contrôle de flux de bas niveau. Ils permettent de maintenir plusieurs «positions» de flux de contrôle au sein de votre programme (comme différents «signets» dans les pages d'un livre) et de basculer entre elles à volonté. Puisque du code arbitraire peut s'exécuter dans une fibre, vous pouvez appeler du code tiers sur une fibre, puis le «figer» et continuer à faire autre chose lorsqu'il rappelle le code que vous contrôlez.
Imaginez quelque chose comme ceci: vous écrivez un programme serveur qui desservira de nombreux clients. Une interaction complète avec un client implique de passer par une série d'étapes, mais chaque connexion est transitoire et vous devez vous souvenir de l'état de chaque client entre les connexions. (Cela ressemble à de la programmation Web?)
Plutôt que de stocker explicitement cet état et de le vérifier à chaque fois qu'un client se connecte (pour voir quelle est la prochaine "étape" qu'il doit faire), vous pouvez maintenir une fibre pour chaque client. Après avoir identifié le client, vous récupéreriez sa fibre et la redémarreriez. Ensuite, à la fin de chaque connexion, vous suspendriez la fibre et la stockiez à nouveau. De cette façon, vous pouvez écrire du code en ligne droite pour implémenter toute la logique d'une interaction complète, y compris toutes les étapes (comme vous le feriez naturellement si votre programme était conçu pour s'exécuter localement).
Je suis sûr qu'il y a de nombreuses raisons pour lesquelles une telle chose n'est peut-être pas pratique (du moins pour le moment), mais encore une fois, j'essaie simplement de vous montrer certaines des possibilités. Qui sait; une fois que vous avez compris le concept, vous pouvez créer une application totalement nouvelle à laquelle personne d'autre n'a encore pensé!