Mengapa kita membutuhkan serat

101

Untuk Fibers kami punya contoh klasik: menghasilkan angka Fibonacci

fib = Fiber.new do  
  x, y = 0, 1 
  loop do  
    Fiber.yield y 
    x,y = y,x+y 
  end 
end

Mengapa kita membutuhkan Serat di sini? Saya dapat menulis ulang ini hanya dengan Proc yang sama (sebenarnya penutupan)

def clsr
  x, y = 0, 1
  Proc.new do
    x, y = y, x + y
    x
  end
end

Begitu

10.times { puts fib.resume }

dan

prc = clsr 
10.times { puts prc.call }

akan mengembalikan hasil yang sama.

Lantas apa saja keunggulan serat. Hal apa yang dapat saya tulis dengan Serat yang tidak dapat saya lakukan dengan lambda dan fitur Ruby keren lainnya?

fl00r
sumber
4
Contoh fibonacci tua hanya mungkin motivator terburuk ;-) Bahkan ada rumus yang dapat digunakan untuk menghitung setiap nomor fibonacci di O (1).
usr
17
Masalahnya bukan tentang algoritme, tetapi tentang pemahaman serat :)
fl00r

Jawaban:

230

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 Enumeratoradalah objek Enumerable, dan eachmetode 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_eachmemiliki eachmetode yang menghasilkan 3,2,1. Pencacah dikembalikan dengan charshasil "c", "b", "a" (dan seterusnya). TAPI, tidak seperti metode iterator asli, Enumerator juga dapat mengembalikan elemen satu per satu jika Anda memanggilnya nextberulang 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 yieldpernyataan dalam an_iteratorkode 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 yieldpernyataan berikut , lalu "berhenti" lagi.

Dapatkah Anda menebak bagaimana ini diterapkan? Pencacah membungkus panggilan ke an_iteratordalam serat, dan melewati blok yang menangguhkan serat . Jadi setiap kali an_iteratormenghasilkan 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_iteratorberlanjut 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 takemetode dari Enumerable?

class InfiniteSeries
  include Enumerable
  def each
    i = 0
    loop { yield(i += 1) }
  end
end

Jika ada yang memanggil eachmetode 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!

Alex D
sumber
Terima kasih atas jawaban Anda! Jadi mengapa mereka tidak menerapkan charsatau enumerator lain hanya dengan penutupan?
fl00r
@ fl00r, saya berpikir untuk menambahkan lebih banyak informasi lagi, tetapi saya tidak tahu apakah jawaban ini sudah terlalu panjang ... apakah Anda mau lagi?
Alex D
13
Jawaban ini sangat bagus sehingga harus ditulis sebagai posting blog di suatu tempat, methinks.
Jason Voegele
1
UPDATE: Sepertinya Enumerableakan menyertakan beberapa metode "malas" di Ruby 2.0.
Alex D
2
taketidak membutuhkan serat. Sebaliknya, takecukup istirahat selama hasil ke-n. Saat digunakan di dalam blok, breakkembalikan kontrol ke bingkai yang mendefinisikan blok. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matius
22

Tidak seperti closure, yang memiliki titik masuk dan keluar yang ditentukan, fiber dapat mempertahankan status dan pengembaliannya (hasil) berkali-kali:

f = Fiber.new do
  puts 'some code'
  param = Fiber.yield 'return' # sent parameter, received parameter
  puts "received param: #{param}"
  Fiber.yield #nothing sent, nothing received 
  puts 'etc'
end

puts f.resume
f.resume 'param'
f.resume

mencetak ini:

some code
return
received param: param
etc

Penerapan logika ini dengan fitur ruby ​​lainnya akan kurang terbaca.

Dengan fitur ini, penggunaan fiber yang baik adalah dengan melakukan penjadwalan kooperatif manual (sebagai pengganti Thread). Ilya Grigorik memiliki contoh yang baik tentang bagaimana mengubah pustaka asinkron ( eventmachinedalam hal ini) menjadi apa yang tampak seperti API sinkron tanpa kehilangan keuntungan dari penjadwalan IO dari eksekusi asinkron. Ini tautannya .

Aliaksei Kliuchnikau
sumber
Terima kasih! Saya membaca dokumen, jadi saya memahami semua keajaiban ini dengan banyak entri dan jalan keluar di dalam fiber. Tetapi saya tidak yakin bahwa hal ini membuat hidup lebih mudah. Saya tidak berpikir bahwa mencoba mengikuti semua resume dan hasil ini adalah ide yang baik. Sepertinya ada tanda yang sulit diurai. Jadi saya ingin memahami jika ada kasus di mana petunjuk serat ini adalah solusi yang baik. Mesin event itu keren tapi bukan tempat terbaik untuk memahami serat, karena pertama-tama Anda harus memahami semua pola reaktor ini. Jadi saya yakin saya bisa memahami serat physical meaningdalam contoh yang lebih sederhana
fl00r