sonsuz listelerle foldl ve foldr davranışı


124

İçinde myAny işlev için kod bu soru kullandığı foldr. Yüklem karşılandığında sonsuz bir listeyi işlemeyi durdurur.

Foldl kullanarak yeniden yazdım:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(Adım işlevinin argümanlarının doğru şekilde tersine çevrildiğine dikkat edin.)

Ancak artık sonsuz listeleri işlemeyi durdurmuyor.

Apocalisp'in cevabında olduğu gibi işlevin çalışmasını izlemeye çalıştım :

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

Ancak, işlevin davranışı bu değildir. Bu nasıl yanlış?

Yanıtlar:


231

Nasıl foldfarklılıklar sıklıkla kafa karıştırıcı bir kaynak gibi görünüyor, bu yüzden işte daha genel bir genel bakış:

Bir [x1, x2, x3, x4 ... xn ]işlev fve tohum içeren n değerlerinin bir listesini katlamayı düşünün z.

foldl dır-dir:

  • Sol çağrışımlı :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Kuyruk özyinelemeli : Listeyi yineleyerek değeri daha sonra üretir
  • Tembel : Sonuç gerekene kadar hiçbir şey değerlendirilmez
  • Geriye doğru : foldl (flip (:)) []listeyi ters çevirir.

foldr dır-dir:

  • Sağ çağrışımlı :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • Bir bağımsız değişkene özyinelemeli : Her yineleme f, bir sonraki değere ve listenin geri kalanının katlanmasının sonucuna uygulanır .
  • Tembel : Sonuç gerekene kadar hiçbir şey değerlendirilmez
  • İleri : foldr (:) []değişmemiş bir liste döndürür.

Biraz ince nokta var burada bazen gezileri insanlar kadar o: Çünkü foldlolduğunu geriye her uygulama feklenir dışarıdan sonucunun; ve tembel olduğu için, sonuç alınana kadar hiçbir şey değerlendirilmez. Bu, sonucun herhangi bir bölümünü hesaplamak için Haskell'in önce iç içe geçmiş işlev uygulamalarının bir ifadesini oluşturarak tüm listeyi yinelediği , ardından en dıştaki işlevi değerlendirdiği ve argümanlarını gerektiği gibi değerlendirdiği anlamına gelir. Her fzaman ilk argümanını kullanırsa, bu Haskell'in en içteki terime kadar tekrar etmesi ve ardından her uygulamasını geriye doğru hesaplayarak çalışması gerektiği anlamına gelir f.

Bu, çoğu işlevsel programcının bildiği ve sevdiği verimli kuyruk özyinelemesinden açıkça farklıdır!

Aslında, foldlteknik olarak kuyruk özyinelemeli olsa da , sonuç ifadesinin tamamı herhangi bir şey değerlendirilmeden önce oluşturulduğundan, foldlyığın taşmasına neden olabilir!

Öte yandan, düşünün foldr. Aynı zamanda tembeldir, ancak ileriye doğru çalıştığı için , föğesinin her uygulaması sonucun içine eklenir . Dolayısıyla, sonucu hesaplamak için Haskell , ikinci argümanı katlanmış listenin geri kalanı olan tek bir işlev uygulaması oluşturur. Eğer fveri yapıcı, örneğin - - ikinci bağımsız değişken yavaş olan sonuç olacaktır aşamalı yavaş hesaplanan kat her adım, sadece bu değerlendirilir ihtiyacı o sonucun bir kısmı.

Öyleyse neden foldrbazen sonsuz listelerde işe yaramadığında işe yaradığını görebiliriz foldl: İlki, sonsuz bir listeyi tembel olarak başka bir tembel sonsuz veri yapısına dönüştürebilirken, ikincisi sonucun herhangi bir bölümünü oluşturmak için tüm listeyi incelemelidir. Öte yandan, foldrhemen her iki argümana da ihtiyaç duyan bir işlevle, örneğin (+), işe yarıyor (veya daha doğrusu çalışmıyor) foldl, değerlendirmeden önce büyük bir ifade oluşturmaya çok benzer .

Dolayısıyla dikkat edilmesi gereken iki önemli nokta şunlardır:

  • foldr tembel özyinelemeli bir veri yapısını diğerine dönüştürebilir.
  • Aksi takdirde, tembel kıvrımlar büyük veya sonsuz listelerde yığın taşmasıyla çökecektir.

Her foldrşeyi foldlyapabilir gibi göründüğünü ve daha fazlasını fark etmiş olabilirsiniz. Bu doğru! Aslında, foldl neredeyse işe yaramaz!

Peki ya büyük (ama sonsuz değil) bir listeyi katlayarak tembel olmayan bir sonuç üretmek istersek? Bunun için, bir istiyoruz sıkı kat , standart kütüphaneler thoughfully sağlamak :

foldl' dır-dir:

  • Sol çağrışımlı :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Kuyruk özyinelemeli : Listeyi yineleyerek değeri daha sonra üretir
  • Katı : Her işlev uygulaması süreç boyunca değerlendirilir
  • Geriye doğru : foldl' (flip (:)) []listeyi ters çevirir.

Çünkü foldl'olduğunu sıkı , Haskell olacak sonuç hesaplamak için değerlendirmek f yerine sol argüman vermektense, her adımda büyük, unevaluated ifadesini birikir. Bu bize istediğimiz olağan, verimli kuyruk özyinelemesini verir! Diğer bir deyişle:

  • foldl' büyük listeleri verimli bir şekilde katlayabilir.
  • foldl' sonsuz bir listede sonsuz bir döngüde asılı kalır (yığın taşmasına neden olmaz).

Haskell wiki'nin de bunu tartışan bir sayfası var .


6
Neden merak ediyorum Buraya geldim çünkü foldrdaha iyi olduğu foldliçinde Haskell tersi de doğrudur, Erlang (daha önce öğrendiği Haskell ). Yana Erlang tembel değildir ve işlevleri değildir curried yüzden foldlde Erlang gibi davranır foldl'üzerindedir. Bu harika bir cevap! İyi iş ve teşekkürler!
Siu Ching Pong - Asuka Kenji -

7
Bu çoğunlukla harika bir açıklamadır, ancak tanımını foldl"geri" ve foldr"ileri" olarak sorunlu buluyorum . Bu kısmen flip, (:)kıvrımın neden geriye doğru olduğunu gösteren resimde uygulanmış olmasından kaynaklanmaktadır . Doğal tepki, "elbette geriye doğru: flipBirbirini listeliyorsunuz!" Tam bir değerlendirmede ilk önce (en içteki) ilk liste öğesi için foldlgeçerli folduğundan "geriye doğru" olarak adlandırılanın da görülmesi garip . İlk önce son öğeye foldruygulayarak "geriye doğru çalışır" f.
Dave Abrahams

1
@DaveAbrahams: Sadece Arasında foldlve foldrve katılığına ve optimizasyonlar, birinci araçlar "en dış" yok sayarak değil "içteki". Bu nedenle foldrsonsuz listeleri işleyebilir ve yapamaz foldl- sağ kat ilk fliste öğesine ve (değerlendirilmemiş) kuyruğu katlamanın sonucuna uygulanır, sol kat ise en dıştaki uygulamasını değerlendirmek için tüm listeyi geçmelidir f.
CA McCann

1
Foldl'un foldl'a tercih edilebileceği herhangi bir durum olup olmadığını merak ediyorum, sizce var mı?
kazuoua

1
@ kazuoua, tembelliğin çok önemli olduğu yerde, ör last xs = foldl (\a z-> z) undefined xs.
Will Ness

28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

vb.

Sezgisel olarak, foldlher zaman "dışarıda" veya "solda" olduğundan önce genişler. Sonsuza dek.


10

Sen Haskell'ın belgelerinde görebilirsiniz burada foldl kuyruk özyinelemeli ve bir değer dönmeden önce sonraki parametreye kendisini çağıran beri, sonsuz bir liste geçtiyseniz hiç bitmeyecek o ...


0

Haskell'i bilmiyorum ama Scheme'de fold-right her zaman önce bir listenin son öğesi üzerinde 'hareket edecek'. Bu nedenle döngüsel liste için çalışmayacaktır (sonsuz olanla aynıdır).

fold-rightTail-recursive yazılabilir mi emin değilim , ancak herhangi bir döngüsel liste için bir yığın taşması almalısınız. fold-leftOTOH normalde kuyruk özyineleme ile uygulanır ve erken sonlandırılmazsa sonsuz bir döngüde sıkışıp kalacaktır.


3
Haskell'de tembellik nedeniyle durum farklı.
Lifu Huang
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.