... atau itu hanya latihan?
Saya menanyakan hal ini karena berdebat dengan profesor saya: Saya kehilangan kredit karena memanggil fungsi secara rekursif atas dasar bahwa kami tidak mencakup rekursi di kelas, dan argumen saya adalah bahwa kami mempelajarinya secara implisit dengan pembelajaran return
dan metode.
Saya bertanya di sini karena saya curiga seseorang memiliki jawaban pasti.
Misalnya, apa perbedaan antara dua metode berikut:
public static void a() {
return a();
}
public static void b() {
return a();
}
Selain " a
berlanjut selamanya" (dalam program sebenarnya, ini digunakan dengan benar untuk meminta pengguna lagi saat diberikan masukan yang tidak valid), apakah ada perbedaan mendasar antara a
dan b
? Untuk kompiler yang tidak dioptimalkan, bagaimana mereka ditangani secara berbeda?
Pada akhirnya turun ke apakah dengan belajar return a()
dari b
yang kami juga untuk itu belajar return a()
dari a
. Apakah kita?
a() { do { good = prompt(); } while (!good); }
.Jawaban:
Untuk menjawab pertanyaan spesifik Anda: Tidak, dari sudut pandang belajar bahasa, rekursi bukanlah fitur. Jika profesor Anda benar-benar memberi Anda nilai karena menggunakan "fitur" yang belum dia ajarkan, itu salah.
Membaca yang tersirat, salah satu kemungkinannya adalah dengan menggunakan rekursi, Anda menghindari penggunaan fitur yang seharusnya menjadi hasil pembelajaran untuk kursusnya. Misalnya, mungkin Anda tidak menggunakan iterasi sama sekali, atau mungkin Anda hanya menggunakan
for
loop daripada menggunakan keduanyafor
danwhile
. Merupakan hal yang umum bahwa sebuah tugas bertujuan untuk menguji kemampuan Anda dalam melakukan hal-hal tertentu, dan jika Anda menghindarinya, profesor Anda tidak dapat memberi Anda nilai yang disisihkan untuk fitur tersebut. Namun, jika itu benar-benar penyebab nilai Anda hilang, profesor harus menganggap ini sebagai pengalaman belajarnya sendiri - jika menunjukkan hasil pembelajaran tertentu adalah salah satu kriteria untuk sebuah tugas, yang harus dijelaskan dengan jelas kepada siswa .Karena itu, saya setuju dengan sebagian besar komentar dan jawaban lain bahwa iterasi adalah pilihan yang lebih baik daripada rekursi di sini. Ada beberapa alasan, dan sementara orang lain telah menyinggungnya sampai batas tertentu, saya tidak yakin mereka telah sepenuhnya menjelaskan pemikiran di baliknya.
Stack Overflows
Yang lebih jelas adalah bahwa Anda berisiko mendapatkan kesalahan stack overflow. Secara realistis, metode yang Anda tulis sangat tidak mungkin benar-benar mengarah ke metode tersebut, karena pengguna harus memberikan masukan yang salah berkali-kali untuk benar-benar memicu luapan tumpukan.
Namun, satu hal yang perlu diingat adalah bahwa tidak hanya metode itu sendiri, tetapi metode lain yang lebih tinggi atau lebih rendah dalam rantai panggilan akan ada di tumpukan. Karena itu, dengan santai melahap ruang tumpukan yang tersedia adalah hal yang sangat tidak sopan untuk dilakukan metode apa pun. Tidak ada yang ingin terus-menerus khawatir tentang ruang tumpukan kosong setiap kali mereka menulis kode karena risiko kode lain mungkin telah menggunakan banyak darinya secara tidak perlu.
Ini adalah bagian dari prinsip yang lebih umum dalam desain perangkat lunak yang disebut abstraksi. Pada dasarnya, saat Anda menelepon
DoThing()
, yang perlu Anda perhatikan hanyalah bahwa Sesuatu sudah selesai. Anda tidak perlu khawatir tentang detail implementasi tentang cara melakukannya. Tetapi penggunaan stack yang serakah melanggar prinsip ini, karena setiap bit kode harus mengkhawatirkan tentang berapa banyak stack yang dapat diasumsikannya dengan aman oleh kode di tempat lain di rantai panggilan.Keterbacaan
Alasan lainnya adalah keterbacaan. Ideal yang harus dicita-citakan oleh kode adalah menjadi dokumen yang dapat dibaca manusia, di mana setiap baris mendeskripsikan apa yang dilakukannya. Ambil dua pendekatan ini:
melawan
Ya, keduanya berfungsi, dan ya keduanya cukup mudah dipahami. Tetapi bagaimana kedua pendekatan tersebut dapat dijelaskan dalam bahasa Inggris? Saya pikir itu akan menjadi seperti:
melawan
Mungkin Anda dapat memikirkan kata-kata yang tidak terlalu kikuk untuk yang terakhir, tetapi saya pikir Anda akan selalu menemukan bahwa yang pertama akan menjadi deskripsi yang lebih akurat, secara konseptual, tentang apa yang sebenarnya Anda coba lakukan. Ini tidak berarti rekursi selalu kurang terbaca. Untuk situasi di mana ia bersinar, seperti penjelajahan pohon, Anda dapat melakukan analisis berdampingan yang sama antara rekursi dan pendekatan lain dan Anda hampir pasti akan menemukan rekursi memberikan kode yang lebih jelas mendeskripsikan dirinya sendiri, baris demi baris.
Dalam isolasi, keduanya adalah poin kecil. Sangat tidak mungkin hal ini akan benar-benar menyebabkan stack overflow, dan peningkatan keterbacaan kecil. Tetapi program apa pun akan menjadi kumpulan dari banyak keputusan kecil ini, jadi meskipun terpisah itu tidak terlalu penting, penting untuk mempelajari prinsip di balik melakukannya dengan benar.
sumber
Untuk menjawab pertanyaan literal, bukan pertanyaan meta: rekursi adalah fitur, dalam arti tidak semua kompiler dan / atau bahasa mengizinkannya. Dalam praktiknya, ini diharapkan dari semua kompiler modern (biasa) - dan tentunya semua kompiler Java! - tapi itu tidak benar secara universal .
Sebagai contoh yang dibuat-buat mengapa rekursi mungkin tidak didukung, pertimbangkan kompilator yang menyimpan alamat pengembalian untuk suatu fungsi di lokasi statis; ini mungkin terjadi, misalnya, untuk kompiler untuk mikroprosesor yang tidak memiliki tumpukan.
Untuk kompiler seperti itu, ketika Anda memanggil fungsi seperti ini
itu diimplementasikan sebagai
dan definisi a (),
diimplementasikan sebagai
Mudah-mudahan masalah ketika Anda mencoba memanggil
a()
secara rekursif dalam kompiler seperti itu sudah jelas; kompilator tidak lagi tahu bagaimana untuk kembali dari panggilan luar, karena alamat pengirim telah ditimpa.Untuk kompiler yang sebenarnya saya gunakan (akhir 70-an atau awal 80-an, menurut saya) tanpa dukungan untuk rekursi, masalahnya sedikit lebih halus daripada itu: alamat pengirim akan disimpan di tumpukan, seperti di kompiler modern, tetapi variabel lokal tidak t. (Secara teoritis ini berarti bahwa rekursi dimungkinkan untuk fungsi tanpa variabel lokal non-statis, tetapi saya tidak ingat apakah kompiler secara eksplisit mendukungnya atau tidak. Mungkin diperlukan variabel lokal implisit karena beberapa alasan.)
Melihat ke depan, saya dapat membayangkan skenario khusus - sistem yang sangat paralel, mungkin - di mana tidak harus menyediakan tumpukan untuk setiap utas dapat menguntungkan, dan oleh karena itu rekursi hanya diizinkan jika kompiler dapat merefaktornya menjadi loop. (Tentu saja kompiler primitif yang saya diskusikan di atas tidak mampu melakukan tugas-tugas rumit seperti kode refactoring.)
sumber
Guru ingin tahu apakah Anda pernah belajar atau belum. Rupanya Anda tidak menyelesaikan masalah dengan cara dia mengajari Anda ( cara yang baik ; iterasi), dan dengan demikian, menganggap bahwa Anda tidak. Saya mendukung solusi kreatif tetapi dalam kasus ini saya harus setuju dengan guru Anda karena alasan yang berbeda:
Jika pengguna memberikan masukan yang tidak valid terlalu banyak (yaitu dengan terus menekan enter), Anda akan memiliki pengecualian stack overflow dan solusi akan macet. Selain itu, solusi iteratif lebih efisien dan lebih mudah dirawat. Saya pikir itulah alasan guru Anda seharusnya memberi Anda.
sumber
Mengurangi poin karena "kami tidak menutupi rekursi di kelas" sangat buruk. Jika Anda telah mempelajari cara memanggil fungsi A yang memanggil fungsi B yang memanggil fungsi C yang kembali ke B yang kembali ke A yang kembali ke pemanggil, dan guru tidak memberi tahu Anda secara eksplisit bahwa ini pasti fungsi yang berbeda (yang mana akan menjadi kasus di versi FORTRAN lama, misalnya), tidak ada alasan bahwa A, B dan C tidak bisa semuanya memiliki fungsi yang sama.
Di sisi lain, kami harus melihat kode sebenarnya untuk memutuskan apakah dalam kasus khusus Anda menggunakan rekursi benar-benar hal yang benar untuk dilakukan. Tidak banyak detail, tetapi kedengarannya salah.
sumber
Ada banyak sudut pandang untuk dilihat terkait pertanyaan spesifik yang Anda ajukan, tetapi yang dapat saya katakan adalah bahwa dari sudut pandang belajar bahasa, rekursi bukanlah fitur tersendiri. Jika profesor Anda benar-benar memberi Anda nilai untuk menggunakan "fitur" yang belum dia ajarkan, itu salah tapi seperti yang saya katakan, ada sudut pandang lain yang perlu dipertimbangkan di sini yang sebenarnya membuat profesor benar saat mengurangi poin.
Dari apa yang dapat saya simpulkan dari pertanyaan Anda, menggunakan fungsi rekursif untuk meminta masukan jika terjadi kegagalan masukan bukanlah praktik yang baik karena setiap panggilan fungsi rekursif didorong ke tumpukan. Karena rekursi ini didorong oleh input pengguna, dimungkinkan untuk memiliki fungsi rekursif tak terbatas dan dengan demikian menghasilkan StackOverflow.
Tidak ada perbedaan antara 2 contoh yang Anda sebutkan dalam pertanyaan Anda dalam arti apa yang mereka lakukan (tetapi berbeda dengan cara lain) - Dalam kedua kasus, alamat pengirim dan semua info metode sedang dimuat ke stack. Dalam kasus rekursi, alamat yang dikembalikan hanyalah baris tepat setelah pemanggilan metode (tentu saja ini tidak persis seperti yang Anda lihat dalam kode itu sendiri, melainkan dalam kode yang dibuat kompilator). Di Java, C, dan Python, rekursi cukup mahal dibandingkan dengan iterasi (secara umum) karena memerlukan alokasi stack frame baru. Belum lagi Anda bisa mendapatkan pengecualian stack overflow jika input tidak valid terlalu banyak.
Saya percaya profesor mengurangi poin karena rekursi dianggap sebagai subjeknya sendiri dan tidak mungkin seseorang yang tidak memiliki pengalaman pemrograman akan memikirkan rekursi. (Tentu saja itu tidak berarti mereka tidak akan melakukannya, tetapi itu tidak mungkin).
IMHO, saya pikir profesor benar dengan mengurangi poin Anda. Anda dapat dengan mudah mengambil bagian validasi ke metode lain dan menggunakannya seperti ini:
Jika apa yang Anda lakukan memang bisa diselesaikan dengan cara itu maka apa yang Anda lakukan adalah amalan yang buruk dan harus dihindari.
sumber
hasWon(x, y, piece)
(untuk hanya memeriksa baris dan kolom yang terpengaruh).Mungkin dosen Anda belum mengajarkannya, tetapi sepertinya Anda siap mempelajari kelebihan dan kekurangan rekursi.
Keuntungan utama rekursi adalah bahwa algoritma rekursif seringkali lebih mudah dan lebih cepat untuk ditulis.
Kerugian utama dari rekursi adalah bahwa algoritma rekursif dapat menyebabkan luapan tumpukan, karena setiap tingkat rekursi membutuhkan bingkai tumpukan tambahan untuk ditambahkan ke tumpukan.
Untuk kode produksi, di mana penskalaan dapat menghasilkan lebih banyak tingkat rekursi dalam produksi daripada dalam pengujian unit pemrogram, kerugiannya biasanya lebih besar daripada keuntungannya, dan kode rekursif sering dihindari jika praktis.
sumber
Mengenai pertanyaan spesifik, apakah rekursi merupakan fitur, saya cenderung mengatakan ya, tetapi setelah menafsirkan ulang pertanyaan tersebut. Ada pilihan desain umum dari bahasa dan kompiler yang memungkinkan rekursi, dan bahasa lengkap Turing memang ada yang tidak mengizinkan rekursi sama sekali . Dengan kata lain, rekursi adalah kemampuan yang diaktifkan oleh pilihan tertentu dalam desain bahasa / kompilator.
Mendukung fungsi kelas satu memungkinkan rekursi dengan asumsi yang sangat minimal; lihat menulis loop di Unlambda sebagai contoh, atau ekspresi Python tumpul ini yang tidak berisi referensi sendiri, loop, atau tugas:
Bahasa / kompiler yang menggunakan pengikatan akhir , atau yang mendefinisikan deklarasi maju , memungkinkan rekursi. Misalnya, sementara Python mengizinkan kode di bawah ini, itu adalah pilihan desain (pengikatan akhir), bukan persyaratan untuk sistem lengkap Turing . Fungsi yang saling rekursif sering bergantung pada dukungan untuk deklarasi maju.
Bahasa yang diketik secara statis yang memungkinkan jenis yang ditentukan secara rekursif berkontribusi untuk memungkinkan rekursi. Lihat implementasi Y Combinator ini di Go . Tanpa tipe yang didefinisikan secara rekursif, masih mungkin untuk menggunakan rekursi di Go, tapi saya yakin kombinator Y secara khusus tidak mungkin.
sumber
Dari apa yang dapat saya simpulkan dari pertanyaan Anda, menggunakan fungsi rekursif untuk meminta masukan jika terjadi kegagalan masukan bukanlah praktik yang baik. Mengapa?
Karena setiap panggilan fungsi rekursif didorong ke stack. Karena rekursi ini didorong oleh input pengguna, dimungkinkan untuk memiliki fungsi rekursif tak terbatas dan dengan demikian menghasilkan StackOverflow :-p
Memiliki loop non rekursif untuk melakukan ini adalah cara yang tepat.
sumber
while(true)
pemanggilan metode yang sama? Jika demikian, saya tidak akan mengatakan ini mendukung perbedaan apa pun antara rekursi, baik untuk diketahui apa adanya.while(true)
adalah pengulangan tanpa batas. Kecuali Anda memilikibreak
pernyataan, saya tidak mengerti maksudnya, kecuali Anda mencoba merusak program Anda lol. Maksud saya adalah, jika Anda memanggil metode yang sama (itu rekursi), terkadang akan memberi Anda StackOverflowError , tetapi jika Anda menggunakanwhile
ataufor
loop, itu tidak akan terjadi. Masalahnya tidak ada dengan loop biasa. Mungkin saya salah paham, tapi jawaban saya untuk Anda adalah tidak.Rekursi adalah konsep pemrograman , fitur (seperti iterasi), dan praktik . Seperti yang Anda lihat dari tautan, ada domain besar penelitian yang didedikasikan untuk subjek tersebut. Mungkin kita tidak perlu membahas topik sedalam itu untuk memahami poin-poin ini.
Rekursi sebagai fitur
Sederhananya, Java mendukungnya secara implisit, karena memungkinkan sebuah metode (yang pada dasarnya adalah fungsi khusus) untuk memiliki "pengetahuan" tentang dirinya sendiri dan metode lain yang menyusun kelasnya. Pertimbangkan bahasa di mana ini bukan masalahnya: Anda akan dapat menulis tubuh metode itu
a
, tetapi Anda tidak akan dapat menyertakan panggilan kea
dalamnya. Satu-satunya solusi adalah menggunakan iterasi untuk mendapatkan hasil yang sama. Dalam bahasa seperti itu, Anda harus membuat perbedaan antara fungsi yang menyadari keberadaannya sendiri (dengan menggunakan token sintaks tertentu), dan yang tidak! Sebenarnya, seluruh kelompok bahasa membuat perbedaan itu (lihat keluarga Lisp dan ML misalnya). Menariknya, Perl bahkan mengizinkan fungsi anonim (disebutlambdas ) untuk memanggil dirinya sendiri secara rekursif (sekali lagi, dengan sintaks khusus).tidak ada rekursi?
Untuk bahasa yang bahkan tidak mendukung kemungkinan rekursi, seringkali ada solusi lain, dalam bentuk kombinator titik tetap , tetapi masih membutuhkan bahasa untuk mendukung fungsi yang disebut objek kelas pertama (yaitu objek yang mungkin dimanipulasi dalam bahasa itu sendiri).
Rekursi sebagai praktik
Memiliki fitur itu tersedia dalam suatu bahasa tidak berarti itu idiomatis. Di Java 8, ekspresi lambda telah disertakan, jadi mungkin akan lebih mudah untuk mengadopsi pendekatan fungsional untuk pemrograman. Namun, ada pertimbangan praktis:
Garis bawah
Untungnya (atau lebih tepatnya, untuk kemudahan penggunaan), Java membiarkan metode menyadari dirinya sendiri secara default, dan dengan demikian mendukung rekursi, jadi ini sebenarnya bukan masalah praktis, tetapi masih tetap bersifat teoritis, dan saya kira itu guru Anda ingin menanganinya secara spesifik. Selain itu, mengingat perkembangan bahasa baru-baru ini, mungkin akan berubah menjadi sesuatu yang penting di masa depan.
sumber