Le fibre sono qualcosa che probabilmente non userete mai direttamente nel codice a livello di applicazione. Sono una primitiva di controllo del flusso che puoi usare per costruire altre astrazioni, che poi usi nel codice di livello superiore.
Probabilmente l'uso n. 1 delle fibre in Ruby è quello di implementare Enumerator
s, che sono una classe principale di Ruby in Ruby 1.9. Questi sono incredibilmente utili.
In Ruby 1.9, se chiami quasi tutti i metodi iteratori sulle classi principali, senza passare un blocco, restituirà un file 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)>
Questi Enumerator
sono oggetti enumerabili e i loro each
metodi producono gli elementi che sarebbero stati prodotti dal metodo iteratore originale, se fosse stato chiamato con un blocco. Nell'esempio che ho appena fornito, l'Enumeratore restituito da reverse_each
ha un each
metodo che restituisce 3,2,1. L'Enumeratore restituito chars
restituisce "c", "b", "a" (e così via). MA, a differenza del metodo iteratore originale, l'Enumeratore può anche restituire gli elementi uno per uno se lo chiami next
ripetutamente:
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"
Potreste aver sentito parlare di "iteratori interni" e "iteratori esterni" (una buona descrizione di entrambi è fornita nel libro "Gang of Four" Design Patterns). L'esempio sopra mostra che gli enumeratori possono essere utilizzati per trasformare un iteratore interno in uno esterno.
Questo è un modo per creare i tuoi enumeratori:
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
Proviamolo:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Aspetta un attimo ... c'è qualcosa di strano lì? Hai scritto le yield
istruzioni an_iterator
come codice lineare, ma l'Enumerator può eseguirle una alla volta . Tra le chiamate a next
, l'esecuzione di an_iterator
è "congelata". Ogni volta che si chiama next
, continua a scorrere fino yield
all'istruzione seguente , quindi "si blocca" di nuovo.
Riuscite a indovinare come viene implementato? L'Enumeratore avvolge la chiamata a an_iterator
in una fibra e passa un blocco che sospende la fibra . Quindi ogni volta che an_iterator
cede al blocco, la fibra su cui sta scorrendo viene sospesa e l'esecuzione continua sul thread principale. La prossima volta che si chiama next
, passa il controllo alla fibra, il blocco ritorna e an_iterator
continua da dove era stato interrotto.
Sarebbe istruttivo pensare a cosa sarebbe necessario per farlo senza fibre. OGNI classe che volesse fornire iteratori interni ed esterni dovrebbe contenere codice esplicito per tenere traccia dello stato tra le chiamate a next
. Ogni chiamata a next dovrebbe controllare quello stato e aggiornarlo prima di restituire un valore. Con le fibre, possiamo convertire automaticamente qualsiasi iteratore interno in uno esterno.
Questo non ha a che fare con le fibre persay, ma lasciatemi menzionare un'altra cosa che puoi fare con gli enumeratori: ti permettono di applicare metodi enumerabili di ordine superiore ad altri iteratori diversi da each
. Pensateci: normalmente tutti i metodi enumerabili, tra cui map
, select
, include?
, inject
, e così via, tutto il lavoro sugli elementi derivanti dai each
. Ma cosa succede se un oggetto ha altri iteratori diversi da 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]
La chiamata dell'iteratore senza blocco restituisce un enumeratore, quindi puoi chiamare altri metodi Enumerable su questo.
Tornando alle fibre, hai usato il take
metodo di Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Se qualcosa chiama quel each
metodo, sembra che non dovrebbe mai tornare, giusto? Controllalo:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Non so se questo utilizza fibre sotto il cofano, ma potrebbe. Le fibre possono essere utilizzate per implementare elenchi infiniti e una valutazione pigra di una serie. Per un esempio di alcuni metodi pigri definiti con Enumerators, ne ho definiti alcuni qui: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Puoi anche costruire una struttura coroutine per uso generale usando le fibre. Non ho ancora usato le coroutine in nessuno dei miei programmi, ma è un buon concetto da sapere.
Spero che questo ti dia un'idea delle possibilità. Come ho detto all'inizio, le fibre sono una primitiva di controllo del flusso di basso livello. Consentono di mantenere più "posizioni" del flusso di controllo all'interno del programma (come diversi "segnalibri" nelle pagine di un libro) e di passare da una all'altra come desiderato. Poiché il codice arbitrario può essere eseguito in una fibra, è possibile chiamare il codice di terze parti su una fibra, quindi "bloccarlo" e continuare a fare qualcos'altro quando richiama nel codice che controlli.
Immagina qualcosa del genere: stai scrivendo un programma server che servirà molti client. Un'interazione completa con un client implica l'esecuzione di una serie di passaggi, ma ogni connessione è transitoria e devi ricordare lo stato di ogni client tra le connessioni. (Suona come la programmazione web?)
Invece di memorizzare esplicitamente quello stato e controllarlo ogni volta che un client si connette (per vedere quale è il "passaggio" successivo che deve fare), è possibile mantenere una fibra per ogni client. Dopo aver identificato il cliente, dovresti recuperare la sua fibra e riavviarlo. Quindi, alla fine di ogni connessione, sospendere la fibra e memorizzarla di nuovo. In questo modo, potresti scrivere codice lineare per implementare tutta la logica per un'interazione completa, inclusi tutti i passaggi (proprio come faresti naturalmente se il tuo programma fosse fatto per essere eseguito localmente).
Sono sicuro che ci sono molte ragioni per cui una cosa del genere potrebbe non essere pratica (almeno per ora), ma ancora una volta sto solo cercando di mostrarti alcune delle possibilità. Chissà; una volta capito il concetto, potresti trovare un'applicazione totalmente nuova a cui nessun altro ha ancora pensato!