Sol tarafı ne zaman ve ne zaman sağa katlamayı kullanacağınızı nasıl anlarsınız?


99

Sol kıvrımın sola yaslanmış ağaçlar ürettiğinin ve sağa kıvrılmanın sağa yaslanmış ağaçlar ürettiğinin farkındayım, ancak bir kıvrıma uzandığımda, bazen ne tür bir kıvrım olduğunu belirlemeye çalışırken baş ağrısına neden olan düşüncede tıkılıp kalıyorum. uygun. Genelde tüm sorunu çözerim ve sorunuma uygulandığı şekilde katlama işlevinin uygulanmasına adım atarım.

Yani bilmek istediğim şey:

  • Sola mı yoksa sağa mı katlanacağına karar vermek için bazı temel kurallar nelerdir?
  • Karşılaştığım sorun göz önüne alındığında hangi tür katlamayı kullanacağıma hızlıca nasıl karar verebilirim?

Örnek olarak Scala'da (PDF), katlama adı verilen ve eleman listelerinin bir listesini tek bir listede birleştiren bir işlev yazmak için kullanılan bir örnek vardır . Bu durumda, doğru katlama doğru seçimdir (listelerin birleştirilme şekli göz önüne alındığında), ancak bu sonuca varmak için biraz düşünmem gerekiyordu.

Bölünme, (işlevsel) programlamada çok yaygın bir eylem olduğu için, bu tür kararları hızlı ve güvenli bir şekilde verebilmek istiyorum. Peki ... herhangi bir ipucu?



1
Bu Q, özellikle Haskell ile ilgili olan bundan daha geneldir. Tembellik, sorunun cevabında büyük bir fark yaratır.
Chris Conway

Oh. ... Garip bir şekilde ben bu soruya bir Haskell etiketi görür gibi, ama sanırım değil
ephemient

Yanıtlar:


105

Bir katlamayı bir infix operatörü gösterimine aktarabilirsiniz (arasına yazarak):

Bu örnek, akümülatör işlevini kullanarak katlayın x

fold x [A, B, C, D]

böylece eşittir

A x B x C x D

Şimdi sadece operatörünüzün ilişkilendirilebilirliği hakkında mantık yürütmeniz gerekiyor (parantez koyarak!).

Eğer bir varsa sol ilişkisel operatörü, böyle parantez belirleriz

((A x B) x C) x D

Burada sol kıvrımı kullanıyorsunuz . Örnek (haskell tarzı sözde kod)

foldl (-) [1, 2, 3] == (1 - 2) - 3 == 1 - 2 - 3 // - is left-associative

Operatörünüz sağa ilişkiliyse ( sağ kıvrım ), parantezler şu şekilde ayarlanır:

A x (B x (C x D))

Örnek: Cons-Operator

foldr (:) [] [1, 2, 3] == 1 : (2 : (3 : [])) == 1 : 2 : 3 : [] == [1, 2, 3]

Genel olarak, aritmetik operatörler (çoğu operatör) sol ilişkilidir, bu yüzden foldldaha yaygındır. Ancak diğer durumlarda, infix notation + parantezler oldukça kullanışlıdır.


6
Eh, ne tarif aslında foldl1ve foldr1Haskell ( foldlve foldrharici ilk değerleri) ve Haskell'ın "eksileri" denir (:)değil (::), ama aksi takdirde bu doğrudur. Haskell'in ek olarak, foldl'/ foldl1''nin katı varyantları olan foldl/ sağladığını eklemek isteyebilirsiniz foldl1, çünkü tembel aritmetik her zaman arzu edilmez.
ephemient

Üzgünüm, bu soruda "Haskell" etiketi gördüğümü sandım, ama orada değil. Yorumum Haskell değilse pek bir anlam ifade etmiyor ...
ephemient

@ephemient Gördün. Bu "haskell tarzı sözde kod". :)
laughing_man

Kıvrımlar arasındaki farkla ilgili şimdiye kadar gördüğüm en iyi cevap.
AleXoundOS

60

Olin Shivers , "foldl temel liste yineleyicidir" ve "foldr temel liste özyineleme operatörüdür" diyerek bunları farklılaştırdı. Foldl'un nasıl çalıştığına bakarsanız:

((1 + 2) + 3) + 4

biriktiricinin (kuyruk özyinelemeli yinelemede olduğu gibi) oluşturulduğunu görebilirsiniz. Buna karşılık, foldr devam eder:

1 + (2 + (3 + 4))

burada temel durum 4'e geçişi görebilir ve buradan sonucu oluşturabilirsin.

Bu yüzden, bir pratik kural koyuyorum: eğer bir liste yinelemesine benziyorsa, kuyruk özyinelemeli biçimde yazması kolay olacaksa, foldl gitmenin yoludur.

Ama gerçekten bu, muhtemelen en çok kullandığınız operatörlerin çağrışımından anlaşılacaktır. Sol ilişkiliyse, foldl kullanın. Doğru ilişkiliyse, foldr kullanın.


29

Diğer posterler iyi cevaplar verdiler ve daha önce söylediklerini tekrar etmeyeceğim. Sorunuzda bir Scala örneği verdiğiniz gibi, Scala'ya özgü bir örnek vereceğim. Gibi hileler zaten söyledi, bir foldRightihtiyaçları korumak için n-1yığın çerçeveler, nlistenizin uzunluğudur ve bu kolayca yığın taşması yol açabilir - ve hatta kuyruk özyineleme bu sizi kurtarabilir.

A List(1,2,3).foldRight(0)(_ + _)şuna indirgenir:

1 + List(2,3).foldRight(0)(_ + _)        // first stack frame
    2 + List(3).foldRight(0)(_ + _)      // second stack frame
        3 + 0                            // third stack frame 
// (I don't remember if the JVM allocates space 
// on the stack for the third frame as well)

while List(1,2,3).foldLeft(0)(_ + _):

(((0 + 1) + 2) + 3)

uygulamasındaList yapıldığı gibi yinelemeli olarak hesaplanabilir .

Scala olarak katı bir şekilde değerlendirilen bir dilde, bir foldRightbüyük listeler için yığını kolayca havaya uçurabilirken, bir foldLeftolmayacaktır.

Misal:

scala> List.range(1, 10000).foldLeft(0)(_ + _)
res1: Int = 49995000

scala> List.range(1, 10000).foldRight(0)(_ + _)
java.lang.StackOverflowError
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRight(List.scala:1081)
        at scala.List.foldRig...

Bu nedenle benim temel kuralım - belirli bir ilişkilendirmeye sahip olmayan operatörler için foldLeft, en azından Scala'da her zaman kullanın . Aksi takdirde, cevaplarda verilen diğer tavsiyelerle devam edin;).


13
Bu daha önce doğruydu, ancak Scala'nın mevcut sürümlerinde, foldRight'ın listenin tersine çevrilmiş bir kopyasına foldLeft uygulamak için değiştirildi. Örneğin, 2.10.3'te , github.com/scala/scala/blob/v2.10.3/src/library/scala/… . Bu değişiklik 2013'ün başlarında yapılmış gibi görünüyor - github.com/scala/scala/commit/… .
Semih Kapoor

4

Ayrıca kayda değer (ve bunun bariz olanı biraz ifade ettiğini anlıyorum), değişmeli bir operatör durumunda ikisi hemen hemen eşdeğerdir. Bu durumda bir katlama daha iyi bir seçim olabilir:

foldl: (((1 + 2) + 3) + 4)her işlemi hesaplayabilir ve birikmiş değeri ileriye taşıyabilir

foldr: hesaplama (1 + (2 + (3 + 4)))için 1 + ?ve 2 + ?hesaplamadan önce bir yığın çerçevesinin açılmasına 3 + 4ihtiyaç duyar, sonra geri dönüp her biri için hesaplama yapması gerekir.

İşlevsel diller veya derleyici optimizasyonları konusunda, bunun gerçekten bir fark yaratıp yaratmayacağını söyleyecek kadar uzman değilim, ancak değişmeli operatörlerle bir foldl kullanmak kesinlikle daha temiz görünüyor.


1
Ekstra yığın çerçeveleri, büyük listeler için kesinlikle bir fark yaratacaktır. Yığın çerçeveleriniz işlemcinin önbelleğinin boyutunu aşarsa, önbellek eksikleriniz performansı etkileyecektir. Liste çift bağlantılı olmadıkça, foldr'yi kuyruk-özyinelemeli bir fonksiyon yapmak biraz zor, bu yüzden, olmamak için bir neden olmadıkça foldl'ı kullanmalısınız.
A. Levy

4
Haskell'in tembel doğası bu analizi karıştırıyor. Katlanan işlev ikinci parametrede katı değilse, o foldrzaman çok daha verimli olabilir foldlve fazladan yığın çerçevesi gerektirmez.
ephemient

2
Üzgünüm, bu soruda "Haskell" etiketi gördüğümü sandım, ama orada değil. Yorumum Haskell değilse pek bir anlam ifade etmiyor ...
ephemient
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.