Serat adalah sesuatu yang mungkin tidak akan pernah Anda gunakan secara langsung dalam kode level aplikasi. Mereka adalah primitif kontrol aliran yang dapat Anda gunakan untuk membuat abstraksi lain, yang kemudian Anda gunakan dalam kode tingkat yang lebih tinggi.
Mungkin penggunaan serat # 1 di Ruby adalah untuk mengimplementasikannya Enumerator
, yang merupakan kelas inti Ruby di Ruby 1.9. Ini sangat berguna.
Di Ruby 1.9, jika Anda memanggil hampir semua metode iterator pada kelas inti, tanpa melewatkan satu blok, itu akan mengembalikan 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)>
Ini Enumerator
adalah objek Enumerable, dan each
metode mereka menghasilkan elemen yang akan dihasilkan oleh metode iterator asli, jika dipanggil dengan sebuah blok. Dalam contoh yang baru saja saya berikan, Pencacah yang dikembalikan oleh reverse_each
memiliki each
metode yang menghasilkan 3,2,1. Pencacah dikembalikan dengan chars
hasil "c", "b", "a" (dan seterusnya). TAPI, tidak seperti metode iterator asli, Enumerator juga dapat mengembalikan elemen satu per satu jika Anda memanggilnya next
berulang kali:
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"
Anda mungkin pernah mendengar tentang "iterator internal" dan "iterator eksternal" (penjelasan yang baik tentang keduanya diberikan dalam buku Pola Desain "Gang of Four"). Contoh di atas menunjukkan bahwa Enumerator dapat digunakan untuk mengubah iterator internal menjadi yang eksternal.
Ini adalah salah satu cara untuk membuat enumerator Anda sendiri:
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
Ayo coba:
e = SomeClass.new.an_iterator
e.next # => 1
e.next # => 2
e.next # => 3
Tunggu sebentar ... apakah ada yang aneh di sana? Anda menulis yield
pernyataan dalam an_iterator
kode garis lurus, tetapi Pencacah dapat menjalankannya satu per satu . Di antara panggilan ke next
, eksekusi an_iterator
"dibekukan". Setiap kali Anda menelepon next
, panggilan terus berjalan hingga yield
pernyataan berikut , lalu "berhenti" lagi.
Dapatkah Anda menebak bagaimana ini diterapkan? Pencacah membungkus panggilan ke an_iterator
dalam serat, dan melewati blok yang menangguhkan serat . Jadi setiap kali an_iterator
menghasilkan ke blok, serat yang dijalankannya ditangguhkan, dan eksekusi berlanjut pada utas utama. Lain kali Anda memanggil next
, itu melewati kontrol ke serat, blok kembali , dan an_iterator
berlanjut di tempat yang ditinggalkannya.
Akan menjadi pelajaran untuk memikirkan apa yang diperlukan untuk melakukan ini tanpa serat. SETIAP kelas yang ingin menyediakan iterator internal dan eksternal harus berisi kode eksplisit untuk melacak status antara panggilan ke next
. Setiap panggilan ke next harus memeriksa status itu, dan memperbaruinya sebelum mengembalikan nilai. Dengan serat, kita dapat secara otomatis mengubah iterator internal menjadi yang eksternal.
Ini tidak ada hubungannya dengan serat persay, tetapi izinkan saya menyebutkan satu hal lagi yang dapat Anda lakukan dengan Pencacah: mereka memungkinkan Anda untuk menerapkan metode Enumerable tingkat tinggi ke iterator lain selain each
. Pikirkan tentang hal ini: biasanya semua metode Enumerable, termasuk map
, select
, include?
, inject
, dan sebagainya, semua pekerjaan pada unsur-unsur yang dihasilkan oleh each
. Tetapi bagaimana jika suatu objek memiliki iterator lain selain 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]
Memanggil iterator tanpa blok akan mengembalikan Enumerator, dan kemudian Anda dapat memanggil metode Enumerable lainnya.
Kembali ke serat, sudahkah Anda menggunakan take
metode dari Enumerable?
class InfiniteSeries
include Enumerable
def each
i = 0
loop { yield(i += 1) }
end
end
Jika ada yang memanggil each
metode itu, sepertinya itu tidak akan pernah kembali, bukan? Lihat ini:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Saya tidak tahu apakah ini menggunakan serat di bawah kap, tetapi bisa. Serat dapat digunakan untuk mengimplementasikan daftar tak terbatas dan evaluasi seri yang lambat. Untuk contoh beberapa metode malas yang didefinisikan dengan Pencacah, saya telah mendefinisikan beberapa di sini: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
Anda juga dapat membangun fasilitas coroutine untuk keperluan umum menggunakan serat. Saya belum pernah menggunakan coroutine di program saya, tapi itu konsep yang bagus untuk diketahui.
Saya harap ini memberi Anda gambaran tentang kemungkinannya. Seperti yang saya katakan di awal, serat adalah primitif kontrol aliran tingkat rendah. Mereka memungkinkan untuk mempertahankan beberapa "posisi" aliran kontrol dalam program Anda (seperti "penanda" yang berbeda di halaman buku) dan beralih di antara mereka sesuai keinginan. Karena kode arbitrer dapat berjalan di fiber, Anda dapat memanggil kode pihak ketiga pada fiber, lalu "membekukan" dan melanjutkan melakukan hal lain saat memanggil kembali ke kode yang Anda kontrol.
Bayangkan sesuatu seperti ini: Anda sedang menulis program server yang akan melayani banyak klien. Interaksi lengkap dengan klien melibatkan melalui serangkaian langkah, tetapi setiap koneksi bersifat sementara, dan Anda harus mengingat status untuk setiap klien antar koneksi. (Terdengar seperti pemrograman web?)
Daripada menyimpan status tersebut secara eksplisit, dan memeriksanya setiap kali klien terhubung (untuk melihat "langkah" berikutnya yang harus mereka lakukan adalah), Anda dapat mempertahankan fiber untuk setiap klien. Setelah mengidentifikasi klien, Anda akan mengambil fiber mereka dan memulainya kembali. Kemudian di akhir setiap sambungan, Anda akan menangguhkan serat dan menyimpannya lagi. Dengan cara ini, Anda dapat menulis kode garis lurus untuk mengimplementasikan semua logika untuk interaksi lengkap, termasuk semua langkah (seperti yang biasa Anda lakukan jika program Anda dibuat untuk dijalankan secara lokal).
Saya yakin ada banyak alasan mengapa hal seperti itu mungkin tidak praktis (setidaknya untuk saat ini), tetapi sekali lagi saya hanya mencoba menunjukkan kepada Anda beberapa kemungkinan. Siapa tahu; setelah Anda mendapatkan konsepnya, Anda mungkin menemukan beberapa aplikasi yang sama sekali baru yang belum pernah terpikirkan oleh orang lain!