Fasern werden Sie wahrscheinlich nie direkt im Code auf Anwendungsebene verwenden. Sie sind ein Flusssteuerungsprimitiv, mit dem Sie andere Abstraktionen erstellen können, die Sie dann in übergeordnetem Code verwenden.
Wahrscheinlich besteht die häufigste Verwendung von Fasern in Ruby darin, Enumerator
s zu implementieren , die eine Ruby-Kernklasse in Ruby 1.9 sind. Diese sind unglaublich nützlich.
Wenn Sie in Ruby 1.9 fast jede Iteratormethode für die Kernklassen aufrufen, ohne einen Block zu übergeben, wird eine zurückgegeben 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)>
Dies Enumerator
sind Enumerable-Objekte, und ihre each
Methoden liefern die Elemente, die von der ursprünglichen Iterator-Methode erhalten worden wären, wenn sie mit einem Block aufgerufen worden wäre. In dem Beispiel, das ich gerade gegeben habe, hat der von zurückgegebene Enumerator reverse_each
eine each
Methode, die 3,2,1 ergibt. Der Enumerator gab die chars
Ausbeuten "c", "b", "a" (und so weiter) zurück. ABER im Gegensatz zur ursprünglichen Iteratormethode kann der Enumerator die Elemente auch einzeln zurückgeben, wenn Sie next
sie wiederholt aufrufen :
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"
Möglicherweise haben Sie von "internen Iteratoren" und "externen Iteratoren" gehört (eine gute Beschreibung von beiden finden Sie im Buch "Gang of Four" -Designmuster). Das obige Beispiel zeigt, dass Enumeratoren verwendet werden können, um einen internen Iterator in einen externen zu verwandeln.
Dies ist eine Möglichkeit, eigene Enumeratoren zu erstellen:
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
Lass es uns versuchen:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Moment mal ... scheint da etwas seltsam? Sie schrieb die yield
Aussagen in an_iterator
als geradlinigen Code, aber der Enumerator kann sie läuft einen nach dem anderen . Zwischen den Aufrufen von next
ist die Ausführung von an_iterator
"eingefroren". Jedes Mal next
, wenn Sie aufrufen , wird die folgende yield
Anweisung ausgeführt und "friert" erneut ein.
Können Sie sich vorstellen, wie dies umgesetzt wird? Der Enumerator umschließt den Aufruf mit an_iterator
einer Faser und übergibt einen Block, der die Faser unterbricht . Jedes Mal, wenn an_iterator
der Block nachgibt, wird die Faser, auf der er ausgeführt wird, angehalten und die Ausführung im Hauptthread fortgesetzt. Beim nächsten Aufruf next
wird die Steuerung an die Glasfaser übergeben, der Block kehrt zurück und wird dort an_iterator
fortgesetzt, wo er aufgehört hat.
Es wäre lehrreich darüber nachzudenken, was erforderlich wäre, um dies ohne Fasern zu tun. JEDE Klasse, die sowohl interne als auch externe Iteratoren bereitstellen wollte, musste expliziten Code enthalten, um den Status zwischen den Aufrufen von zu verfolgen next
. Jeder Aufruf zum nächsten müsste diesen Status überprüfen und aktualisieren, bevor ein Wert zurückgegeben wird. Mit Fasern können wir jeden internen Iterator automatisch in einen externen konvertieren.
Dies hat nichts mit Fasern zu tun, aber ich möchte noch eines erwähnen, das Sie mit Enumeratoren tun können: Sie ermöglichen es Ihnen, Enumerable-Methoden höherer Ordnung auf andere Iteratoren als anzuwenden each
. Denken Sie daran: normalerweise alle Enumerable - Methoden, einschließlich map
, select
, include?
, inject
, und so weiter, alle Arbeiten an den Elementen erhalten durch each
. Aber was ist, wenn ein Objekt andere Iteratoren als hat 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]
Wenn Sie den Iterator ohne Block aufrufen, wird ein Enumerator zurückgegeben. Anschließend können Sie andere Enumerable-Methoden aufrufen.
Zurück zu den Fasern, haben Sie die take
Methode von Enumerable verwendet?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Wenn irgendetwas diese each
Methode aufruft, sieht es so aus, als sollte sie niemals zurückkehren, oder? Überprüfen Sie dies heraus:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Ich weiß nicht, ob hier Fasern unter der Haube verwendet werden, aber es könnte sein. Fasern können verwendet werden, um unendliche Listen und eine verzögerte Auswertung einer Reihe zu implementieren. Als Beispiel für einige mit Enumerators definierte Lazy-Methoden habe ich hier einige definiert: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Sie können auch eine Allzweck-Coroutine-Anlage aus Fasern bauen. Ich habe noch nie Coroutinen in einem meiner Programme verwendet, aber es ist ein gutes Konzept zu wissen.
Ich hoffe, dies gibt Ihnen eine Vorstellung von den Möglichkeiten. Wie ich zu Beginn sagte, sind Fasern ein Grundelement für die Flusskontrolle auf niedriger Ebene. Sie ermöglichen es, mehrere Kontrollfluss- "Positionen" in Ihrem Programm beizubehalten (wie verschiedene "Lesezeichen" auf den Seiten eines Buches) und nach Bedarf zwischen ihnen zu wechseln. Da beliebiger Code in einer Glasfaser ausgeführt werden kann, können Sie Code von Drittanbietern auf einer Glasfaser aufrufen, ihn dann "einfrieren" und etwas anderes tun, wenn er den von Ihnen kontrollierten Code zurückruft.
Stellen Sie sich so etwas vor: Sie schreiben ein Serverprogramm, das viele Clients bedient. Eine vollständige Interaktion mit einem Client umfasst eine Reihe von Schritten, aber jede Verbindung ist vorübergehend, und Sie müssen sich den Status für jeden Client zwischen den Verbindungen merken. (Klingt nach Webprogrammierung?)
Anstatt diesen Status explizit zu speichern und jedes Mal zu überprüfen, wenn ein Client eine Verbindung herstellt (um zu sehen, was der nächste "Schritt" ist, den er tun muss), können Sie für jeden Client eine Glasfaser verwalten. Nachdem Sie den Client identifiziert haben, rufen Sie dessen Glasfaser ab und starten ihn neu. Am Ende jeder Verbindung würden Sie die Glasfaser aussetzen und erneut speichern. Auf diese Weise können Sie geradlinigen Code schreiben, um die gesamte Logik für eine vollständige Interaktion einschließlich aller Schritte zu implementieren (genau wie Sie es natürlich tun würden, wenn Ihr Programm lokal ausgeführt würde).
Ich bin mir sicher, dass es viele Gründe gibt, warum so etwas (zumindest für den Moment) nicht praktikabel ist, aber ich versuche nur, Ihnen einige der Möglichkeiten aufzuzeigen. Wer weiß; Sobald Sie das Konzept erhalten haben, können Sie sich eine völlig neue Anwendung einfallen lassen, an die noch niemand gedacht hat!