Jawaban-jawaban lain ini agak menyesatkan. Saya setuju bahwa mereka menyatakan detail implementasi yang dapat menjelaskan perbedaan ini, tetapi mereka melebih-lebihkan kasus ini. Seperti yang disarankan dengan benar oleh jmite, mereka berorientasi pada implementasi menuju implementasi yang rusak dari pemanggilan fungsi / rekursi. Banyak bahasa menerapkan loop melalui rekursi, jadi loop jelas tidak akan lebih cepat dalam bahasa tersebut. Rekursi sama sekali tidak seefisien looping (ketika keduanya berlaku) secara teori. Izinkan saya mengutip abstrak dari makalah Guy Steele pada tahun 1977 yang Membongkar Mitos "Panggilan Prosedur Mahal" atau, Implementasi Prosedur yang Dianggap Berbahaya atau, Lambda: the Ultimate GOTO
Cerita rakyat menyatakan bahwa pernyataan GOTO "murah", sementara panggilan prosedur "mahal". Mitos ini sebagian besar merupakan hasil dari implementasi bahasa yang dirancang dengan buruk. Pertumbuhan historis mitos ini dipertimbangkan. Baik ide-ide teoretis dan implementasi yang ada dibahas yang menyanggah mitos ini. Terlihat bahwa penggunaan pemanggilan prosedur yang tidak dibatasi memungkinkan kebebasan gaya yang hebat. Khususnya, diagram alur apa pun dapat ditulis sebagai program "terstruktur" tanpa memperkenalkan variabel tambahan. Kesulitan dengan pernyataan GOTO dan panggilan prosedur ditandai sebagai konflik antara konsep pemrograman abstrak dan konstruksi bahasa yang konkret.
"Konflik antara konsep pemrograman abstrak dan konstruksi bahasa konkret" dapat dilihat dari fakta bahwa sebagian besar model teoretis, misalnya, kalkulus lambda yang tidak diketik , tidak memiliki tumpukan . Tentu saja, konflik ini tidak perlu seperti yang digambarkan oleh makalah di atas dan juga ditunjukkan oleh bahasa yang tidak memiliki mekanisme iterasi selain rekursi seperti Haskell.
fix
fix f x = f (fix f) x
( λ x . M) N⇝ M.[ N/ x][ N/ x]xM.N⇝
Sekarang sebagai contoh. Tentukan fact
sebagai
fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1
Inilah evaluasi fact 3
, di mana, untuk kekompakan, saya akan gunakan g
sebagai sinonim untuk fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1))
, yaitu fact = g 1
. Ini tidak mempengaruhi argumen saya.
fact 3
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6
Anda dapat melihat dari bentuknya tanpa melihat detail bahwa tidak ada pertumbuhan dan setiap iterasi membutuhkan jumlah ruang yang sama. (Secara teknis, hasil numerik tumbuh yang tidak dapat dihindari dan sama benarnya untuk sebuah while
loop.) Saya menentang Anda untuk menunjukkan "tumpukan" yang tumbuh tanpa batas di sini.
Tampaknya semantik arketipe dari kalkulus lambda sudah melakukan apa yang umumnya disebut "optimisasi panggilan ekor". Tentu saja, tidak ada "optimasi" yang terjadi di sini. Tidak ada aturan khusus di sini untuk panggilan "ekor" yang bertentangan dengan panggilan "normal". Untuk alasan ini, sulit untuk memberikan karakterisasi "abstrak" tentang apa yang dilakukan "optimisasi" panggilan ekor, seperti dalam banyak penokohan abstrak semantik fungsi panggil, tidak ada yang dapat dilakukan "optimasi" panggilan ekor!
Bahwa definisi analog fact
dalam banyak bahasa "stack overflows", adalah kegagalan oleh bahasa-bahasa tersebut untuk mengimplementasikan semantik panggilan fungsi dengan benar. (Beberapa bahasa memiliki alasan.) Situasi ini kira-kira analog dengan penerapan bahasa yang mengimplementasikan array dengan daftar tertaut. Pengindeksan ke dalam "array" demikian akan menjadi operasi O (n) yang tidak memenuhi harapan array. Jika saya membuat implementasi terpisah dari bahasa, yang menggunakan array nyata dan bukan daftar yang ditautkan, Anda tidak akan mengatakan saya telah mengimplementasikan "optimasi akses array", Anda akan mengatakan saya memperbaiki implementasi array yang rusak.
Jadi, menanggapi jawaban Veedrac. Tumpukan tidak "mendasar" untuk rekursi . Sejauh perilaku "tumpukan-seperti" terjadi selama evaluasi, ini hanya dapat terjadi dalam kasus di mana loop (tanpa struktur data tambahan) tidak akan berlaku di tempat pertama! Dengan kata lain, saya dapat menerapkan loop dengan rekursi dengan karakteristik kinerja yang persis sama. Memang, Skema dan SML keduanya mengandung konstruksi perulangan, tetapi keduanya mendefinisikan mereka dalam hal rekursi (dan, setidaknya dalam Skema, do
sering kali diimplementasikan sebagai makro yang berkembang menjadi panggilan rekursif.) Demikian pula, untuk jawaban Johan, tidak ada yang mengatakan kompiler harus memancarkan perakitan yang dijelaskan Johan untuk rekursi. Memang,dengan rakitan yang sama persis apakah Anda menggunakan loop atau rekursi. Satu-satunya waktu kompiler adalah ( agak) wajibuntuk memancarkan perakitan seperti yang dijelaskan Johan adalah ketika Anda melakukan sesuatu yang tidak dapat diungkapkan oleh loop. Sebagaimana diuraikan dalam makalah Steele dan didemonstrasikan oleh praktik nyata bahasa seperti Haskell, Skema, dan SML, bukan "sangat jarang" bahwa panggilan ekor dapat "dioptimalkan", mereka selalu dapat "dioptimalkan". Apakah penggunaan rekursi tertentu akan berjalan dalam ruang konstan tergantung pada bagaimana hal itu ditulis, tetapi batasan yang perlu Anda terapkan untuk memungkinkan itu adalah batasan yang Anda perlukan untuk menyesuaikan masalah Anda ke dalam bentuk lingkaran. (Sebenarnya, mereka kurang ketat. Ada masalah, seperti mesin pengkodean negara, yang lebih bersih dan efisien ditangani melalui panggilan ekor sebagai lawan loop yang akan memerlukan variabel tambahan.) Sekali lagi, membutuhkan lebih banyak pekerjaan adalah ketika kode Anda bukan loop.
Dugaan saya adalah Johan mengacu pada kompiler C yang memiliki batasan sewenang-wenang ketika akan melakukan "optimasi" panggilan ekor. Johan juga mungkin merujuk ke bahasa seperti C ++ dan Rust ketika ia berbicara tentang "bahasa dengan tipe yang dikelola". The RAII idiom dari C ++ dan hadir di Rust juga membuat hal-hal yang dangkal terlihat seperti panggilan ekor, tidak panggilan ekor (karena "destructors" masih perlu dipanggil). Ada proposal untuk menggunakan sintaks yang berbeda untuk memilih ke semantik yang sedikit berbeda yang akan memungkinkan rekursi ekor (yaitu pemanggil panggilan sebelumpanggilan ekor terakhir dan jelas-jelas melarang untuk mengakses benda yang "dihancurkan"). (Pengumpulan sampah tidak memiliki masalah seperti itu, dan semua Haskell, SML, dan Skema adalah bahasa pengumpulan sampah.) Dalam nada yang sangat berbeda, beberapa bahasa, seperti Smalltalk, mengekspos "tumpukan" sebagai objek kelas satu, dalam hal ini huruf "tumpukan" tidak lagi detail implementasi, meskipun ini tidak menghalangi memiliki jenis panggilan terpisah dengan semantik yang berbeda. (Java mengatakan tidak bisa karena cara menangani beberapa aspek keamanan, tetapi ini sebenarnya salah .)
Dalam praktiknya, prevalensi implementasi fungsi yang rusak berasal dari tiga faktor utama. Pertama, banyak bahasa mewarisi implementasi yang rusak dari bahasa implementasi mereka (biasanya C). Kedua, manajemen sumber daya deterministik bagus dan membuat masalah menjadi lebih rumit, meskipun hanya sedikit bahasa yang menawarkan ini. Ketiga, dan, dalam pengalaman saya, alasan kebanyakan orang peduli, adalah bahwa mereka ingin tumpukan jejak ketika kesalahan terjadi untuk keperluan debugging. Hanya alasan kedua adalah alasan yang berpotensi termotivasi secara teoritis.