Tembel değerlendirmenin neden yararlı olduğunu uzun zamandır merak ediyorum. Henüz bana mantıklı bir şekilde açıklama yapacak kimsem yok; çoğunlukla "bana güvenmek" için kaynar.
Not: Hafızayı kastetmiyorum.
Tembel değerlendirmenin neden yararlı olduğunu uzun zamandır merak ediyorum. Henüz bana mantıklı bir şekilde açıklama yapacak kimsem yok; çoğunlukla "bana güvenmek" için kaynar.
Not: Hafızayı kastetmiyorum.
Yanıtlar:
Çoğunlukla daha verimli olabileceği için - kullanılmayacaklarsa değerlerin hesaplanmasına gerek yoktur. Örneğin, bir işleve üç değer aktarabilirim, ancak koşullu ifadelerin sırasına bağlı olarak, gerçekte yalnızca bir alt küme kullanılabilir. C gibi bir dilde, her üç değer de yine de hesaplanacaktır; ancak Haskell'de yalnızca gerekli değerler hesaplanır.
Ayrıca sonsuz listeler gibi harika şeylere izin verir. C gibi bir dilde sonsuz bir listeye sahip olamam ama Haskell'de bu sorun değil. Sonsuz listeler matematiğin belirli alanlarında oldukça sık kullanılır, bu yüzden onları manipüle etme yeteneğine sahip olmak yararlı olabilir.
Tembel değerlendirmenin faydalı bir örneği şunların kullanımıdır quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
Şimdi listenin minimumunu bulmak istiyorsak, tanımlayabiliriz
minimum ls = head (quickSort ls)
İlk olarak listeyi sıralar ve sonra listenin ilk öğesini alır. Ancak, tembel değerlendirme nedeniyle sadece kafa hesaplanır. Örneğin, listenin [2, 1, 3,]
minimumunu alırsak, quickSort önce ikiden küçük olan tüm öğeleri filtreleyecektir. Daha sonra bunun üzerinde (tekli listeyi [1] döndürerek) zaten yeterli olan quickSort yapar. Tembel değerlendirme nedeniyle, geri kalanı asla sıralanmaz ve çok fazla hesaplama süresi tasarrufu sağlar.
Bu elbette çok basit bir örnek ama tembellik çok büyük programlar için de aynı şekilde çalışıyor.
Bununla birlikte, tüm bunların bir dezavantajı vardır: programınızın çalışma zamanı hızını ve bellek kullanımını tahmin etmek zorlaşır. Bu, tembel programların daha yavaş olduğu veya daha fazla bellek aldığı anlamına gelmez, ancak bilmek iyidir.
take k $ quicksort list
sadece O (n + k log k) zamanını alır, burada n = length list
. Tembel olmayan bir karşılaştırma sıralamasıyla, bu her zaman O (n log n) süresini alır.
Tembel değerlendirmeyi birçok şey için yararlı buluyorum.
Birincisi, mevcut tüm tembel diller saftır çünkü tembel bir dilde yan etkiler hakkında mantık yürütmek çok zordur.
Saf diller, eşitlikçi akıl yürütmeyi kullanarak işlev tanımları hakkında akıl yürütmenize izin verir.
foo x = x + 3
Maalesef tembel olmayan bir ortamda, tembel bir ayara göre daha fazla ifade geri dönmez, bu nedenle bu, ML gibi dillerde daha az yararlıdır. Ancak tembel bir dilde eşitlik konusunda güvenle akıl yürütebilirsiniz.
İkinci olarak, Haskell gibi tembel dillerde makine öğrenimindeki 'değer kısıtlaması' gibi pek çok şeye gerek yoktur. Bu, söz diziminde büyük bir dağınıklığa yol açar. ML benzeri diller, var veya fun gibi anahtar kelimeler kullanmalıdır. Haskell'de bu şeyler tek bir fikre indirgeniyor.
Üçüncüsü, tembellik, parçalar halinde anlaşılabilecek çok işlevsel bir kod yazmanıza izin verir. Haskell'de aşağıdaki gibi bir işlev gövdesi yazmak yaygındır:
foo x y = if condition1
then some (complicated set of combinators) (involving bigscaryexpression)
else if condition2
then bigscaryexpression
else Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
Bu, bir işlevin gövdesini anlamanıza rağmen 'yukarıdan aşağıya' çalışmanıza izin verir. Makine öğrenimi benzeri diller, sizi let
kesinlikle değerlendirilen a'yı kullanmaya zorlar . Sonuç olarak, let cümlesini işlevin ana gövdesine 'kaldırmaya' cesaret edemezsiniz, çünkü eğer pahalıysa (veya yan etkileri varsa) her zaman değerlendirilmesini istemezsiniz. Haskell, ayrıntıları where cümlesine 'itebilir' çünkü bu cümlenin içeriğinin yalnızca ihtiyaç duyulduğunda değerlendirileceğini bilir.
Pratikte, korumaları kullanma eğilimindeyiz ve bunu daha da ileri götürerek:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
Dördüncüsü, tembellik bazen belirli algoritmaların çok daha zarif bir ifadesini sunar. Haskell'de tembel bir 'hızlı sıralama' tek satırlık bir programdır ve yalnızca ilk birkaç öğeye bakarsanız, yalnızca bu öğeleri seçme maliyetiyle orantılı maliyetler ödersiniz. Hiçbir şey kesinlikle bunu yapmanızı engellemez, ancak aynı asimptotik performansı elde etmek için muhtemelen her seferinde algoritmayı yeniden kodlamanız gerekir.
Beşinci olarak, tembellik, dilde yeni kontrol yapıları tanımlamanıza izin verir. Katı bir dilde yapı gibi yeni bir 'eğer .. o zaman .. başka ..' yazamazsınız. Şunun gibi bir işlev tanımlamaya çalışırsanız:
if' True x y = x
if' False x y = y
katı bir dilde, bu durumda her iki dal da koşul değerinden bağımsız olarak değerlendirilir. Döngüleri düşündüğünüzde daha da kötüleşiyor. Tüm katı çözümler, dilin size bir tür teklif veya açık lambda yapısı sunmasını gerektirir.
Son olarak, aynı şekilde, monadlar gibi tip sistemdeki yan etkilerle başa çıkmak için en iyi mekanizmalardan bazıları gerçekten ancak tembel bir ortamda etkili bir şekilde ifade edilebilir. Bu, F # İş Akışlarının karmaşıklığını Haskell Monad'ları ile karşılaştırarak görülebilir. (Bir monad'ı katı bir dilde tanımlayabilirsiniz, ancak maalesef tembellik eksikliği nedeniyle bir veya iki monad yasasında başarısız olursunuz ve buna kıyasla İş Akışları bir ton katı bagaj alır.)
let
tehlikeli bir canavardır, R6RS şemasında #f
düğümü bağlamanın kesin olarak bir döngüye yol açtığı her yerde sizin teriminizde rastgele ifadelerin görünmesine izin verir ! Kelime oyunu amaçlanmamıştır, ancak let
tembel bir dilde kesinlikle daha özyinelemeli bağlamalar mantıklıdır. where
Kesinlik, SCC dışında, göreceli etkileri hiçbir şekilde sıralamak için hiçbir yolun olmadığı gerçeğini daha da kötüleştirir , bu bir ifade düzeyinde bir yapıdır, etkileri kesinlikle herhangi bir sırayla olabilir ve saf bir dile sahip olsanız bile, #f
konu. where
Yerel olmayan kaygılarla kodunuzu katı bir şekilde çözer.
ifFunc(True, x, y)
her ikisi biçecek x
ve y
bunun yerine sadece bir x
.
Normal düzen değerlendirme ile tembel değerlendirme arasında bir fark vardır (Haskell'de olduğu gibi).
square x = x * x
Aşağıdaki ifade değerlendirilerek ...
square (square (square 2))
... istekli bir değerlendirme ile:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
... normal sipariş değerlendirmesiyle:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256
... tembel değerlendirme ile:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256
Bunun nedeni, tembel değerlendirmenin sözdizimi ağacına bakması ve ağaç dönüşümleri yapmasıdır ...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
... oysa normal sıra değerlendirmesi yalnızca metinsel genişletmeler yapar.
Bu nedenle, tembel değerlendirmeyi kullanırken, performans istekli değerlendirmeye eşdeğerken (en azından O-notasyonunda) daha güçlü hale geliriz (değerlendirme diğer stratejilerden daha sık sona erer).
CPU ile ilgili tembel değerlendirme, RAM ile ilgili çöp toplama ile aynı şekilde. GC, sınırsız miktarda belleğiniz varmış gibi davranmanıza ve böylece bellekte ihtiyaç duyduğunuz kadar çok nesne talep etmenize olanak tanır. Çalışma zamanı, kullanılamayan nesneleri otomatik olarak geri alır. LE, sınırsız hesaplama kaynağınız varmış gibi davranmanıza izin verir - ihtiyaç duyduğunuz kadar hesaplama yapabilirsiniz. Çalışma zamanı sadece (belirli bir durum için) gereksiz hesaplamaları yürütmeyecektir.
Bu "numara yapan" modellerin pratik avantajı nedir? Geliştiriciyi kaynakları yönetmekten (bir dereceye kadar) serbest bırakır ve kaynaklarınızdan bazı standart kodlar kaldırır. Ancak daha da önemlisi, çözümünüzü daha geniş bağlamlarda verimli bir şekilde yeniden kullanabilmenizdir.
S sayıları ve N sayılarından oluşan bir listeniz olduğunu hayal edin. S listesinden N numarasına en yakın olanı M'yi bulmanız gerekir. İki bağlamınız olabilir: tek N ve bazı L Ns listesi (L'deki her N için ei S'de en yakın M'ye bakarsınız). Tembel değerlendirmeyi kullanırsanız, S'yi sıralayabilir ve M'den N'ye en yakın olanı bulmak için ikili aramayı uygulayabilirsiniz. İyi bir tembel sıralama için, tek N ve O (ln (boyut (S))) için O (boyut (S)) adımları gerektirecektir * Eşit olarak dağıtılmış L için (size (S) + size (L))) adımlar. Optimal verimliliği elde etmek için tembel bir değerlendirmeye sahip değilseniz, her bağlam için algoritma uygulamanız gerekir.
Simon Peyton Jones'a inanıyorsanız, tembel değerlendirme kendi başına önemli değil, sadece tasarımcıları dili saf tutmaya zorlayan bir 'saç tişörtü' olarak önemli . Kendimi bu bakış açısına sempati duyuyorum.
Richard Bird, John Hughes ve daha az bir ölçüde Ralf Hinze, tembel bir değerlendirmeyle harika şeyler yapabiliyor. Onların çalışmalarını okumak, onu takdir etmenize yardımcı olacaktır. Bird'ün muhteşem Sudoku çözücüsü ve Hughes'un Why Functional Programming Matters konulu makalesi iyi bir başlangıç noktasıdır .
IO
main
String -> String
IO
?
Bir tic-tac-toe programı düşünün. Bunun dört işlevi vardır:
Bu, endişelerin güzel ve net bir şekilde ayrılmasını sağlar. Özellikle, hamle oluşturma işlevi ve tahta değerlendirme işlevleri, oyunun kurallarını anlamaya ihtiyaç duyan yegane işlevlerdir: hareket ağacı ve minimax işlevleri tamamen yeniden kullanılabilir.
Şimdi tic-tac-toe yerine satrancı uygulamayı deneyelim. "Hevesli" (yani geleneksel) bir dilde bu işe yaramaz çünkü taşıma ağacı belleğe sığmaz. Bu nedenle, artık tahta değerlendirme ve hareket oluşturma işlevlerinin, hareket ağacı ve minimum eksen mantığı ile karıştırılması gerekiyor çünkü hangi hareketlerin üretileceğine karar vermek için minimax mantığının kullanılması gerekiyor. Güzel temiz modüler yapımız yok oluyor.
Bununla birlikte, tembel bir dilde, hareket ağacının öğeleri yalnızca minimax işlevinden gelen taleplere yanıt olarak üretilir: en üst öğede minimax'ı serbest bırakmadan önce tüm taşıma ağacının oluşturulması gerekmez. Yani temiz modüler yapımız hala gerçek bir oyunda çalışıyor.
İşte henüz tartışmada gündeme getirildiğine inanmadığım iki nokta daha.
Tembellik, eşzamanlı bir ortamda bir senkronizasyon mekanizmasıdır. Bazı hesaplamalara referans oluşturmanın ve sonuçlarını birçok iş parçacığı arasında paylaşmanın hafif ve kolay bir yoludur. Birden fazla iş parçacığı değerlendirilmemiş bir değere erişmeye çalışırsa, bunlardan yalnızca biri onu yürütür ve diğerleri buna göre engelleyerek değeri kullanılabilir hale geldiğinde alır.
Tembellik, veri yapılarını saf bir ortamda amorti etmek için esastır. Bu, Okasaki tarafından Purely Functional Data Structures'ta ayrıntılı olarak açıklanmıştır, ancak temel fikir, tembel değerlendirmenin, belirli veri yapılarını verimli bir şekilde uygulamamıza izin veren kritik bir kontrollü mutasyon biçimi olmasıdır. Bizi saf saç tişörtü giymeye zorlayan tembellikten sık sık söz ederken, diğer yol da geçerlidir: bunlar bir çift sinerjik dil özelliğidir.
Bilgisayarınızı açtığınızda ve Windows, sabit sürücünüzdeki her dizini Windows Gezgini'nde açmayı ve siz belirli bir dizinin gerekli olduğunu veya belirli bir programın gerekli olduğunu belirtene kadar bilgisayarınızda yüklü olan her bir programı başlatmaktan kaçınırsa, "tembel" değerlendirmedir.
"Tembel" değerlendirme, işlemleri gerektiği zaman ve gerektiği şekilde gerçekleştirmektir. Bir programlama dilinin veya kitaplığının bir özelliği olduğunda kullanışlıdır, çünkü genellikle her şeyi önceden hesaplamaktan çok kendi başınıza tembel değerlendirmeyi uygulamak daha zordur.
Bunu düşün:
if (conditionOne && conditionTwo) {
doSomething();
}
DoSomething () yöntemi yalnızca conditionOne true ve conditionTwo true ise çalıştırılır . ConditionOne'ın yanlış olduğu durumda, nedenTwo koşulunun sonucunu hesaplamanız gerekiyor? DurumTwo'nun değerlendirilmesi, bu durumda, özellikle de durumunuz bir yöntem sürecinin sonucuysa, zaman kaybı olacaktır.
Tembel değerlendirme ilgisine bir örnek bu ...
Verimliliği artırabilir. Bu apaçık görünen ama aslında en önemlisi değil. (Ayrıca tembelliğin de verimliliği öldürebileceğini unutmayın - bu gerçek hemen açık değildir. Ancak, çok sayıda geçici sonucu hemen hesaplamak yerine depolayarak büyük miktarda RAM kullanabilirsiniz.)
Dile sabit kodlanmak yerine, normal kullanıcı düzeyi kodunda akış denetimi yapılarını tanımlamanıza olanak tanır. (Örneğin, Java'nın for
döngüleri vardır; Haskell'in bir for
işlevi vardır. Java'nın istisna işlemesi vardır; Haskell'in çeşitli istisna türleri vardır. C # vardır goto
; Haskell'in devamı monad'ı vardır ...)
Ne kadar veri üretileceğine karar vermek için algoritmadan veri üretme algoritmasını ayırmanıza olanak tanır . Kavramsal olarak sonsuz bir sonuç listesi oluşturan bir işlev ve bu listenin ihtiyaç duyduğu kadar çoğunu işleyen başka bir işlev yazabilirsiniz. Daha da önemlisi, her iki eylemi aynı anda birleştiren 5 x 5 = 25 işlevi manuel olarak kodlamak yerine, beş jeneratör işlevine ve beş tüketici işlevine sahip olabilirsiniz ve herhangi bir kombinasyonu verimli bir şekilde üretebilirsiniz. (!) Ayrılmanın iyi bir şey olduğunu hepimiz biliyoruz.
Aşağı yukarı sizi saf işlevsel bir dil tasarlamaya zorlar . Kestirme yollar kullanmak her zaman caziptir, ancak tembel bir dilde, en ufak bir kirlilik, kodunuzu çılgınca öngörülemez hale getirir ve bu da kestirme kullanmanın önüne geçilmesini sağlar.
Tembelliğin en büyük faydalarından biri, makul amortismana tabi sınırlarla değişmez veri yapıları yazma yeteneğidir. Basit bir örnek, değişmez bir yığın (F # kullanarak):
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let rec append x y =
match x with
| EmptyStack -> y
| StackNode(hd, tl) -> StackNode(hd, append tl y)
Kod mantıklıdır, ancak iki yığın eklemek x ve y en iyi, en kötü ve ortalama durumlarda O (x uzunluğu) süresi alır. İki yığın eklemek yekpare bir işlemdir, x yığınındaki tüm düğümlere dokunur.
Veri yapısını tembel bir yığın olarak yeniden yazabiliriz:
type 'a lazyStack =
| StackNode of Lazy<'a * 'a lazyStack>
| EmptyStack
let rec append x y =
match x with
| StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
| Empty -> y
lazy
yapıcısındaki kodun değerlendirmesini askıya alarak çalışır. Kullanılarak değerlendirildikten sonra .Force()
, dönüş değeri önbelleğe alınır ve sonraki her gün yeniden kullanılır .Force()
.
Geç sürümde, ekler bir O (1) işlemidir: 1 düğüm döndürür ve listenin asıl yeniden oluşturulmasını askıya alır. Bu listenin başını aldığınızda, düğümün içeriğini değerlendirecek, kafayı geri döndürmeye zorlayacak ve kalan elemanlarla bir süspansiyon oluşturacaktır, bu nedenle listenin başını almak bir O (1) işlemidir.
Bu nedenle, tembel listemiz sürekli bir yeniden inşa halindedir, tüm unsurlarını geçene kadar bu listeyi yeniden inşa etmenin maliyetini ödemezsiniz. Tembellik kullanarak, bu liste O (1) düşünmeyi ve eklemeyi destekler. İlginç bir şekilde, düğümleri erişilene kadar değerlendirmediğimiz için, potansiyel olarak sonsuz öğeler içeren bir liste oluşturmak tamamen mümkündür.
Yukarıdaki veri yapısı, düğümlerin her geçişte yeniden hesaplanmasını gerektirmez, bu nedenle bunlar .NET'teki vanilya IEnumerables'tan belirgin şekilde farklıdır.
Bu pasaj, tembel ve tembel olmayan değerlendirme arasındaki farkı gösterir. Elbette bu fibonacci işlevinin kendisi optimize edilebilir ve özyineleme yerine tembel değerlendirme kullanabilir, ancak bu örneği bozacaktır.
Diyelim ki bir şey için ilk 20 sayıyı kullanmak zorunda OLABİLİRİZ , tembel değil, tüm 20 sayının önceden oluşturulması gerekir, ancak tembel değerlendirme ile yalnızca ihtiyaç duyulduğunda üretilecektir. Böylece, gerektiğinde yalnızca hesaplama bedelini ödersiniz.
Örnek çıktı
Tembel değil nesil: 0.023373 Tembel nesil: 0.000009 Tembel değil çıktı: 0.000921 Tembel çıktı: 0.024205
import time
def now(): return time.time()
def fibonacci(n): #Recursion for fibonacci (not-lazy)
if n < 2:
return n
else:
return fibonacci(n-1)+fibonacci(n-2)
before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()
before3 = now()
for i in notlazy:
print i
after3 = now()
before4 = now()
for i in lazy:
print i
after4 = now()
print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)
Tembel değerlendirme en çok veri yapılarında yararlıdır. Yapıda yalnızca belirli noktaları belirleyen ve diğerlerini tüm dizi açısından ifade eden bir dizi veya vektörü tümevarımlı olarak tanımlayabilirsiniz. Bu, veri yapılarını çok kısa ve öz bir şekilde ve yüksek çalışma zamanı performansıyla oluşturmanıza olanak sağlar.
Bunu iş başında görmek için içgüdü adlı sinir ağı kitaplığıma bakabilirsiniz . Zarafet ve yüksek performans için yoğun bir şekilde tembel değerlendirme yapar. Örneğin, geleneksel olarak zorunlu aktivasyon hesaplamasından tamamen kurtuldum. Basit tembel bir ifade benim için her şeyi yapar.
Bu, örneğin aktivasyon işlevinde ve ayrıca geri yayılım öğrenme algoritmasında kullanılır (yalnızca iki bağlantı gönderebilirim, bu nedenle modüldeki learnPat
işlevi AI.Instinct.Train.Delta
kendiniz aramanız gerekir ). Geleneksel olarak her ikisi de çok daha karmaşık yinelemeli algoritmalar gerektirir.
Başkaları zaten tüm büyük nedenleri verdiler, ancak tembelliğin neden önemli olduğunu anlamaya yardımcı olacak yararlı bir alıştırma, katı bir dilde sabit nokta işlevi denemek ve yazmaktır .
Haskell'de sabit nokta işlevi çok kolaydır:
fix f = f (fix f)
bu genişler
f (f (f ....
ancak Haskell tembel olduğu için, bu sonsuz hesaplama zinciri sorun değil; değerlendirme "dışarıdan içeriye" yapılır ve her şey harika çalışır:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
Daha da önemlisi, fix
tembel olmak değil, tembel olmak önemli f
. Zaten bir katı madde verildiğinde f
, ya ellerinizi havaya kaldırabilir ve pes edebilirsiniz ya da onu genişletir ve işleri karıştırırsınız. (Bu, Noah'ın katı / tembel olan kütüphane olduğu hakkında söylediklerine çok benziyor , dil değil).
Şimdi aynı işlevi katı Scala'da yazdığınızı hayal edin:
def fix[A](f: A => A): A = f(fix(f))
val fact = fix[Int=>Int] { f => n =>
if (n == 0) 1
else n*f(n-1)
}
Elbette bir yığın taşması yaşarsınız. Çalışmasını istiyorsanız, f
argümanı ihtiyaca göre yapmanız gerekir:
def fix[A](f: (=>A) => A): A = f(fix(f))
def fact1(f: =>Int=>Int) = (n: Int) =>
if (n == 0) 1
else n*f(n-1)
val fact = fix(fact1)
Şu anda bir şeyler hakkında nasıl düşündüğünüzü bilmiyorum, ancak tembel değerlendirmeyi bir dil özelliği yerine bir kütüphane sorunu olarak düşünmeyi faydalı buluyorum.
Demek istediğim, katı dillerde, birkaç veri yapısı oluşturarak tembel değerlendirme uygulayabilirim ve tembel dillerde (en azından Haskell), istediğim zaman kesinlik isteyebilirim. Bu nedenle, dil seçimi programlarınızı gerçekten tembel veya tembel yapmaz, sadece varsayılan olarak aldığınız şeyi etkiler.
Bunu böyle düşündüğünüzde, daha sonra veri oluşturmak için kullanabileceğiniz bir veri yapısı yazdığınız tüm yerleri düşünün (daha önce çok fazla bakmadan) ve tembellik için birçok kullanım göreceksiniz. değerlendirme.
Kullandığım tembel değerlendirmenin en yararlı istismarı, belirli bir sırayla bir dizi alt işlevi çağıran bir işlevdi. Bu alt işlevlerden herhangi biri başarısız olursa (yanlış döndürülürse), çağıran işlevin hemen geri dönmesi gerekir. Yani bunu şu şekilde yapabilirdim:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
veya daha zarif çözüm:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
Kullanmaya başladığınızda, onu daha sık kullanma fırsatları göreceksiniz.
Tembel değerlendirme olmadan böyle bir şey yazmanıza izin verilmez:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
Diğer şeylerin yanı sıra, tembel diller çok boyutlu sonsuz veri yapılarına izin verir.
Şema, python vb. Akışlarla tek boyutlu sonsuz veri yapılarına izin verirken, yalnızca bir boyut boyunca gezinebilirsiniz.
Tembellik, aynı sınır problemi için kullanışlıdır , ancak bu bağlantıda bahsedilen koroutin bağlantısına dikkat çekmeye değer.
Tembel değerlendirme, yoksul adamın eşitlikçi muhakemesidir (ideal olarak, kodun özelliklerini ilgili türlerin ve işlemlerin özelliklerinden çıkarmak beklenebilir).
Oldukça iyi çalışıyor Örnek: sum . take 10 $ [1..10000000000]
. Tek bir doğrudan ve basit sayısal hesaplama yerine toplamda 10 sayıya indirgenmeyi önemsemediğimiz bir şey. Elbette tembel değerlendirme olmadan bu, hafızada sadece ilk 10 unsurunu kullanmak için devasa bir liste yaratır. Kesinlikle çok yavaş olacaktır ve yetersiz bellek hatasına neden olabilir.
Biz istediğimiz kadar bu harika olarak değil Örnek: sum . take 1000000 . drop 500 $ cycle [1..20]
. Bir liste yerine döngüde olsa bile, aslında 1.000.000 sayıyı toplayacaktır; hala gereken birkaç Koşullamalar ve birkaç formüllerle, sadece bir direkt sayısal hesaplamaya azaltılabilir. Hangi olur daha iyi o zaman 1 000 000 numara özetliyor bir sürü. Bir döngüde olsa ve listede olmasa bile (yani ormansızlaştırma optimizasyonundan sonra).
Başka bir şey de, kuyruk özyineleme modulo cons stilinde kodlamayı mümkün kılıyor ve sadece çalışıyor .
bakınız ilgili cevap .
"Tembel değerlendirme" derken, bileşik boole'lerde olduğu gibi,
if (ConditionA && ConditionB) ...
o zaman cevap basitçe, programın tükettiği CPU döngüsü ne kadar azsa, o kadar hızlı çalışacaktır ... ve bir yığın işleme talimatının programın sonucunu etkilememesi durumunda, bu gereksizdir (ve dolayısıyla yine de onları gerçekleştirmek için ...
otoh ise, benim "tembel başlatıcılar" olarak bildiğim şeyi kastediyorsunuz, örneğin:
class Employee
{
private int supervisorId;
private Employee supervisor;
public Employee(int employeeId)
{
// code to call database and fetch employee record, and
// populate all private data fields, EXCEPT supervisor
}
public Employee Supervisor
{
get
{
return supervisor?? (supervisor = new Employee(supervisorId));
}
}
}
Pekala, bu teknik, Çalışan nesnesini kullanan istemcinin süpervizörün verilerine erişim gerektirmesi dışında Süpervizör veri kaydı için veritabanını arama ihtiyacını ortadan kaldırmak için sınıfı kullanan müşteri koduna izin verir ... bu, bir Çalışanı örnekleme sürecini daha hızlı hale getirir, ve yine de Süpervizöre ihtiyaç duyduğunuzda, Süpervizör özelliğine yapılan ilk çağrı Veritabanı çağrısını tetikleyecek ve veriler alınacak ve kullanılabilir olacaktır ...
Yüksek sıra işlevlerinden alıntı
3829'a bölünebilen 100.000'in altındaki en büyük sayıyı bulalım. Bunu yapmak için, çözümün yalan söylediğini bildiğimiz bir dizi olasılığı filtreleyeceğiz.
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
Önce 100.000'den küçük tüm sayıların azalan bir listesini yaparız. Sonra onu yüklemimize göre filtreliyoruz ve sayılar azalan bir şekilde sıralandığından, yüklemimizi karşılayan en büyük sayı filtrelenmiş listenin ilk öğesidir. Başlangıç setimiz için sonlu bir liste kullanmamıza bile gerek yoktu. Bu yine tembelliktir. Sonunda filtrelenmiş listenin başını kullandığımız için, filtrelenmiş listenin sonlu veya sonsuz olması önemli değildir. İlk uygun çözüm bulunduğunda değerlendirme durur.