Neden liflere ihtiyacımız var


101

Elyaflar için klasik bir örneğimiz var: Fibonacci sayılarının oluşturulması

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

Neden burada Fibere ihtiyacımız var? Bunu sadece aynı Proc ile yeniden yazabilirim (aslında kapanış)

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

Yani

10.times { puts fib.resume }

ve

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

sadece aynı sonucu döndürecektir.

Peki liflerin avantajları nelerdir? Fibers ile ne tür şeyler yazabilirim lambdalar ve diğer harika Ruby özellikleriyle yapamıyorum?


4
Eski fibonacci örneği mümkün olan en kötü motive edicidir ;-) O (1) 'de herhangi bir fibonacci sayısını hesaplamak için kullanabileceğiniz bir formül bile var .
usr

17
Sorun algoritmayla ilgili değil, lifleri
anlamakla

Yanıtlar:


230

Lifler, muhtemelen doğrudan uygulama düzeyinde kodda kullanmayacağınız bir şeydir. Bunlar, daha sonra daha yüksek seviyeli kodda kullanabileceğiniz diğer soyutlamaları oluşturmak için kullanabileceğiniz bir akış kontrolü ilkelidir.

Muhtemelen Ruby'deki fiberlerin 1 numaralı kullanımı, EnumeratorRuby 1.9'da çekirdek bir Ruby sınıfı olan s'yi uygulamaktır . Bunlar inanılmaz derecede faydalıdır.

Eğer çekirdek sınıfları üzerinde hemen hemen herhangi bir yineleyici yöntemi çağrısı Ruby 1.9 içinde, olmayan bir blok geçen bir döner 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)>

Bunlar EnumeratorNumaralandırılabilir nesnelerdir ve eachyöntemleri, bir blokla çağrılmış olsaydı, orijinal yineleme yöntemiyle verilecek olan öğeleri verir. Az önce verdiğim örnekte, Numaralandırıcı 3,2,1 veren reverse_eachbir eachyönteme sahip. Tarafından döndürülen Numaralandırıcı chars"c", "b", "a" (vb.) Sonucunu verir. AMA, orijinal yineleme yönteminin aksine Numaralandırıcı, nexttekrar tekrar çağırırsanız öğeleri tek tek de döndürebilir :

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"

"Dahili yineleyiciler" ve "harici yineleyiciler" i duymuş olabilirsiniz (her ikisinin de iyi bir açıklaması "Dörtlü Çete" Tasarım Kalıpları kitabında verilmiştir). Yukarıdaki örnek, Numaralandırıcıların dahili bir yineleyiciyi harici bir yineleyiciye dönüştürmek için kullanılabileceğini göstermektedir.

Bu, kendi numaralandırıcılarınızı oluşturmanın bir yoludur:

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

Hadi deneyelim:

e = SomeClass.new.an_iterator
e.next  # => 1
e.next  # => 2
e.next  # => 3

Bekle bir dakika ... orada tuhaf bir şey var mı? Sen yazdığı yieldifadeleri an_iteratordoğrusal kodu olarak ancak Listeleyicisi onlara çalıştırabilir birer birer . Yapılan aramalar arasında next, uygulama an_iterator"dondurulur". Her aradığınızda next, aşağıdaki yieldifadeye doğru çalışmaya devam eder ve ardından tekrar "donar".

Bunun nasıl uygulandığını tahmin edebilir misiniz? Numaralandırıcı, çağrıyı an_iteratorbir fiberde sarar ve fiberi askıya alan bir bloğu geçer . Böylece an_iteratorbloğa her dönüşünde, üzerinde çalıştığı fiber askıya alınır ve yürütme ana iş parçacığı üzerinde devam eder. Bir dahaki sefere çağırdığınızda next, kontrolü fibere aktarır, blok geri döner ve an_iteratorkaldığı yerden devam eder.

Lifler olmadan bunu yapmak için neyin gerekli olduğunu düşünmek öğretici olacaktır. Hem iç hem de dış yineleyiciler sağlamak isteyen HER sınıf, çağrılar arasındaki durumu takip etmek için açık kod içermelidir next. Bir sonrakine yapılacak her çağrı, bu durumu kontrol etmeli ve bir değer döndürmeden önce onu güncellemelidir. Fiberlerle, herhangi bir dahili yineleyiciyi otomatik olarak harici bir yineleyiciye dönüştürebiliriz.

Bunun fiber persay ile ilgisi yoktur, ancak Numaralandırıcılar ile yapabileceğiniz bir şeyden daha bahsetmeme izin verin: bunlar, dışındaki diğer yineleyiciler için daha yüksek mertebeden Numaralandırılabilir yöntemleri uygulamanıza izin verir each. Bir düşünün: Normalde tüm Enumerable yöntemleri, dahil map, select, include?, inject, ve benzeri tüm unsurları üzerinde çalışmak tarafından vermiştir each. Peki ya bir nesnenin dışında başka yineleyiciler varsa 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]

Yineleyiciyi blok olmadan çağırmak bir Numaralandırıcı döndürür ve ardından bununla ilgili diğer Numaralandırılabilir yöntemleri çağırabilirsiniz.

Liflere geri takedönersek, Enumerable'daki yöntemi kullandınız mı?

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

Bu eachyöntemi çağıran bir şey varsa , asla geri dönmemesi gerekiyor, değil mi? Şuna bir bak:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Bunun kaputun altında lif kullanıp kullanmadığını bilmiyorum, ama olabilir. Lifler, sonsuz listeleri ve bir serinin tembel değerlendirmesini uygulamak için kullanılabilir. Numaralandırıcılar ile tanımlanan bazı tembel yöntemlere bir örnek için, burada bazılarını tanımladım: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

Ayrıca lifleri kullanarak genel amaçlı bir koroutin tesisi de oluşturabilirsiniz. Henüz programlarımın hiçbirinde coroutine kullanmadım, ancak bu bilmek iyi bir kavram.

Umarım bu size olasılıklar hakkında bir fikir verir. Başlangıçta söylediğim gibi, lifler düşük seviyeli bir akış kontrol ilkelidir. Programınızda çoklu kontrol akışı "konumlarını" (bir kitabın sayfalarındaki farklı "yer imleri" gibi) korumayı ve bunlar arasında istenildiği gibi geçiş yapmayı mümkün kılarlar. Rasgele kod bir fiberde çalışabileceğinden, bir fiber üzerindeki 3. taraf kodunu çağırabilir ve ardından onu "dondurabilir" ve kontrol ettiğiniz koda geri döndüğünde başka bir şey yapmaya devam edebilirsiniz.

Şöyle bir şey hayal edin: birçok istemciye hizmet verecek bir sunucu programı yazıyorsunuz. Bir müşteri ile tam bir etkileşim, bir dizi adımdan geçmeyi içerir, ancak her bağlantı geçicidir ve bağlantılar arasındaki her müşteri için durumu hatırlamanız gerekir. (Web programlama gibi geliyor mu?)

Bu durumu açıkça depolamak ve bir istemci her bağlandığında bunu kontrol etmek yerine (yapmaları gereken bir sonraki "adımın" ne olduğunu görmek için), her istemci için bir fiber tutabilirsiniz. Müşteriyi belirledikten sonra, fiberlerini alır ve yeniden başlatırsınız. Sonra her bağlantının sonunda, fiberi askıya alır ve tekrar depolarsınız. Bu şekilde, tüm adımlar da dahil olmak üzere tam bir etkileşim için tüm mantığı uygulamak için düz satırlı kod yazabilirsiniz (tıpkı programınız yerel olarak çalıştırıldığında doğal olarak yapacağınız gibi).

Eminim böyle bir şeyin pratik olmamasının pek çok nedeni vardır (en azından şimdilik), ama yine size sadece bazı olasılıkları göstermeye çalışıyorum. Kim bilir; Konsepti bir kez edindikten sonra, henüz kimsenin aklına gelmeyen tamamen yeni bir uygulama bulabilirsin!


Cevabınız için teşekkür ederim! Öyleyse neden charssadece kapatmalarla veya diğer numaralandırıcıları uygulamıyorlar ?
fl00r

@ fl00r, daha fazla bilgi eklemeyi düşünüyorum, ancak bu cevabın çok uzun olup olmadığını bilmiyorum ... Daha fazlasını ister misin?
Alex D

13
Bu cevap o kadar iyi ki bir yere blog yazısı olarak yazılmalı.
Jason Voegele

1
GÜNCELLEME: EnumerableRuby 2.0'da bazı "tembel" yöntemler içerecek gibi görünüyor .
Alex D

2
takeelyaf gerektirmez. Bunun yerine, taken'inci verim sırasında kırılır. Bir blok içinde kullanıldığında, breakkontrolü bloğu tanımlayan çerçeveye döndürür. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matthew

22

Tanımlanmış bir giriş ve çıkış noktasına sahip kapakların aksine, lifler durumlarını koruyabilir ve birçok kez geri dönebilir (verim):

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

bunu yazdırır:

some code
return
received param: param
etc

Bu mantığın diğer Ruby özellikleriyle uygulanması daha az okunabilir olacaktır.

Bu özellik sayesinde, iyi fiber kullanımı, manuel işbirliğine dayalı çizelgeleme yapmaktır (İplik değişimi olarak). Ilya Grigorik'in, eşzamansız bir kitaplığın ( eventmachinebu durumda) eşzamansız yürütmenin IO-planlamasının avantajlarını kaybetmeden eşzamanlı bir API'ye nasıl dönüştürüleceğine dair iyi bir örneği var . İşte bağlantı .


Teşekkür ederim! Dokümanları okudum, bu yüzden tüm bu sihri fiberin içindeki birçok giriş ve çıkışla anlıyorum. Ama bunun hayatı kolaylaştırdığından emin değilim. Tüm bu özgeçmişleri ve getirileri takip etmeye çalışmanın iyi bir fikir olduğunu sanmıyorum. Çözmesi zor bir yumak gibi görünüyor. Bu yüzden, bu lif yumağının iyi bir çözüm olduğu durumlar olup olmadığını anlamak istiyorum. Eventmachine havalı ama lifleri anlamak için en iyi yer değil, çünkü önce tüm bu reaktör modellerini anlamalısın. Bu yüzden lifleri physical meaningdaha basit bir örnekte anlayabileceğime inanıyorum
fl00r
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.