Tembel değerlendirme neden faydalıdır?


119

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:


96

Ç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.


6
Python, yineleyiciler aracılığıyla sonsuz listeleri tembel olarak değerlendirdi
Mark Cidade

4
Üreteçleri ve oluşturucu ifadeleri kullanarak Python'da sonsuz bir listeyi taklit edebilirsiniz (liste anlamasına benzer şekilde çalışır): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery

24
Oluşturucular Python'da tembel listeleri kolaylaştırır, ancak diğer tembel değerlendirme teknikleri ve veri yapıları gözle görülür şekilde daha az zariftir.
Peter Burns

3
Korkarım bu cevaba katılmıyorum. Tembelliğin verimlilikle ilgili olduğunu düşünürdüm, ancak Haskell'i önemli miktarda kullandım ve ardından Scala'ya geçip deneyimi karşılaştırarak, tembelliğin çoğu kez önemli olduğunu ancak nadiren verimlilik nedeniyle önemli olduğunu söylemeliyim. Bence Edward Kmett gerçek nedenlere dayanıyor.
Owen

3
Benzer şekilde katılmıyorum, C'de katı bir değerlendirme nedeniyle açık bir sonsuz liste kavramı bulunmamakla birlikte, aynı numarayı başka bir dilde (ve aslında, her tembel dilin gerçek imlementasyonunda) thunks kullanarak ve fonksiyonun etrafından dolaşarak kolayca oynayabilirsiniz. benzer ifadelerle üretilen sonsuz yapının sonlu bir önekiyle çalışmak için işaretçiler.
Kristopher Micinski

71

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.


19
Daha genel olarak, take k $ quicksort listsadece 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.
ephemient

@ephemient O (nk log k) demek istemiyor musun?
MaiaVictor

1
@Viclib Hayır, dediğimi kastetmiştim.
ephemient

@ephemient sonra ne yazık ki
anlamadığımı

2
@Viclib n'den en üstteki k öğeleri bulmak için bir seçim algoritması O (n + k log k) 'dir. Hızlı sıralamayı tembel bir dilde uyguladığınızda ve yalnızca ilk k öğelerini belirlemek için yeterince değerlendirdiğinizde (daha sonra değerlendirmeyi durdurarak), tembel olmayan bir seçim algoritmasının yapacağı gibi tam olarak aynı karşılaştırmaları yapar.
ephemient

70

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 letkesinlikle 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.)


5
Çok hoş; bunlar gerçek cevaplar. Haskell'i önemli miktarda kullanana ve bunun gerçekten de sebebin bu olmadığını anlayıncaya kadar bunun verimlilikle ilgili olduğunu düşünürdüm (daha sonra hesaplamaları ertelemek).
Owen

11
Ayrıca, tembel bir dilin saf olması gerektiği teknik olarak doğru olmasa da (örnek olarak R), saf olmayan tembel bir dilin çok tuhaf şeyler yapabileceği doğrudur (örnek olarak R).
Owen

4
Elbette var. Sıkı bir dilde özyinelemeli lettehlikeli bir canavardır, R6RS şemasında #fdüğü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 lettembel bir dilde kesinlikle daha özyinelemeli bağlamalar mantıklıdır. whereKesinlik, 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, #fkonu. whereYerel olmayan kaygılarla kodunuzu katı bir şekilde çözer.
Edward KMETT

2
Tembelliğin değer kısıtlamasından kaçınmaya nasıl yardımcı olduğunu açıklayabilir misiniz? Bunu anlayamadım.
Tom Ellis

3
@PaulBone Neden bahsediyorsun? Tembelliğin kontrol yapılarıyla çok ilgisi vardır. Kendi denetim yapınızı katı bir dilde tanımlarsanız, ya bir sürü lambda ya da benzeri kullanmak zorunda kalacak ya da berbat olacaktır. Çünkü ifFunc(True, x, y)her ikisi biçecek xve ybunun yerine sadece bir x.
noktalı virgül

28

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).


25

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.


GC ile benzerlik bana biraz yardımcı oldu, ancak "bazı standart kodların kaldırılmasına" bir örnek verebilir misiniz lütfen?
Abdul

1
@Abdul, herhangi bir ORM kullanıcısına aşina olan bir örnek: geç ilişkilendirme yükleniyor. İlişkilendirmeyi "tam zamanında" DB'den yükler ve aynı zamanda bir geliştiriciyi ne zaman yükleneceğini ve nasıl önbelleğe alınacağını açıkça belirtme ihtiyacından kurtarır (bu, demek istediğim ortak metindir). İşte başka bir örnek: projectlombok.org/features/GetterLazy.html .
Alexey

25

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 .


Onları dili saf tutmaya zorlamakla kalmadı , aynı zamanda ( monadın tanıtımından önce ) imzası olduğunda ve zaten düzgün şekilde etkileşimli programlar yazabildiğiniz zaman bunu yapmalarına izin verdi . IOmainString -> String
leftaroundabout

@leftaroundabout: Katı bir dilin tüm etkileri bir monad'a zorlamasını engelleyen nedir IO?
Tom Ellis

13

Bir tic-tac-toe programı düşünün. Bunun dört işlevi vardır:

  • Mevcut bir panoyu alan ve her birine bir hareket uygulanmış yeni kartların bir listesini oluşturan bir hareket oluşturma işlevi.
  • Daha sonra, bundan sonra gelebilecek tüm olası pano konumlarını türetmek için hareket oluşturma işlevini uygulayan bir "ağaç taşıma" işlevi vardır.
  • Bir sonraki en iyi hareketi bulmak için ağacı (veya muhtemelen sadece bir kısmını) gezdiren bir minimax işlevi vardır.
  • Oyunculardan birinin kazanıp kazanmadığını belirleyen bir tahta değerlendirme 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.


1
["Hevesli" (yani geleneksel) bir dilde bu işe yaramaz çünkü taşıma ağacı hafızaya sığmaz] - Tic-Tac-Toe için kesinlikle olacaktır. Kaydedilecek en fazla 3 ** 9 = 19683 pozisyon vardır. Her birini abartılı 50 baytta saklarsak, bu neredeyse bir megabayttır. Bu bir şey değil ...
Jonas

6
Evet, benim açımdan bu. Hevesli diller, önemsiz oyunlar için temiz bir yapıya sahip olabilir, ancak gerçek herhangi bir şey için bu yapıdan ödün vermek zorundadır. Tembel dillerde böyle bir sorun yok.
Paul Johnson

3
Adil olmak gerekirse, tembel değerlendirme kendi hafıza sorunlarına yol açabilir. İnsanlar haskell bir istekli değerlendirmede, O hafızada kapladığı olurdu, o şey (1) için 's hafızasını üfleme neden sormak için alışılmadık bir durum değil
RHSeeger

@PaulJohnson Tüm pozisyonları değerlendirirseniz, onları isteyerek veya tembel olarak değerlendirmeniz fark etmez. Aynı işin yapılması gerekiyor. Ortada durur ve pozisyonların sadece yarısını değerlendirirseniz, bu da bir fark yaratmaz, çünkü her iki durumda da işin yarısının yapılması gerekir. İki değerlendirme arasındaki tek fark, algoritmanın tembel yazılırsa daha iyi görünmesidir.
2019

12

İşte henüz tartışmada gündeme getirildiğine inanmadığım iki nokta daha.

  1. 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.

  2. 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.


10

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.


1
Bazı insanlar bunun gerçekten "tembel infaz" olduğunu söyleyebilir. Aradaki fark, Haskell gibi makul derecede saf diller dışında gerçekten önemsizdir; ancak fark, sadece hesaplamanın geciktirilmesi değil, aynı zamanda bununla ilişkili yan etkilerin de (dosyaları açma ve okuma gibi) olmasıdır.
Owen

8

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 ...


Bunun tembel bir değerlendirme değil, kısa devre olduğunu düşündüm.
Thomas Owens

2
ConditionTwo sadece gerçekten gerekliyse hesaplanır (yani conditionOne doğruysa).
Romain Linsolas

7
Sanırım kısa devre, dejenere bir tembel değerlendirme vakası, ama kesinlikle bunu düşünmenin yaygın bir yolu değil.
rmeador

19
Kısa devre, aslında özel bir tembel değerlendirme durumudur. Açıkçası, tembel değerlendirme kısa devre yapmaktan çok daha fazlasını kapsar. Ya da, kısa devre yapmanın tembel değerlendirmenin ötesinde nesi var?
yfeldblum

2
@Juliet: "Tembel" için güçlü bir tanımınız var. İki parametre alan bir işlev örneğiniz, kısa devre if ifadesi ile aynı değildir. Kısa devre if ifadesi gereksiz hesaplamaları önler. Sanırım örneğinizle daha iyi bir karşılaştırma, her iki koşulu da değerlendirmeye zorlayan Visual Basic operatörü "andalso" olacaktır

8
  1. 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.)

  2. 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 fordöngüleri vardır; Haskell'in bir foriş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 ...)

  3. 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.

  4. 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.


6

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

lazyyapı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.


5

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)

5

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 learnPatişlevi AI.Instinct.Train.Deltakendiniz aramanız gerekir ). Geleneksel olarak her ikisi de çok daha karmaşık yinelemeli algoritmalar gerektirir.


4

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, fixtembel 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, fargü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)

3

Ş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.


1
Sıkı dillerde tembel değerlendirme uygulamak genellikle bir Turing Tarpitidir.
itsbruce

2

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.


2

Tembel değerlendirme olmadan böyle bir şey yazmanıza izin verilmez:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

Pekala, imo, bunu yapmak kötü bir fikir. Bu kod doğru olsa da (neyi başarmaya çalıştığınıza bağlı olarak), okumak zordur ve bu her zaman kötü bir şeydir.
Brann

12
Ben öyle düşünmüyorum. C ve akrabalarında standart bir yapıdır.
Paul Johnson

Bu, tembel değerlendirme değil, kısa devre değerlendirme örneğidir. Yoksa bu gerçekten aynı şey mi?
RufusVS

2

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.


2

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 .


1

"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 ...


0

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.

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.