Mengapa evaluasi malas berguna?

119

Saya sudah lama bertanya-tanya mengapa evaluasi malas berguna. Saya belum memiliki seorang pun yang menjelaskan kepada saya dengan cara yang masuk akal; sebagian besar akhirnya bermuara pada "percayalah padaku".

Catatan: Saya tidak bermaksud memoisasi.

Joel McCracken
sumber

Jawaban:

96

Sebagian besar karena ini bisa lebih efisien - nilai tidak perlu dihitung jika tidak akan digunakan. Misalnya, saya dapat meneruskan tiga nilai ke dalam suatu fungsi, tetapi bergantung pada urutan ekspresi kondisional, hanya subset yang benar-benar dapat digunakan. Dalam bahasa seperti C, ketiga nilai tersebut akan tetap dihitung; tetapi di Haskell, hanya nilai yang diperlukan yang dihitung.

Ini juga memungkinkan untuk hal-hal keren seperti daftar tak terbatas. Saya tidak dapat memiliki daftar yang tidak terbatas dalam bahasa seperti C, tetapi di Haskell, itu tidak masalah. Daftar tak terbatas cukup sering digunakan dalam bidang matematika tertentu, sehingga kemampuan untuk memanipulasinya dapat bermanfaat.

mipadi
sumber
6
Python memiliki daftar tak terbatas dengan malas mengevaluasi melalui iterator
Mark Cidade
4
Anda sebenarnya dapat meniru daftar tak terbatas dengan Python menggunakan generator dan ekspresi generator (yang bekerja dengan cara yang mirip dengan pemahaman daftar): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery
24
Generator membuat daftar malas menjadi mudah dengan Python, tetapi teknik evaluasi malas dan struktur data lainnya terasa kurang elegan.
Peter Burns
3
Saya khawatir saya tidak setuju dengan jawaban ini. Dulu saya berpikir bahwa kemalasan adalah tentang efisiensi, tetapi setelah menggunakan Haskell dalam jumlah besar, lalu beralih ke Scala dan membandingkan pengalaman, saya harus mengatakan bahwa kemalasan sering kali penting, tetapi jarang karena efisiensi. Saya pikir Edward Kmett menemukan alasan sebenarnya.
Owen
3
Saya juga tidak setuju, meskipun tidak ada gagasan eksplisit tentang daftar tak terbatas di C karena evaluasi yang ketat, Anda dapat dengan mudah memainkan trik yang sama dalam bahasa lain (dan memang, di sebagian besar implementasi aktual setiap bahasa malas) dengan menggunakan thunks dan fungsi passing-around pointer untuk bekerja dengan prefiks berhingga dari struktur tak hingga yang dihasilkan oleh ekspresi serupa.
Kristopher Micinski
71

Contoh evaluasi malas yang berguna adalah penggunaan quickSort:

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

Jika sekarang kita ingin menemukan daftar minimum, kita dapat menentukan

minimum ls = head (quickSort ls)

Yang pertama mengurutkan daftar dan kemudian mengambil elemen pertama dari daftar. Namun, karena evaluasi malas, hanya head yang dihitung. Misalnya, jika kita mengambil jumlah minimum dari daftar, [2, 1, 3,]quickSort akan terlebih dahulu memfilter semua elemen yang lebih kecil dari dua. Kemudian quickSort melakukan itu (mengembalikan daftar tunggal [1]) yang sudah cukup. Karena evaluasi malas, sisanya tidak pernah diurutkan, menghemat banyak waktu komputasi.

Ini tentu saja contoh yang sangat sederhana, tetapi kemalasan bekerja dengan cara yang sama untuk program yang sangat besar.

Namun, ada sisi negatifnya: menjadi lebih sulit untuk memprediksi kecepatan runtime dan penggunaan memori program Anda. Ini tidak berarti bahwa program malas lebih lambat atau membutuhkan lebih banyak memori, tetapi ada baiknya untuk mengetahuinya.

Chris Eidhof
sumber
19
Secara lebih umum, take k $ quicksort listhanya membutuhkan waktu O (n + k log k), di mana n = length list. Dengan jenis perbandingan non-malas, ini akan selalu membutuhkan waktu O (n log n).
singkat
@efemient bukan maksudmu O (nk log k)?
MaiaVictor
1
@Viclib Tidak, saya serius apa yang saya katakan.
efemient
@ephemient maka saya pikir saya tidak mengerti, sayangnya
MaiaVictor
2
@Viclib Algoritme pemilihan untuk mencari k elemen teratas dari n adalah O (n + k log k). Saat Anda mengimplementasikan quicksort dalam bahasa lazy, dan hanya mengevaluasinya cukup jauh untuk menentukan elemen k pertama (menghentikan evaluasi setelahnya), itu membuat perbandingan yang sama persis seperti yang dilakukan algoritme pemilihan non-lazy.
efemient
70

Saya menemukan evaluasi malas berguna untuk sejumlah hal.

Pertama, semua bahasa lazy yang ada adalah murni, karena sangat sulit untuk menjelaskan efek samping dalam bahasa lazy.

Bahasa murni memungkinkan Anda bernalar tentang definisi fungsi menggunakan penalaran persamaan.

foo x = x + 3

Sayangnya, dalam setelan non-malas, lebih banyak pernyataan yang gagal ditampilkan daripada setelan malas, jadi ini kurang berguna dalam bahasa seperti ML. Tapi dalam bahasa malas Anda bisa dengan aman bernalar tentang kesetaraan.

Kedua, banyak hal seperti 'batasan nilai' di ML tidak diperlukan dalam bahasa lazy seperti Haskell. Ini mengarah ke deklarasi sintaks yang bagus. Bahasa seperti ML perlu menggunakan kata kunci seperti var atau fun. Di Haskell hal-hal ini runtuh menjadi satu gagasan.

Ketiga, kemalasan memungkinkan Anda menulis kode yang sangat fungsional yang dapat dipahami dalam beberapa bagian. Di Haskell, hal umum untuk menulis badan fungsi seperti:

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

Ini memungkinkan Anda bekerja 'dari atas ke bawah' meskipun pemahaman tentang tubuh suatu fungsi. Bahasa seperti ML memaksa Anda untuk menggunakan letyang dievaluasi secara ketat. Akibatnya, Anda tidak berani 'mengangkat' klausa let keluar ke bagian utama fungsi, karena jika mahal (atau memiliki efek samping) Anda tidak ingin selalu dievaluasi. Haskell dapat 'mendorong' detail ke klausa where secara eksplisit karena ia tahu bahwa konten klausa tersebut hanya akan dievaluasi sesuai kebutuhan.

Dalam praktiknya, kita cenderung menggunakan pelindung dan menutupnya lebih jauh untuk:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

Keempat, kemalasan terkadang menawarkan ekspresi algoritme tertentu yang jauh lebih elegan. 'Pengurutan cepat' yang malas di Haskell bersifat one-liner dan memiliki keuntungan bahwa jika Anda hanya melihat beberapa item pertama, Anda hanya membayar biaya yang sebanding dengan biaya pemilihan item tersebut saja. Tidak ada yang mencegah Anda melakukan ini secara ketat, tetapi Anda mungkin harus mengodekan ulang algoritme setiap kali untuk mencapai kinerja asimtotik yang sama.

Kelima, kemalasan memungkinkan Anda menentukan struktur kontrol baru dalam bahasa tersebut. Anda tidak dapat menulis 'if .. then .. else ..' seperti konstruksi baru dalam bahasa yang ketat. Jika Anda mencoba untuk mendefinisikan fungsi seperti:

if' True x y = x
if' False x y = y

dalam bahasa yang ketat maka kedua cabang akan dievaluasi terlepas dari nilai kondisinya. Ini menjadi lebih buruk ketika Anda mempertimbangkan loop. Semua solusi yang ketat membutuhkan bahasa untuk memberi Anda semacam kutipan atau konstruksi lambda eksplisit.

Akhirnya, dengan nada yang sama, beberapa mekanisme terbaik untuk menangani efek samping dalam sistem tipe, seperti monad, benar-benar hanya dapat diekspresikan secara efektif dalam pengaturan malas. Hal ini dapat dilihat dengan membandingkan kompleksitas Alur Kerja F # dengan Haskell Monads. (Anda dapat mendefinisikan monad dalam bahasa yang ketat, tetapi sayangnya Anda akan sering gagal satu atau dua hukum monad karena kurangnya kemalasan dan Alur Kerja dengan perbandingan mengambil banyak bagasi ketat.)

Edward KMETT
sumber
5
Sangat bagus; ini adalah jawaban yang sebenarnya. Saya dulu berpikir bahwa ini tentang efisiensi (menunda perhitungan untuk nanti) sampai saya menggunakan Haskell dalam jumlah yang signifikan dan melihat bahwa itu sama sekali bukan alasannya.
Owen
11
Juga, meskipun secara teknis tidak benar bahwa bahasa lazy harus murni (R sebagai contoh), memang benar bahwa bahasa malas yang tidak murni dapat melakukan hal-hal yang sangat aneh (R sebagai contoh).
Owen
4
Tentu ada. Dalam bahasa yang ketat, rekursif letadalah binatang yang berbahaya, dalam skema R6RS memungkinkan #fkemunculan acak dalam istilah Anda di mana pun mengikat simpul secara ketat mengarah ke siklus! Tidak ada permainan kata-kata yang dimaksudkan, tetapi letbinding yang lebih rekursif sangat masuk akal dalam bahasa malas. Keketatan juga memperburuk fakta bahwa wheretidak ada cara untuk memesan efek relatif sama sekali, kecuali oleh SCC, itu adalah konstruksi tingkat pernyataan, efeknya dapat terjadi dalam urutan apa pun secara ketat, dan bahkan jika Anda memiliki bahasa murni, Anda akan berakhir dengan #fisu. Ketat wherememecahkan kode Anda dengan masalah non-lokal.
Edward KMETT
2
Bisakah Anda menjelaskan bagaimana kemalasan membantu menghindari batasan nilai? Saya belum bisa memahami ini.
Tom Ellis
3
@PaulBone Apa yang kamu bicarakan? Kemalasan berkaitan erat dengan struktur kontrol. Jika Anda mendefinisikan struktur kontrol Anda sendiri dalam bahasa yang ketat, itu harus menggunakan banyak lambda atau serupa, atau itu akan payah. Karena ifFunc(True, x, y)akan mengevaluasi keduanya xdan ybukannya hanya x.
titik koma
28

Ada perbedaan antara evaluasi pesanan normal dan evaluasi malas (seperti di Haskell).

square x = x * x

Mengevaluasi ekspresi berikut ...

square (square (square 2))

... dengan evaluasi penuh semangat:

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... dengan evaluasi pesanan normal:

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

... dengan evaluasi malas:

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

Itu karena evaluasi malas melihat pohon sintaks dan melakukan transformasi pohon ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... sedangkan evaluasi urutan normal hanya melakukan perluasan tekstual.

Itulah mengapa kami, ketika menggunakan evaluasi malas, menjadi lebih kuat (evaluasi berakhir lebih sering daripada strategi lain) sementara kinerjanya setara dengan evaluasi bersemangat (setidaknya dalam notasi-O).

Thomas Danecker
sumber
25

Evaluasi malas terkait CPU dengan cara yang sama seperti pengumpulan sampah yang terkait dengan RAM. GC memungkinkan Anda untuk berpura-pura memiliki jumlah memori yang tidak terbatas dan dengan demikian meminta objek dalam memori sebanyak yang Anda butuhkan. Waktu proses akan secara otomatis mengambil kembali objek yang tidak dapat digunakan. LE memungkinkan Anda berpura-pura memiliki sumber daya komputasi tak terbatas - Anda dapat melakukan komputasi sebanyak yang Anda butuhkan. Waktu proses tidak akan menjalankan komputasi yang tidak perlu (untuk kasus tertentu).

Apa keuntungan praktis dari model "berpura-pura" ini? Ini melepaskan pengembang (sampai batas tertentu) dari mengelola sumber daya dan menghapus beberapa kode boilerplate dari sumber Anda. Tetapi yang lebih penting adalah Anda dapat menggunakan kembali solusi Anda secara efisien dalam konteks yang lebih luas.

Bayangkan Anda memiliki daftar angka S dan angka N. Anda perlu mencari yang paling dekat dengan angka N angka M dari daftar S. Anda dapat memiliki dua konteks: satu N dan beberapa daftar L dari Ns (ei untuk setiap N di L Anda mencari M terdekat di S). Jika Anda menggunakan evaluasi malas, Anda dapat mengurutkan S dan menerapkan pencarian biner untuk menemukan M terdekat ke N. Untuk pengurutan malas yang baik, diperlukan langkah O (ukuran (S)) untuk satu N dan O (ln (ukuran (S)) * (size (S) + size (L))) langkah-langkah untuk L. yang terdistribusi merata. Jika Anda tidak memiliki evaluasi malas untuk mencapai efisiensi optimal, Anda harus mengimplementasikan algoritme untuk setiap konteks.

Alexey
sumber
Analogi dengan GC sedikit membantu saya, tetapi dapatkah Anda memberikan contoh "hapus beberapa kode boilerplate"?
Abdul
1
@Abdul, contoh yang familiar bagi setiap pengguna ORM: pemuatan asosiasi malas. Ini memuat asosiasi dari DB "tepat pada waktunya" dan pada saat yang sama melepaskan pengembang dari kebutuhan untuk secara eksplisit menentukan kapan harus memuatnya dan bagaimana cara menyimpannya (ini adalah boilerplate yang saya maksud). Berikut adalah contoh lainnya: projectlombok.org/features/GetterLazy.html .
Alexey
25

Jika Anda yakin Simon Peyton Jones, malas evaluasi tidak penting per se tapi hanya sebagai 'baju rambut' yang memaksa desainer untuk menjaga bahasa murni. Saya menemukan diri saya bersimpati pada sudut pandang ini.

Richard Bird, John Hughes, dan Ralf Hinze mampu melakukan hal-hal menakjubkan dengan evaluasi malas. Membaca karya mereka akan membantu Anda menghargainya. Titik awal yang baik adalah pemecah Sudoku yang luar biasa dari Bird dan makalah Hughes tentang Why Functional Programming Matters .

Norman Ramsey
sumber
Itu tidak hanya memaksa mereka untuk menjaga bahasa tetap murni, itu juga memungkinkan mereka untuk melakukannya, ketika (sebelum pengenalan IOmonad) tanda tangan mainakan String -> Stringdan Anda sudah bisa menulis program interaktif dengan benar.
kiri sekitar sekitar
@leftaroundabout: Apa yang menghentikan bahasa ketat untuk memaksa semua efek menjadi IOmonad?
Tom Ellis
13

Pertimbangkan program tic-tac-toe. Ini memiliki empat fungsi:

  • Fungsi generasi bergerak yang mengambil papan saat ini dan menghasilkan daftar papan baru masing-masing dengan satu gerakan diterapkan.
  • Lalu ada fungsi "pohon bergerak" yang menerapkan fungsi generasi bergerak untuk mendapatkan semua kemungkinan posisi papan yang bisa mengikuti dari yang satu ini.
  • Ada fungsi minimax yang berjalan di pohon (atau mungkin hanya sebagian) untuk menemukan gerakan terbaik berikutnya.
  • Ada fungsi evaluasi dewan yang menentukan apakah salah satu pemain menang.

Ini menciptakan pemisahan perhatian yang jelas dan bagus. Khususnya fungsi pembuatan gerakan dan fungsi evaluasi papan adalah satu-satunya yang perlu memahami aturan permainan: fungsi pohon bergerak dan fungsi minimum dapat digunakan kembali sepenuhnya.

Sekarang mari kita coba menerapkan catur alih-alih tic-tac-toe. Dalam bahasa "bersemangat" (yaitu konvensional) ini tidak akan berfungsi karena pohon pemindahan tidak akan muat dalam memori. Jadi sekarang fungsi evaluasi papan dan generasi bergerak perlu dicampur dengan pohon bergerak dan logika minimax karena logika minimax harus digunakan untuk memutuskan gerakan mana yang akan dihasilkan. Struktur modular bersih kami yang bagus menghilang.

Namun dalam bahasa lazy, elemen pohon pemindahan hanya dihasilkan sebagai respons terhadap permintaan dari fungsi minimax: seluruh pohon pemindahan tidak perlu dibuat sebelum kami melepaskan minimax di elemen atas. Jadi, struktur modular bersih kami masih berfungsi di game nyata.

Paul Johnson
sumber
1
[Dalam bahasa "bersemangat" (yaitu konvensional) ini tidak akan berfungsi karena pohon bergerak tidak akan muat dalam memori] - untuk Tic-Tac-Toe pasti akan. Ada paling banyak 3 ** 9 = 19683 posisi untuk disimpan. Jika kita menyimpan masing-masing dalam 50 byte yang luar biasa, itu hampir satu megabyte.
Bukan
6
Ya, itulah maksud saya. Bahasa yang bersemangat dapat memiliki struktur yang bersih untuk game sepele, tetapi harus mengkompromikan struktur itu untuk sesuatu yang nyata. Bahasa malas tidak memiliki masalah itu.
Paul Johnson
3
Agar adil, evaluasi malas dapat menyebabkan masalah ingatan itu sendiri. Tidak jarang orang bertanya mengapa haskell meledakkan ingatannya untuk sesuatu yang, dalam evaluasi yang bersemangat, akan memiliki konsumsi memori O (1)
RHSeeger
@PaulJohnson Jika Anda mengevaluasi semua posisi, tidak ada bedanya apakah Anda mengevaluasinya dengan penuh semangat atau malas. Pekerjaan yang sama harus dilakukan. Jika Anda berhenti di tengah dan mengevaluasi hanya setengah dari posisi, itu juga tidak ada bedanya, karena dalam kedua kasus setengah dari pekerjaan harus dilakukan. Satu-satunya perbedaan antara kedua evaluasi tersebut adalah, algoritme terlihat lebih bagus, jika ditulis dengan malas.
ceving
12

Berikut adalah dua poin lagi yang saya tidak percaya telah diangkat dalam diskusi.

  1. Kemalasan adalah mekanisme sinkronisasi dalam lingkungan yang bersamaan. Ini adalah cara yang ringan dan mudah untuk membuat referensi ke beberapa komputasi, dan membagikan hasilnya di antara banyak utas. Jika beberapa utas mencoba mengakses nilai yang tidak dievaluasi, hanya satu dari mereka yang akan mengeksekusinya, dan yang lain akan memblokir sesuai, menerima nilai setelah tersedia.

  2. Kemalasan sangat penting untuk amortisasi struktur data dalam pengaturan murni. Hal ini dijelaskan oleh Okasaki dalam Struktur Data Fungsional Murni secara mendetail, tetapi gagasan dasarnya adalah bahwa evaluasi malas adalah bentuk mutasi terkontrol yang penting untuk memungkinkan kita mengimplementasikan tipe struktur data tertentu secara efisien. Sementara kita sering berbicara tentang kemalasan yang memaksa kita untuk memakai kaos rambut yang murni, cara lain juga berlaku: mereka adalah sepasang fitur bahasa yang sinergis.

Edward Z. Yang
sumber
10

Ketika Anda menyalakan komputer dan Windows menahan diri dari membuka setiap direktori pada hard drive Anda di Windows Explorer dan menahan diri untuk tidak meluncurkan setiap program yang diinstal pada komputer Anda, sampai Anda menunjukkan bahwa direktori tertentu diperlukan atau program tertentu diperlukan, yang adalah evaluasi "malas".

Evaluasi "malas" adalah melakukan operasi kapan dan sesuai kebutuhan. Ini berguna jika merupakan fitur dari pustaka atau bahasa pemrograman karena umumnya lebih sulit untuk mengimplementasikan evaluasi malas sendiri daripada hanya menghitung sebelumnya semuanya di depan.

yfeldblum
sumber
1
Beberapa orang mungkin mengatakan bahwa itu benar-benar "eksekusi malas". Perbedaannya benar-benar tidak penting kecuali dalam bahasa yang cukup murni seperti Haskell; Namun perbedaannya adalah bukan hanya penghitungan yang tertunda, tetapi juga efek samping yang terkait dengannya (seperti membuka dan membaca file).
Owen
8

Pertimbangkan ini:

if (conditionOne && conditionTwo) {
  doSomething();
}

Metode doSomething () akan dijalankan hanya jika conditionOne benar dan conditionTwo benar. Dalam kasus di mana conditionOne salah, mengapa Anda perlu menghitung hasil dari conditionTwo? Evaluasi conditionTwo akan membuang-buang waktu dalam hal ini, terutama jika kondisi Anda adalah hasil dari beberapa proses metode.

Itulah salah satu contoh minat evaluasi malas ...

Romain Linsolas
sumber
Saya pikir itu korsleting, bukan evaluasi malas.
Thomas Owens
2
Evaluasi malas karena conditionTwo hanya dihitung jika benar-benar diperlukan (yaitu jika conditionOne benar).
Romain Linsolas
7
Saya kira korsleting adalah kasus evaluasi malas yang merosot, tapi jelas bukan cara yang umum untuk memikirkannya.
rmeador
19
Korsleting sebenarnya adalah kasus khusus evaluasi malas. Evaluasi malas jelas mencakup jauh lebih dari sekadar hubungan arus pendek. Atau, apa yang dimiliki oleh korsleting di atas evaluasi malas?
yfeldblum
2
@ Juliet: Anda memiliki definisi yang kuat tentang 'malas'. Contoh Anda dari suatu fungsi yang mengambil dua parameter tidak sama dengan korsleting pernyataan if. Hubung singkat jika pernyataan menghindari perhitungan yang tidak perlu. Saya pikir perbandingan yang lebih baik dengan contoh Anda adalah operator Visual Basic "andalso" yang memaksa kedua kondisi untuk dievaluasi
8
  1. Ini dapat meningkatkan efisiensi. Ini yang terlihat jelas, tapi sebenarnya bukan yang paling penting. (Perhatikan juga bahwa kemalasan juga dapat mematikan efisiensi - fakta ini tidak langsung terlihat. Namun, dengan menyimpan banyak hasil sementara daripada langsung menghitungnya, Anda dapat menggunakan sejumlah besar RAM.)

  2. Ini memungkinkan Anda menentukan konstruksi kontrol aliran dalam kode tingkat pengguna normal, daripada di-hardcode ke dalam bahasa. (Misalnya, Java memiliki forloop; Haskell memiliki forfungsi. Java memiliki penanganan pengecualian; Haskell memiliki berbagai jenis pengecualian monad. C # memiliki goto; Haskell memiliki monad lanjutan ...)

  3. Ini memungkinkan Anda memisahkan algoritme untuk menghasilkan data dari algoritme untuk memutuskan berapa banyak data yang akan dihasilkan. Anda dapat menulis satu fungsi yang menghasilkan daftar hasil nosional-tak terbatas, dan fungsi lain yang memproses sebanyak mungkin daftar ini sesuai kebutuhannya. Lebih tepatnya, Anda dapat memiliki lima fungsi generator dan lima fungsi konsumen, dan Anda dapat secara efisien menghasilkan kombinasi apa pun - alih-alih mengkodekan fungsi 5 x 5 = 25 secara manual yang menggabungkan kedua tindakan sekaligus. (!) Kita semua tahu bahwa decoupling adalah hal yang baik.

  4. Ini kurang lebih memaksa Anda untuk merancang bahasa fungsional murni . Selalu menggoda untuk mengambil jalan pintas, tetapi dalam bahasa malas, ketidakmurnian sekecil apa pun membuat kode Anda sangat tidak dapat diprediksi, yang sangat menghalangi pengambilan jalan pintas.

MathematicalOrchid
sumber
6

Salah satu manfaat besar kemalasan adalah kemampuan untuk menulis struktur data yang tidak dapat diubah dengan batas amortisasi yang wajar. Contoh sederhananya adalah tumpukan yang tidak dapat diubah (menggunakan F #):

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)

Kodenya masuk akal, tetapi menambahkan dua tumpukan x dan y membutuhkan waktu O (panjang x) dalam kasus terbaik, terburuk, dan rata-rata. Menambahkan dua tumpukan adalah operasi monolitik, ini menyentuh semua node di tumpukan x.

Kita dapat menulis ulang struktur data sebagai tumpukan malas:

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

lazybekerja dengan menangguhkan evaluasi kode dalam konstruktornya. Setelah dievaluasi menggunakan .Force(), nilai yang dikembalikan disimpan dalam cache dan digunakan kembali pada setiap berikutnya .Force().

Dengan versi malas, menambahkan adalah operasi O (1): mengembalikan 1 node dan menangguhkan pembuatan ulang daftar yang sebenarnya. Saat Anda mendapatkan kepala dari daftar ini, itu akan mengevaluasi isi simpul, memaksanya mengembalikan kepala dan membuat satu suspensi dengan elemen yang tersisa, jadi mengambil kepala daftar adalah operasi O (1).

Jadi, daftar malas kita selalu dalam keadaan membangun kembali, Anda tidak perlu membayar biaya untuk membangun kembali daftar ini sampai Anda menjelajahi semua elemennya. Menggunakan kemalasan, daftar ini mendukung O (1) consing dan appending. Menariknya, karena kami tidak mengevaluasi node hingga node diakses, sangat mungkin untuk membuat daftar dengan elemen yang berpotensi tidak terbatas.

Struktur data di atas tidak memerlukan node untuk dihitung ulang pada setiap traversal, sehingga sangat berbeda dari vanilla IEnumerables di .NET.

Juliet
sumber
5

Cuplikan ini menunjukkan perbedaan antara evaluasi malas dan tidak malas. Tentu saja fungsi fibonacci ini sendiri dapat dioptimalkan dan menggunakan evaluasi malas daripada rekursi, tapi itu akan merusak contoh.

Misalkan kita MUNGKIN harus menggunakan 20 angka pertama untuk sesuatu, tanpa evaluasi malas, semua 20 angka harus dibuat dimuka, tetapi, dengan evaluasi malas, angka tersebut akan dibuat sesuai kebutuhan saja. Dengan demikian Anda hanya akan membayar harga kalkulasi saat dibutuhkan.

Keluaran sampel

Bukan generasi malas: 0,023373
Generasi malas: 0,000009
Bukan keluaran malas: 0,000921
Keluaran malas: 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)
Vinko Vrsalovic
sumber
5

Evaluasi malas paling berguna dengan struktur data. Anda dapat mendefinisikan sebuah larik atau vektor secara induktif dengan menetapkan hanya titik-titik tertentu dalam struktur dan mengekspresikan semua yang lain dalam kerangka keseluruhan larik. Ini memungkinkan Anda menghasilkan struktur data dengan sangat ringkas dan dengan kinerja waktu proses yang tinggi.

Untuk melihat ini beraksi, Anda dapat melihat perpustakaan jaringan saraf saya yang disebut insting . Itu membuat banyak penggunaan evaluasi malas untuk keanggunan dan kinerja tinggi. Misalnya, saya benar-benar menyingkirkan kalkulasi aktivasi imperatif tradisional. Ekspresi malas yang sederhana melakukan segalanya untukku.

Ini digunakan misalnya dalam fungsi aktivasi dan juga dalam algoritma pembelajaran propagasi mundur (saya hanya dapat memposting dua tautan, jadi Anda harus mencari sendiri learnPatfungsi di AI.Instinct.Train.Deltamodul). Secara tradisional keduanya membutuhkan algoritma iteratif yang jauh lebih rumit.

ertes
sumber
4

Orang lain sudah memberikan semua alasan besarnya, tapi menurut saya latihan yang berguna untuk membantu memahami mengapa kemalasan itu penting adalah mencoba dan menulis fungsi titik tetap dalam bahasa yang ketat.

Di Haskell, fungsi titik tetap sangat mudah:

fix f = f (fix f)

ini berkembang menjadi

f (f (f ....

tetapi karena Haskell malas, rangkaian komputasi tak terbatas itu bukanlah masalah; evaluasi dilakukan "dari luar ke dalam", dan semuanya bekerja dengan sangat baik:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

Yang penting, yang penting bukan fixmalas, tapi fmalas. Begitu Anda sudah diberi aturan ketat f, Anda bisa mengangkat tangan ke udara dan menyerah, atau eta mengembangkannya dan mengacaukan semuanya. (Ini sangat mirip dengan apa yang dikatakan Nuh tentang perpustakaan yang ketat / malas, bukan bahasanya).

Sekarang bayangkan menulis fungsi yang sama dalam Scala yang ketat:

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

Anda tentu saja mendapatkan stack overflow. Jika Anda ingin berhasil, Anda perlu membuat fargumen panggilan-oleh-kebutuhan:

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)
Owen
sumber
3

Saya tidak tahu bagaimana Anda saat ini memikirkan berbagai hal, tetapi saya merasa berguna untuk memikirkan evaluasi malas sebagai masalah perpustakaan daripada fitur bahasa.

Maksud saya, dalam bahasa yang ketat, saya dapat mengimplementasikan evaluasi malas dengan membangun beberapa struktur data, dan dalam bahasa malas (setidaknya Haskell), saya dapat meminta ketegasan saat saya menginginkannya. Oleh karena itu, pilihan bahasa tidak benar-benar membuat program Anda malas atau tidak malas, tetapi hanya memengaruhi program yang Anda dapatkan secara default.

Setelah Anda memikirkannya seperti itu, pikirkan semua tempat di mana Anda menulis struktur data yang nantinya dapat Anda gunakan untuk menghasilkan data (tanpa melihatnya terlalu banyak sebelumnya), dan Anda akan melihat banyak kegunaan untuk malas. evaluasi.

Noah Lavine
sumber
1
Menerapkan evaluasi malas dalam bahasa yang ketat sering kali merupakan Turing Tarpit.
itsbruce
2

Eksploitasi paling berguna dari evaluasi malas yang saya gunakan adalah fungsi yang disebut serangkaian sub-fungsi dalam urutan tertentu. Jika salah satu dari sub-fungsi ini gagal (dikembalikan salah), fungsi pemanggil harus segera dikembalikan. Jadi saya bisa melakukannya dengan cara ini:

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

atau, solusi yang lebih elegan:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Setelah Anda mulai menggunakannya, Anda akan melihat peluang untuk menggunakannya lebih dan lebih sering.

Marc Bernier
sumber
2

Tanpa evaluasi malas Anda tidak akan diizinkan untuk menulis sesuatu seperti ini:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }
kupas
sumber
Yah, imo, ini adalah ide yang buruk untuk melakukan itu. Meskipun kode ini mungkin benar (tergantung pada apa yang Anda coba capai), sulit dibaca, yang selalu merupakan hal yang buruk.
Brann
12
Saya kira tidak. Ini adalah konstruksi standar di C dan kerabatnya.
Paul Johnson
Ini adalah contoh evaluasi sirkuit pendek, bukan evaluasi malas. Atau apakah itu sama secara efektif?
RufusVS
2

Antara lain, bahasa lazy memungkinkan struktur data tak hingga multidimensi.

Sementara skema, python, dll memungkinkan struktur data tak terbatas berdimensi tunggal dengan aliran, Anda hanya dapat melintasi sepanjang satu dimensi.

Kemalasan berguna untuk masalah pinggiran yang sama , tetapi perlu diperhatikan koneksi coroutines yang disebutkan dalam tautan itu.

shapr
sumber
2

Evaluasi malas adalah penalaran persamaan orang miskin (yang dapat diharapkan, idealnya, untuk mendeduksi properti kode dari properti tipe dan operasi yang terlibat).

Contoh di mana itu bekerja dengan cukup baik: sum . take 10 $ [1..10000000000] . Yang kami tidak keberatan direduksi menjadi jumlah 10 angka, bukan hanya satu perhitungan numerik langsung dan sederhana. Tanpa evaluasi malas tentu saja ini akan membuat daftar raksasa di memori hanya untuk menggunakan 10 elemen pertamanya. Ini pasti akan sangat lambat, dan mungkin menyebabkan kesalahan kehabisan memori.

Contoh di mana itu tidak besar seperti yang kita inginkan: sum . take 1000000 . drop 500 $ cycle [1..20]. Yang benar-benar akan menjumlahkan 1.000.000 angka, bahkan jika dalam lingkaran, bukan dalam daftar; masih harus direduksi menjadi hanya satu kalkulasi numerik langsung, dengan sedikit kondisional dan sedikit rumus. Yang akan jauh lebih baik daripada menjumlahkan 1.000.000 angka. Bahkan jika dalam satu lingkaran, dan tidak dalam daftar (yaitu setelah optimasi deforestasi).


Hal lain adalah, memungkinkan untuk membuat kode dalam gaya modulo kontra rekursi ekor , dan itu hanya berfungsi .

cf. jawaban terkait .

Will Ness
sumber
1

Jika dengan "evaluasi malas" yang Anda maksud seperti di boolean gabungan, seperti di

   if (ConditionA && ConditionB) ... 

maka jawabannya sederhana saja bahwa semakin sedikit siklus CPU yang dikonsumsi program, semakin cepat program tersebut berjalan ... dan jika sejumlah instruksi pemrosesan tidak akan berdampak pada hasil program maka itu tidak perlu, (dan karenanya sia-sia waktu) untuk melakukannya ...

jika otoh, maksud Anda apa yang saya kenal sebagai "penginisialisasi malas", seperti dalam:

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)); 
          } 
    }
}

Nah, teknik ini memungkinkan kode klien menggunakan kelas untuk menghindari kebutuhan memanggil database untuk catatan data Supervisor kecuali ketika klien yang menggunakan objek Employee memerlukan akses ke data supervisor ... ini membuat proses membuat instance Employee lebih cepat, namun saat Anda membutuhkan Supervisor, panggilan pertama ke properti Supervisor akan memicu panggilan Database dan data akan diambil dan tersedia ...

Charles Bretana
sumber
0

Kutipan dari fungsi orde tinggi

Mari kita cari angka terbesar di bawah 100.000 yang habis dibagi 3829. Untuk melakukannya, kita hanya akan memfilter sekumpulan kemungkinan yang kita tahu solusinya.

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

Pertama-tama kami membuat daftar semua angka yang lebih rendah dari 100.000, menurun. Kemudian kami memfilternya berdasarkan predikat kami dan karena angkanya diurutkan secara menurun, angka terbesar yang memenuhi predikat kami adalah elemen pertama dari daftar yang difilter. Kami bahkan tidak perlu menggunakan daftar terbatas untuk set awal kami. Itu kemalasan beraksi lagi. Karena kita hanya menggunakan kepala dari daftar yang difilter, tidak masalah apakah daftar yang difilter itu terbatas atau tidak terbatas. Evaluasi berhenti ketika solusi pertama yang memadai ditemukan.

onmyway133
sumber