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 # 1 delle fibre in Ruby è quello di implementare Enumerator
s, che sono una classe core di Ruby in Ruby 1.9. Questi sono incredibilmente utili.
In Ruby 1.9, se si chiama quasi qualsiasi metodo iteratore sulle classi principali, senza passare un blocco, esso restituisce 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)>
Questi Enumerator
s sono oggetti enumerabili, e loro each
metodi producono gli elementi che sono stati generati dal metodo iteratore originale, se fosse stato chiamato con un blocco. Nell'esempio che ho appena dato, l'Enumeratore restituito da reverse_each
ha un metodo each
che restituisce 3,2,1. L'enumeratore restituito da chars
restituisce "c", "b", "a" (e così via). Ma, a differenza del metodo iteratore originale, l'Enumerator può anche restituire gli elementi uno ad uno se si chiama next
su di esso più volte:
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 interne" e "iteratori esterni" (un buon la descrizione di entrambi è riportata nel libro "Pattern of Design" di Gang of Four). L'esempio sopra mostra che gli Enumeratori possono essere usati per trasformare un iteratore interno in uno esterno.
Questo è un modo per rendere i propri 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
Proviamo:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Aspetta un minuto ... fa niente sembra strano lì? Hai scritto le dichiarazioni yield
nel codice an_iterator
come codice lineare, ma l'Enumeratore può eseguirle uno alla volta. Tra le chiamate a next
, l'esecuzione di an_iterator
è "congelata". Ogni volta che si chiama next
, continua a scorrere fino alla seguente istruzione yield
, quindi "si blocca" nuovamente.
Riesci a indovinare come viene implementato? L'Enumeratore esegue il wrapping della chiamata a an_iterator
in una fibra e passa un blocco che sospende la fibra. Quindi ogni volta che an_iterator
si arrende al blocco, la fibra su cui è in esecuzione viene sospesa e l'esecuzione continua sul thread principale. La prossima volta che chiami next
, passa il controllo alla fibra, il blocco restituisce e an_iterator
continua da dove era stato interrotto.
Sarebbe istruttivo pensare a cosa sarebbe necessario fare senza fibre. OGNI classe che desiderava fornire iteratori interni ed esterni dovrebbe contenere codice esplicito per tenere traccia dello stato tra le chiamate a next
. Ogni chiamata al prossimo dovrebbe verificare quello stato e aggiornarlo prima di restituire un valore. Con le fibre, possiamo automaticamente convertire qualsiasi iteratore interno a uno esterno.
Questo non ha a che fare con la persistenza delle fibre, ma vorrei 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 lavoro sugli elementi prodotti 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]
Chiamando l'iteratore senza alcun blocco restituito un enumeratore, e quindi è possibile chiamare altri metodi enumerabili su questo.
Tornando alle fibre, è stato utilizzato il metodo take
da Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Semmai chiama che each
metodo, sembra che non dovrebbe mai tornare, giusto? Verificare:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Non so se questo utilizza le fibre sotto il cofano, ma potrebbe. Le fibre possono essere utilizzate per implementare liste infinite e una valutazione lenta di una serie. Per un esempio di alcuni metodi lazy definiti con Enumeratori, ne ho definiti alcuni qui: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
È inoltre possibile creare un impianto di coroutine generico utilizzando le fibre. Non ho mai 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 alternare tra loro come desiderato. Poiché il codice arbitrario può essere eseguito in una fibra, puoi chiamare il codice di terze parti su una fibra, quindi "congelarlo" e continuare a fare qualcos'altro quando richiama il codice che controlli.
Immagina qualcosa del genere: stai scrivendo un programma server che servirà molti clienti. Un'interazione completa con un cliente comporta una serie di passaggi, ma ogni connessione è transitoria e devi ricordare lo stato per ciascun client tra le connessioni. (Sembra programmazione web?)
Anziché memorizzare in modo esplicito tale stato e verificarlo ogni volta che un client si connette (per vedere quale passo successivo devono fare), è possibile mantenere una fibra per ogni client . Dopo aver identificato il cliente, recupereresti la fibra e la riavvii. Quindi alla fine di ogni connessione, si sospenderebbe la fibra e la si memorizzasse di nuovo. In questo modo, è possibile scrivere codice di linea per implementare tutta la logica per un'interazione completa, compresi tutti i passaggi (proprio come faresti naturalmente se il tuo programma fosse stato creato 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 di nuovo sto solo cercando di mostrarvi alcune delle possibilità. Chissà; una volta ottenuto il concetto, potresti trovare un'applicazione totalmente nuova che nessun altro ha ancora pensato!
Il vecchio esempio di fibonacci è solo il peggior motivatore possibile ;-) Esiste anche una formula che è possibile utilizzare per calcolare il numero _any_ fibonacci in O (1). – usr
Il problema non riguarda l'algoritmo, ma la comprensione delle fibre :) – fl00r