Haskell, özyinelemeyi uygulamak için tembel değerlendirme kullanır, bu nedenle her şeyi gerektiğinde bir değer sağlamak için bir söz olarak ele alır (buna thunk denir). Thunks yalnızca ilerlemek için gerektiği kadar azaltılır, artık değil. Bu, matematiksel olarak bir ifadeyi basitleştirme şeklinize benzer, bu yüzden onu bu şekilde düşünmek yardımcı olur. Değerlendirme sırası olması değil sizin koduyla belirtilen derleyici için kullanılan sadece kuyruk çağrı eleme konum bile daha zeki optimizasyonlar bir sürü yapmak için izin verir. Optimizasyon istiyorsanız ile derleyin -O2
!
facSlow 5
Bir örnek olay olarak nasıl değerlendirdiğimizi görelim :
facSlow 5
5 * facSlow 4
5 * (4 * facSlow 3)
5 * (4 * (3 * facSlow 2))
5 * (4 * (3 * (2 * facSlow 1)))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120
Yani endişelendiğiniz gibi, herhangi bir hesaplama yapılmadan önce bir sayı birikimimiz var, ancak endişelendiğinizden farklı olarak , facSlow
sona erdirmeyi bekleyen hiçbir işlev çağrısı yığını yok - her bir azaltma uygulanıyor ve kayboluyor, içinde bir yığın çerçevesi bırakıyor . uyanmak (çünkü (*)
katıdır ve bu nedenle ikinci argümanının değerlendirilmesini tetikler).
Haskell'in özyinelemeli işlevleri çok özyinelemeli bir şekilde değerlendirilmez! Etrafta dolaşan tek çağrı yığını, çarpımların kendisidir. Eğer (*)
sıkı bir veri yapıcısı olarak görülüyor, bu şekilde bilinen şeydir korunan (genellikle böyle olarak adlandırılır rağmen özyineleme olmayan - daha fazla erişime tarafından zorla veri kurucular onun ardından geriye ne -strict veri kurucular,).
Şimdi kuyruk özyinelemesine bakalım fac 5
:
fac 5
fac' 5 1
fac' 4 {5*1}
fac' 3 {4*{5*1}}
fac' 2 {3*{4*{5*1}}}
fac' 1 {2*{3*{4*{5*1}}}}
{2*{3*{4*{5*1}}}}
(2*{3*{4*{5*1}}})
(2*(3*{4*{5*1}}))
(2*(3*(4*{5*1})))
(2*(3*(4*(5*1))))
(2*(3*(4*5)))
(2*(3*20))
(2*60)
120
Böylece kuyruk özyinelemesinin tek başına size zaman veya yer kazandırmadığını görebilirsiniz. Yalnızca genelden daha fazla adım facSlow 5
atmakla kalmaz, aynı zamanda iç içe geçmiş bir thunk (burada gösterildiği gibi {...}
) oluşturur - bunun için fazladan bir alana ihtiyaç duyar - bu, gelecekteki hesaplamayı, yapılacak iç içe çarpımları açıklar.
Bu thunk sonra geçme tarafından ortaya olup bu yığın hesaplama yeniden dibe. Her iki sürüm için de çok uzun hesaplamalarda yığın taşmasına neden olma tehlikesi vardır.
Bunu elle optimize etmek istiyorsak, tek yapmamız gereken katı hale getirmek. Sen sıkı uygulama operatörünü kullanabilirsiniz $!
tanımlamak için
facSlim :: (Integral a) => a -> a
facSlim x = facS' x 1 where
facS' 1 y = y
facS' x y = facS' (x-1) $! (x*y)
Bu facS'
, ikinci argümanında katı olmaya zorlar . (İlk argümanında zaten katıdır çünkü hangi tanımın facS'
uygulanacağına karar vermek için bunun değerlendirilmesi gerekir .)
Bazen katılık çok yardımcı olabilir, bazen büyük bir hatadır çünkü tembellik daha etkilidir. İşte bu iyi bir fikir:
facSlim 5
facS' 5 1
facS' 4 5
facS' 3 20
facS' 2 60
facS' 1 120
120
Sanırım başarmak istediğin şey bu.
Özet
- Kodunuzu optimize etmek istiyorsanız, birinci adım,
-O2
- Kuyruk özyineleme, yalnızca hiçbir şey oluşmadığında iyidir ve sıkılık eklemek genellikle, uygunsa ve olduğunda bunu önlemeye yardımcı olur. Bu, daha sonra ihtiyaç duyulan bir sonucu aynı anda oluşturduğunuzda olur.
- Bazen kuyruk özyineleme kötü bir plandır ve korumalı özyineleme daha uygun olur, yani oluşturduğunuz sonuca parça parça, parça parça ihtiyaç duyulduğunda. Bkz bu soruyu hakkında
foldr
ve foldl
örneğin ve birbirlerine karşı onları test.
Bu ikisini deneyin:
length $ foldl1 (++) $ replicate 1000
"The size of intermediate expressions is more important than tail recursion."
length $ foldr1 (++) $ replicate 1000
"The number of reductions performed is more important than tail recursion!!!"
foldl1
kuyruk özyinelemelidir, oysa foldr1
korumalı özyinelemeyi gerçekleştirir , böylece ilk öğe sonraki işlem / erişim için hemen sunulur. (Birincisi, bir kerede sola "parantezler" (...((s+s)+s)+...)+s
oluşturarak girdi listesini sonuna kadar zorlar ve tüm sonuçlarından çok daha kısa sürede gelecekteki hesaplamaların büyük bir kısmını oluşturur; ikinci parantez yavaş yavaş s+(s+(...+(s+s)...))
girdiyi tüketerek parça parça sıralayın, böylece her şey sabit bir alanda optimizasyonlarla çalışabilir).
Hangi donanımı kullandığınıza bağlı olarak sıfır sayısını ayarlamanız gerekebilir.