Apakah ini cara umum untuk mengubah prosedur rekursif menjadi rekursi ekor?

13

Tampaknya saya telah menemukan cara umum untuk mengubah prosedur rekursif apa pun menjadi rekursi ekor:

  1. Tentukan sub-prosedur pembantu dengan parameter "hasil" tambahan.
  2. Terapkan apa yang akan diterapkan pada nilai pengembalian prosedur ke parameter itu.
  3. Hubungi prosedur bantuan ini untuk memulai. Nilai awal untuk parameter "hasil" adalah nilai untuk titik keluar dari proses rekursif, sehingga proses iteratif yang dihasilkan dimulai dari tempat proses rekursif mulai menyusut.

Sebagai contoh, berikut adalah prosedur rekursif asli yang akan dikonversi ( latihan SICP 1.17 ):

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

Berikut adalah prosedur konversi ekor-rekursif ( latihan SICP 1.18 ):

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

Adakah yang bisa membuktikan atau menyangkal ini?

nalzok
sumber
1
Pikiran pertama: Ini mungkin bekerja untuk semua fungsi rekursif tunggal , tapi saya akan terkejut jika itu berfungsi untuk fungsi yang membuat beberapa panggilan rekursif, karena itu akan menyiratkan, misalnya, bahwa Anda dapat mengimplementasikan quicksort tanpa perlu stack ruang. (Implementasi quicksort efisien yang ada umumnya membuat 1 panggilan rekursif pada stack, dan mengubah panggilan rekursif lainnya menjadi tail-call yang dapat (secara manual atau otomatis) diubah menjadi sebuah loop.)HAI(catatann)
j_random_hacker
Pikiran kedua: Memilih bmenjadi kekuatan 2 menunjukkan bahwa awalnya menetapkan productke 0 tidak cukup benar; tetapi mengubahnya menjadi 1 tidak berfungsi ketika baneh. Mungkin Anda membutuhkan 2 parameter akumulator yang berbeda?
j_random_hacker
3
Anda belum benar-benar mendefinisikan transformasi definisi rekursif non-ekor, menambahkan beberapa parameter hasil dan menggunakannya untuk akumulasi cukup samar, dan sulit digeneralisasikan ke kasus yang lebih kompleks, misalnya lintas pohon, di mana Anda memiliki dua panggilan rekursif. Meskipun demikian, ada gagasan yang lebih tepat tentang "kelanjutan", di mana Anda melakukan bagian dari pekerjaan, dan kemudian mengizinkan fungsi "kelanjutan" untuk mengambil alih, menerima sebagai parameter pekerjaan yang telah Anda lakukan sejauh ini. Ini disebut gaya kelanjutan lewat (cps), lihat en.wikipedia.org/wiki/Continuation-passing_style .
Ariel
4
Slide ini fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdf berisi deskripsi transformasi cps, di mana Anda mengambil ekspresi sewenang-wenang (mungkin dengan definisi fungsi dengan panggilan non tail) dan mengubahnya menjadi ekspresi yang setara dengan hanya panggilan ekor.
Ariel
@ j_random_hacker Ya, saya dapat melihat bahwa prosedur "dikonversi" saya ternyata salah ...
nalzok

Jawaban:

12

Deskripsi algoritma Anda terlalu samar untuk dievaluasi saat ini. Tapi, berikut beberapa hal yang perlu dipertimbangkan.

CPS

Bahkan, ada cara untuk mengubah kode apa pun menjadi bentuk yang hanya menggunakan panggilan ekor. Ini adalah transformasi CPS. CPS ( Continuation-Passing Style ) adalah bentuk kode pengekspresian dengan meneruskan setiap fungsi sebagai kelanjutan. Kelanjutan adalah gagasan abstrak yang mewakili "sisa dari suatu kompuasi". Dalam kode yang dinyatakan dalam bentuk CPS, cara alami untuk memverifikasi kelanjutan adalah sebagai fungsi yang menerima nilai. Dalam CPS, alih-alih fungsi yang mengembalikan nilai, melainkan menerapkan fungsi yang mewakili kelanjutan saat ini untuk "dikembalikan" oleh fungsi.

Misalnya, pertimbangkan fungsi berikut:

(lambda (a b c d)
  (+ (- a b) (* c d)))

Ini dapat diungkapkan dalam CPS sebagai berikut:

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

Itu jelek, dan sering lambat, tetapi memang memiliki keuntungan tertentu:

  • Transformasi dapat sepenuhnya otomatis. Jadi tidak perlu menulis (atau melihat) kode dalam bentuk CPS.
  • Dikombinasikan dengan thunking dan trampolining , ini dapat digunakan untuk memberikan optimisasi panggilan-tail dalam bahasa yang tidak memberikan optimisasi panggilan-panggilan. (Optimalisasi Tail-call dari fungsi tail-recursive langsung dapat dicapai melalui cara lain, seperti mengubah panggilan rekursif menjadi sebuah loop. Tetapi rekursi tidak langsung tidak sepele untuk dikonversi dengan cara ini.)
  • Dengan CPS, kelanjutan menjadi objek kelas satu. Karena kelanjutan adalah inti dari kontrol, ini memungkinkan setiap operator kontrol untuk diimplementasikan sebagai perpustakaan tanpa memerlukan dukungan khusus dari bahasa. Misalnya, goto, pengecualian, dan threading kooperatif semua dapat dimodelkan menggunakan kelanjutan.

TCO

Tampak bagi saya bahwa satu-satunya alasan untuk khawatir dengan ekor-rekursi (atau ekor-panggilan pada umumnya) adalah untuk keperluan optimasi ekor-panggilan (TCO). Jadi saya pikir pertanyaan yang lebih baik untuk ditanyakan adalah "apakah kode hasil transformasi saya yang ekor-panggilan dioptimalkan?".

Jika kita sekali lagi mempertimbangkan CPS, salah satu karakteristiknya adalah bahwa kode yang diekspresikan dalam CPS hanya terdiri dari ekor-panggilan. Karena semuanya adalah panggilan ekor, kita tidak perlu menyimpan titik kembali ke stack. Jadi semua kode dalam bentuk CPS harus dioptimalkan untuk panggilan ekor, bukan?

Ya tidak cukup. Anda tahu, sementara itu mungkin tampak bahwa kita telah menghilangkan tumpukan, semua yang telah kita lakukan hanyalah mengubah cara kita mewakilinya. Tumpukan sekarang merupakan bagian dari penutupan yang mewakili kelanjutan. Jadi CPS tidak secara ajaib membuat semua kode panggilan ekor kami dioptimalkan.

Jadi jika CPS tidak bisa membuat semuanya TCO, apakah ada transformasi khusus untuk rekursi langsung yang bisa? Tidak, tidak secara umum. Beberapa rekursi adalah linier, tetapi beberapa tidak. Rekursi non-linear (misalnya, pohon) hanya harus mempertahankan jumlah variabel dari suatu tempat.

Nathan Davis
sumber
ini agak membingungkan ketika di sub-bagian " TCO ", ketika Anda mengatakan "tail-call dioptimalkan" yang Anda maksud sebenarnya "dengan penggunaan memori yang konstan". Bahwa penggunaan memori dinamis tidak konstan masih tidak meniadakan fakta bahwa panggilan memang ekor dan tidak ada pertumbuhan tak terbatas dalam penggunaan tumpukan . SICP menyebut perhitungan seperti itu "berulang", sehingga mengatakan "meskipun TCO, itu masih tidak membuatnya berulang" mungkin kata-kata yang lebih baik (bagi saya).
Will Ness
@ WillNess Kami masih memiliki tumpukan panggilan, itu hanya diwakili berbeda. Struktur tidak berubah hanya karena kami menggunakan heap, bukan tumpukan perangkat keras . Lagi pula, ada banyak struktur data berdasarkan memori tumpukan dinamis yang memiliki "tumpukan" dalam namanya.
Nathan Davis
satu-satunya titik di sini adalah bahwa beberapa bahasa memiliki batas bawaan untuk menggunakan tumpukan panggilan.
Will Ness