Bagaimana fungsi fibonacci ini dikosongkan?

114

Dengan mekanisme apa fungsi fibonacci ini dimoalkan?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

Dan pada catatan terkait, mengapa versi ini tidak?

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    
bjornars
sumber
13
Sedikit tidak terkait, fib 0tidak berakhir: Anda mungkin ingin kasus dasar fib'menjadi fib' 0 = 0dan fib' 1 = 1.
huon
1
Perhatikan bahwa versi pertama dapat dibuat lebih ringkas: fibs = 1:1:zipWith (+) fibs (tail fibs)dan fib = (fibs !!).
Bastian

Jawaban:

95

Mekanisme evaluasi di Haskell adalah dengan kebutuhan : ketika sebuah nilai dibutuhkan, itu dihitung, dan tetap siap jika diminta lagi. Jika kita mendefinisikan beberapa daftar, xs=[0..]dan kemudian meminta elemen ke-100, xs!!99slot ke-100 dalam daftar akan "disempurnakan", menahan nomor tersebut 99sekarang, siap untuk akses berikutnya.

Itulah yang dieksploitasi oleh trik itu, "melalui daftar". Dalam definisi Fibonacci rekursi ganda normal fib n = fib (n-1) + fib (n-2), fungsi itu sendiri dipanggil, dua kali dari atas, menyebabkan ledakan eksponensial. Tapi dengan trik itu, kami membuat daftar untuk hasil sementara, dan pergi "melalui daftar":

fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

Triknya adalah membuat daftar itu dibuat, dan menyebabkan daftar itu tidak hilang (melalui pengumpulan sampah) di antara panggilan ke fib. Cara termudah untuk mencapai ini, adalah dengan menamai daftar itu. "Jika Anda menyebutkannya, itu akan tetap ada."


Versi pertama Anda mendefinisikan konstanta monomorfik, dan versi kedua mendefinisikan fungsi polimorfik. Fungsi polimorfik tidak dapat menggunakan daftar internal yang sama untuk jenis berbeda yang mungkin perlu dilayaninya, jadi tidak ada pembagian , yaitu tidak ada memoisasi.

Dengan versi pertama, compiler bermurah hati dengan kami, mengambil subekspresi konstan ( map fib' [0..]) dan menjadikannya entitas terpisah yang dapat dibagikan, tetapi tidak ada kewajiban untuk melakukannya. dan sebenarnya ada kasus di mana kami tidak ingin melakukannya untuk kami secara otomatis.

( sunting :) Pertimbangkan penulisan ulang ini:

fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

Jadi kisah sebenarnya tampaknya tentang definisi lingkup bersarang. Tidak ada lingkup luar dengan definisi pertama, dan definisi ketiga berhati-hati untuk tidak memanggil lingkup luar fib3, tetapi tingkat yang sama f.

Setiap pemanggilan baru fib2tampaknya membuat definisi bersarangnya lagi karena salah satu dari mereka dapat (secara teori) didefinisikan secara berbeda tergantung pada nilai n(terima kasih kepada Vitus dan Tikhon untuk menunjukkannya). Dengan definisi pertama tidak ada yang nbisa diandalkan, dan dengan definisi ketiga ada ketergantungan, tetapi setiap panggilan terpisah ke fib3panggilan fyang berhati-hati untuk hanya memanggil definisi dari lingkup tingkat yang sama, internal ke pemanggilan spesifik ini fib3, sehingga hal yang sama xsakan terjadi. digunakan kembali (yaitu dibagikan) untuk pemanggilan itu fib3.

Tetapi tidak ada yang menghalangi kompilator untuk mengenali bahwa definisi internal dalam salah satu versi di atas sebenarnya tidak bergantung pada npengikatan luar , untuk melakukan pengangkatan lambda bagaimanapun juga, menghasilkan memoisasi penuh (kecuali untuk definisi polimorfik). Sebenarnya itulah yang terjadi dengan ketiga versi ketika dideklarasikan dengan tipe monomorphic dan dikompilasi dengan flag -O2. Dengan deklarasi tipe polimorfik, fib3menunjukkan berbagi lokal dan fib2tidak berbagi sama sekali.

Pada akhirnya, bergantung pada kompilator, dan pengoptimalan kompilator yang digunakan, dan bagaimana Anda mengujinya (memuat file di GHCI, dikompilasi atau tidak, dengan -O2 atau tidak, atau mandiri), dan apakah ia mendapat jenis monomorfik atau polimorfik, perilaku mungkin berubah sepenuhnya - apakah itu menunjukkan berbagi lokal (per panggilan) (yaitu waktu linier pada setiap panggilan), memoisasi (yaitu waktu linier pada panggilan pertama, dan 0 waktu pada panggilan berikutnya dengan argumen yang sama atau lebih kecil), atau tidak berbagi sama sekali ( waktu eksponensial).

Jawaban singkatnya adalah, ini adalah kompiler. :)

Will Ness
sumber
4
Hanya untuk memperbaiki detail kecil: versi kedua tidak mendapatkan pembagian setiap terutama karena fungsi lokal fib'didefinisikan ulang untuk setiap ndan dengan demikian fib'di fib 1fib'di fib 2, yang juga berarti daftar yang berbeda. Bahkan jika Anda memperbaiki tipe menjadi monomorfik, itu masih menunjukkan perilaku ini.
Vitus
1
whereklausa memperkenalkan berbagi seperti letekspresi, tetapi mereka cenderung menyembunyikan masalah seperti ini. Menulis ulang sedikit lebih eksplisit, Anda mendapatkan ini: hpaste.org/71406
Vitus
1
Hal menarik lainnya tentang penulisan ulang Anda: jika Anda memberi mereka tipe monomorfik (yaitu Int -> Integer), kemudian fib2berjalan dalam waktu eksponensial, fib1dan fib3keduanya berjalan dalam waktu linier tetapi fib1juga dimoisasi - sekali lagi karena untuk fib3definisi lokal didefinisikan ulang untuk setiap n.
Vitus
1
@misterbee Tetapi memang akan menyenangkan memiliki semacam jaminan dari kompilator; semacam kontrol atas tempat tinggal memori dari entitas tertentu. Terkadang kami ingin berbagi, terkadang kami ingin mencegahnya. Saya membayangkan / berharap itu harus mungkin ...
Will Ness
1
@ElizaBrandt yang saya maksud adalah bahwa terkadang kami ingin menghitung ulang sesuatu yang berat sehingga tidak disimpan untuk kami dalam memori - yaitu biaya penghitungan ulang lebih rendah daripada biaya retensi memori yang besar. salah satu contohnya adalah pembuatan PowerSet: di sini pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]kami ingin pwr xsdihitung secara mandiri, dua kali, sehingga sampah tersebut dapat dikumpulkan dengan cepat saat diproduksi dan dikonsumsi.
Will Ness
23

Saya tidak sepenuhnya yakin, tapi inilah tebakan yang masuk akal:

Compiler berasumsi bahwa ini fib nbisa berbeda di tempat yang berbeda ndan oleh karena itu perlu menghitung ulang daftar tersebut setiap saat. Bit-bit di dalam wherepernyataan itu bisa bergantung n. Artinya, dalam hal ini, seluruh daftar angka pada dasarnya adalah fungsi dari n.

Versi tanpa n dapat membuat daftar sekali dan membungkusnya dalam sebuah fungsi. Daftar tidak dapat bergantung pada nilai yang nditeruskan dan ini mudah diverifikasi. Daftar ini adalah konstanta yang kemudian diindeks menjadi. Ini, tentu saja, adalah konstanta yang dievaluasi secara malas, jadi program Anda tidak mencoba untuk segera mendapatkan seluruh daftar (tak terbatas). Karena ini adalah konstanta, ini dapat dibagikan ke seluruh pemanggilan fungsi.

Ini dikosongkan sama sekali karena panggilan rekursif hanya perlu mencari nilai dalam daftar. Sejak fibversi membuat daftar sekali malas, itu hanya menghitung cukup untuk mendapatkan jawaban tanpa melakukan perhitungan yang berlebihan. Di sini, "malas" berarti bahwa setiap entri dalam daftar adalah thunk (ekspresi yang tidak dievaluasi). Ketika Anda melakukan evaluasi dunk, menjadi nilai, sehingga mengakses waktu berikutnya tidak ada mengulang perhitungan. Karena daftar dapat dibagi di antara panggilan, semua entri sebelumnya sudah dihitung pada saat Anda membutuhkan yang berikutnya.

Ini pada dasarnya adalah bentuk pemrograman dinamis yang cerdas dan murah berdasarkan semantik malas GHC. Saya pikir standar hanya menentukan bahwa itu harus tidak ketat , jadi kompiler yang patuh berpotensi mengkompilasi kode ini untuk tidak memo. Namun, dalam praktiknya, setiap kompilator yang masuk akal akan menjadi malas.

Untuk informasi lebih lanjut tentang mengapa kasus kedua berfungsi, baca Memahami daftar yang didefinisikan secara rekursif (fib dalam istilah zipWith) .

Tikhon Jelvis
sumber
apakah maksud Anda fib' nmungkin " bisa berbeda di lain n"?
Will Ness
Saya pikir saya tidak terlalu jelas: yang saya maksud adalah bahwa segala sesuatu di dalamnya fib, termasuk fib', dapat berbeda pada setiap perbedaan n. Menurut saya contoh aslinya agak sedikit membingungkan karena fib'juga tergantung nbayangannya sendiri yang mana yang lain n.
Tikhon Jelvis
20

Pertama, dengan ghc-7.4.2, dikompilasi dengan -O2, versi non-memoised tidak terlalu buruk, daftar internal nomor Fibonacci masih dikompilasi untuk setiap panggilan tingkat atas ke fungsi tersebut. Tetapi ini tidak, dan tidak dapat secara masuk akal, dicatat di berbagai panggilan tingkat atas. Namun, untuk versi lainnya, daftar tersebut dibagikan ke semua panggilan.

Itu karena pembatasan monomorfisme.

Yang pertama terikat oleh pengikatan pola sederhana (hanya nama, tidak ada argumen), oleh karena itu dengan pembatasan monomorfisme ia harus mendapatkan tipe monomorfik. Jenis yang disimpulkan adalah

fib :: (Num n) => Int -> n

dan batasan seperti itu menjadi default (jika tidak ada deklarasi default yang mengatakan sebaliknya) ke Integer, memperbaiki tipe sebagai

fib :: Int -> Integer

Jadi, hanya ada satu daftar (jenis [Integer]) yang perlu diingat.

Yang kedua didefinisikan dengan argumen fungsi, sehingga tetap polimorfik, dan jika daftar internal telah dimoisasi di seluruh panggilan, satu daftar harus dimoisasi untuk setiap tipe dalam Num. Itu tidak praktis.

Kompilasi kedua versi dengan pembatasan monomorfisme dinonaktifkan, atau dengan jenis tanda tangan yang identik, dan keduanya menunjukkan perilaku yang persis sama. (Itu tidak benar untuk versi kompilator yang lebih lama, saya tidak tahu versi mana yang pertama kali melakukannya.)

Daniel Fischer
sumber
Mengapa tidak praktis untuk membuat memo daftar untuk setiap jenis? Pada prinsipnya, dapatkah GHC membuat kamus (seperti untuk memanggil fungsi yang dibatasi kelas jenis) untuk memuat daftar yang dihitung sebagian untuk setiap jenis Num yang ditemukan selama runtime?
misterbee
1
@misterbee Pada prinsipnya, itu bisa, tetapi jika program memanggil fib 1000000banyak tipe, itu memakan banyak memori. Untuk menghindarinya, seseorang akan membutuhkan heuristik yang mendaftar untuk membuang cache ketika itu tumbuh terlalu besar. Dan strategi memoisation seperti itu juga akan berlaku untuk fungsi atau nilai lain, mungkin, jadi kompilator harus berurusan dengan sejumlah besar hal yang berpotensi untuk dimoise untuk banyak tipe yang berpotensi. Saya pikir akan mungkin untuk mengimplementasikan memoisation (parsial) polimorfik dengan heuristik yang cukup baik, tapi saya ragu itu akan bermanfaat.
Daniel Fischer
5

Anda tidak perlu fungsi memoize untuk Haskell. Hanya bahasa pemrograman empiratif yang membutuhkan fungsi itu. Namun, Haskel adalah bahasa fungsional dan ...

Jadi, ini contoh algoritma Fibonacci yang sangat cepat:

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipWith adalah fungsi dari Prelude standar:

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

Uji:

print $ take 100 fib

Keluaran:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

Waktu berlalu: 0,00018s

Валерий Кобзарь
sumber
Solusi ini luar biasa!
Larry