TL; DR: Apakah bahasa fungsional menangani rekursi lebih baik daripada yang non-fungsional?
Saat ini saya sedang membaca Kode Selesai 2. Pada titik tertentu dalam buku ini, penulis memperingatkan kita tentang rekursi. Dia mengatakan itu harus dihindari bila memungkinkan dan bahwa fungsi menggunakan rekursi umumnya kurang efektif daripada solusi menggunakan loop. Sebagai contoh, penulis menulis fungsi Java menggunakan rekursi untuk menghitung faktorial dari angka seperti itu (mungkin tidak persis sama karena saya tidak memiliki buku dengan saya saat ini):
public int factorial(int x) {
if (x <= 0)
return 1;
else
return x * factorial(x - 1);
}
Ini disajikan sebagai solusi buruk. Namun, dalam bahasa fungsional, menggunakan rekursi sering kali merupakan cara yang disukai untuk melakukan sesuatu. Misalnya, inilah fungsi faktorial di Haskell menggunakan rekursi:
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
Dan diterima secara luas sebagai solusi yang baik. Seperti yang telah saya lihat, Haskell sangat sering menggunakan rekursi, dan saya tidak melihat di mana pun hal itu disukai.
Jadi pertanyaan saya pada dasarnya adalah:
- Apakah bahasa fungsional menangani rekursi lebih baik daripada yang non-fungsional?
EDIT: Saya sadar bahwa contoh yang saya gunakan bukan yang terbaik untuk menggambarkan pertanyaan saya. Saya hanya ingin menunjukkan bahwa Haskell (dan bahasa fungsional pada umumnya) menggunakan rekursi lebih sering daripada bahasa non-fungsional.
sumber
factorial n = product [1..n]
lebih ringkas, lebih efisien, dan tidak meluap tumpukan untuk besarn
(dan jika Anda membutuhkan memoisasi, opsi yang sama sekali berbeda diperlukan).product
didefinisikan dalam hal beberapafold
, yang adalah didefinisikan secara rekursif, tetapi dengan sangat hati-hati. Rekursi adalah solusi yang dapat diterima sebagian besar waktu, tetapi masih mudah untuk melakukannya dengan salah / suboptimal.Jawaban:
Ya, mereka melakukannya, tetapi bukan hanya karena mereka dapat , tetapi karena mereka harus melakukannya .
Konsep kunci di sini adalah kemurnian : fungsi murni adalah fungsi tanpa efek samping dan tanpa kondisi. Bahasa pemrograman fungsional umumnya mencakup kemurnian karena berbagai alasan, seperti alasan tentang kode dan menghindari ketergantungan yang tidak jelas. Beberapa bahasa, terutama Haskell, bahkan hanya mengizinkan kode murni; efek samping apa pun yang mungkin dimiliki program (seperti menjalankan I / O) dipindahkan ke runtime yang tidak murni, menjaga bahasa itu sendiri tetap murni.
Tidak memiliki efek samping berarti Anda tidak dapat memiliki penghitung lingkaran (karena penghitung lingkaran akan membentuk keadaan yang bisa berubah, dan memodifikasi keadaan tersebut akan menjadi efek samping), sehingga yang paling berulang yang bisa didapat bahasa fungsional murni adalah dengan mengulangi daftar ( operasi ini biasanya disebut
foreach
ataumap
). Rekursi, bagaimanapun, adalah kecocokan alami dengan pemrograman fungsional murni - tidak diperlukan kondisi rekursi, kecuali untuk argumen fungsi (hanya baca) dan nilai pengembalian (hanya-tulis).Namun, tidak memiliki efek samping juga berarti rekursi dapat diimplementasikan lebih efisien, dan kompiler dapat mengoptimalkannya secara lebih agresif. Saya belum mempelajari kompiler semacam itu secara mendalam, tetapi sejauh yang saya tahu, kebanyakan kompiler bahasa pemrograman fungsional melakukan optimasi panggilan ekor, dan beberapa bahkan dapat mengkompilasi beberapa jenis konstruksi rekursif ke dalam loop di belakang layar.
sumber
Anda membandingkan rekursi vs iterasi. Tanpa eliminasi tail-call , iterasi memang lebih efisien karena tidak ada panggilan fungsi tambahan. Juga, iterasi dapat berlangsung selamanya, sedangkan dimungkinkan untuk kehabisan ruang stack dari terlalu banyak panggilan fungsi.
Namun, iterasi membutuhkan perubahan penghitung. Itu berarti harus ada variabel yang bisa berubah , yang dilarang dalam pengaturan fungsional murni. Jadi bahasa fungsional dirancang khusus untuk beroperasi tanpa perlu iterasi, oleh karena itu fungsi yang disederhanakan memanggil.
Tapi tidak ada yang membahas mengapa sampel kode Anda begitu ramping. Contoh Anda menunjukkan properti yang berbeda, yaitu pencocokan pola . Itu sebabnya sampel Haskell tidak memiliki persyaratan eksplisit. Dengan kata lain, bukan rekursi efisien yang membuat kode Anda kecil; itu adalah pencocokan pola.
sumber
Secara teknis tidak, tetapi praktis ya.
Rekursi jauh lebih umum ketika Anda mengambil pendekatan fungsional untuk masalah tersebut. Karena itu, bahasa yang dirancang untuk menggunakan pendekatan fungsional sering menyertakan fitur yang membuat rekursi lebih mudah / lebih baik / kurang bermasalah. Dari atas kepala saya, ada tiga yang umum:
Optimasi Panggilan Ekor. Seperti yang ditunjukkan oleh poster lain, bahasa fungsional sering membutuhkan TCO.
Evaluasi Malas. Haskell (dan beberapa bahasa lainnya) dievaluasi dengan malas. Ini menunda 'pekerjaan' sebenarnya dari suatu metode sampai diperlukan. Ini cenderung mengarah pada struktur data yang lebih rekursif dan dengan perluasan, metode rekursif untuk bekerja pada mereka.
Kekekalan. Mayoritas hal yang Anda gunakan dalam bahasa pemrograman fungsional tidak dapat diubah. Ini membuat rekursi lebih mudah karena Anda tidak perlu khawatir dengan keadaan objek dari waktu ke waktu. Misalnya, Anda tidak dapat mengubah nilai dari bawah Anda. Juga, banyak bahasa dirancang untuk mendeteksi fungsi murni . Karena fungsi murni tidak memiliki efek samping, kompiler memiliki lebih banyak kebebasan tentang urutan fungsi yang dijalankan dan optimisasi lainnya.
Tidak satu pun dari hal-hal ini yang benar-benar spesifik untuk bahasa fungsional dibandingkan yang lain, jadi mereka tidak hanya lebih baik karena mereka fungsional. Tetapi karena mereka fungsional, keputusan desain yang dibuat cenderung ke fitur-fitur ini karena mereka lebih berguna (dan kelemahannya kurang bermasalah) ketika pemrograman secara fungsional.
sumber
Haskell dan bahasa fungsional lainnya umumnya menggunakan evaluasi malas. Fitur ini memungkinkan Anda menulis fungsi rekursif tanpa akhir.
Jika Anda menulis fungsi rekursif tanpa mendefinisikan kasus dasar di mana rekursi berakhir, Anda berakhir dengan memiliki panggilan tak terbatas ke fungsi tersebut dan stackoverflow.
Haskell juga mendukung optimisasi panggilan fungsi rekursif. Di Jawa, setiap panggilan fungsi akan menumpuk dan menyebabkan overhead.
Jadi ya, bahasa fungsional menangani rekursi lebih baik daripada yang lain.
sumber
forever a = a >> forever a
).Satu-satunya alasan teknis yang saya ketahui adalah bahwa beberapa bahasa fungsional (dan beberapa bahasa imperatif jika saya ingat) memiliki apa yang disebut optimasi panggilan ekor yang memungkinkan metode rekursif untuk tidak menambah ukuran tumpukan dengan setiap panggilan rekursif (mis. Panggilan rekursif lebih atau kurang menggantikan panggilan saat ini di stack).
Perhatikan, bahwa pengoptimalan ini tidak berfungsi pada panggilan rekursif apa pun , hanya metode rekursif panggilan ekor (mis. Metode yang tidak mempertahankan status pada saat panggilan rekursif)
sumber
Anda akan ingin melihat Pengumpulan Sampah Cepat, Tapi Tumpukan Lebih Cepat , makalah tentang menggunakan apa yang akan dipikirkan oleh programmer C sebagai "tumpukan" untuk frame tumpukan di kompilasi C. Saya percaya penulis mengutak-atik Gcc untuk melakukan itu . Itu bukan jawaban yang pasti, tetapi itu mungkin membantu Anda memahami beberapa masalah dengan rekursi.
The Alef bahasa pemrograman , yang digunakan untuk datang bersama dengan Plan 9 dari Bell Labs, memiliki "menjadi" Pernyataan (Lihat bagian 6.6.4 dari referensi ini ). Ini semacam optimisasi rekursi ekor-panggilan eksplisit. "Tapi itu menggunakan tumpukan panggilan!" Argumen menentang rekursi berpotensi dapat dihilangkan.
sumber
TL; DR: Ya, mereka melakukan
Rekursi adalah alat utama dalam pemrograman fungsional dan karena itu banyak pekerjaan telah dilakukan untuk mengoptimalkan panggilan-panggilan ini. Sebagai contoh, R5RS mensyaratkan (dalam spek!) Bahwa semua implementasi menangani panggilan rekursi ekor yang tidak terikat tanpa programmer khawatir tentang stack overflow. Sebagai perbandingan, secara default kompiler C tidak akan melakukan optimasi tail-call yang jelas (coba kebalikan rekursif dari daftar tertaut) dan setelah beberapa panggilan, program akan berakhir (kompiler akan mengoptimalkan, meskipun, jika Anda menggunakan - O2).
Tentu saja, dalam program yang ditulis dengan mengerikan, seperti
fib
contoh terkenal yang eksponensial, kompiler memiliki sedikit atau tidak ada pilihan untuk melakukan 'sihirnya'. Jadi perhatian harus diambil untuk tidak menghalangi upaya kompiler dalam optimasi.EDIT: Dengan contoh fib, maksud saya yang berikut:
sumber
Bahasa fungsional lebih baik pada dua jenis rekursi yang sangat spesifik: rekursi ekor dan rekursi tak terbatas. Mereka sama buruknya dengan bahasa lain pada jenis rekursi lainnya, seperti
factorial
contoh Anda .Itu tidak berarti tidak ada algoritma yang bekerja dengan baik dengan rekursi reguler di kedua paradigma. Misalnya, apa pun yang memerlukan struktur data seperti tumpukan, seperti pencarian pohon kedalaman-pertama, paling sederhana untuk diterapkan dengan rekursi.
Rekursi muncul lebih sering dengan pemrograman fungsional, tetapi juga terlalu sering digunakan, terutama oleh pemula atau dalam tutorial untuk pemula, mungkin karena kebanyakan pemula untuk pemrograman fungsional telah menggunakan rekursi sebelumnya dalam pemrograman imperatif. Ada konstruksi pemrograman fungsional lainnya, seperti pemahaman daftar, fungsi tingkat tinggi, dan operasi lain pada koleksi, yang biasanya jauh lebih cocok secara konseptual, untuk gaya, untuk keringkasan, untuk efisiensi, dan untuk kemampuan untuk mengoptimalkan.
Misalnya, saran delnan tentang
factorial n = product [1..n]
tidak hanya lebih ringkas dan lebih mudah dibaca, tetapi juga sangat paralel. Sama untuk menggunakanfold
ataureduce
jika bahasa Anda tidak memilikiproduct
bawaan. Rekursi adalah solusi terakhir untuk masalah ini. Alasan utama Anda melihatnya dipecahkan secara berulang dalam tutorial adalah sebagai titik awal sebelum mendapatkan solusi yang lebih baik, bukan sebagai contoh praktik terbaik.sumber