Đây có phải là một cách chung để chuyển đổi bất kỳ thủ tục đệ quy sang đệ quy đuôi?


13

Có vẻ như tôi đã tìm thấy một cách chung để chuyển đổi bất kỳ thủ tục đệ quy nào thành đệ quy đuôi:

  1. Xác định thủ tục phụ trợ giúp với tham số "kết quả" bổ sung.
  2. Áp dụng những gì sẽ được áp dụng cho giá trị trả về của thủ tục cho tham số đó.
  3. Gọi thủ tục trợ giúp này để bắt đầu. Giá trị ban đầu cho tham số "kết quả" là giá trị cho điểm thoát của quy trình đệ quy, do đó quá trình lặp lại kết quả bắt đầu từ nơi quá trình đệ quy bắt đầu thu hẹp.

Ví dụ, đây là quy trình đệ quy ban đầu được chuyển đổi ( bài tập 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))))

Dưới đây là quy trình đệ quy đuôi được chuyển đổi ( bài tập 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))

Ai đó có thể chứng minh hoặc bác bỏ điều này?


1
Ôi(đăng nhậpn)

Suy nghĩ thứ hai: Chọn btrở thành lũy thừa 2 cho thấy ban đầu đặt productthành 0 không hoàn toàn đúng; nhưng thay đổi nó thành 1 không hoạt động khi blà số lẻ. Có lẽ bạn cần 2 thông số tích lũy khác nhau?
j_random_hacker

3
Bạn chưa thực sự định nghĩa một phép biến đổi của một định nghĩa đệ quy không đuôi, thêm một số tham số kết quả và sử dụng nó để tích lũy là khá mơ hồ và hầu như không khái quát cho các trường hợp phức tạp hơn, ví dụ như các giao dịch cây, trong đó bạn có hai lệnh gọi đệ quy. Mặc dù vậy, một ý tưởng chính xác hơn về "tiếp tục" tồn tại, trong đó bạn thực hiện một phần công việc và sau đó cho phép chức năng "tiếp tục" tiếp nhận, nhận như một tham số công việc bạn đã làm cho đến nay. Nó được gọi là kiểu chuyển tiếp tiếp tục (cps), xem en.wikipedia.org/wiki/Contininating-passing_style .
Ariel

4
Các slide này fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdf chứa mô tả về chuyển đổi cps, trong đó bạn có một số biểu thức tùy ý (có thể với các định nghĩa hàm với các lệnh gọi không phải đuôi) và biến đổi nó thành một biểu thức tương đương chỉ với các lệnh gọi đuôi.
Ariel

@j_random_hacker Vâng, tôi có thể thấy rằng quy trình "đã chuyển đổi" của tôi thực tế là sai ...
nalzok

Câu trả lời:


12

Mô tả về thuật toán của bạn thực sự quá mơ hồ để đánh giá nó vào thời điểm này. Nhưng, đây là một số điều cần xem xét.

CPS

Trong thực tế, có một cách để chuyển đổi bất kỳnào thành một hình thức chỉ sử dụng các cuộc gọi đuôi. Đây là biến đổi CPS. CPS ( Kiểu tiếp tục truyền ) là một hình thức thể hiện mã bằng cách truyền cho mỗi chức năng một phần tiếp theo. Tiếp tục là một khái niệm trừu tượng đại diện cho "phần còn lại của một phép tính". Trong mã được biểu thị dưới dạng CPS, cách tự nhiên để xác định lại phần tiếp theo là một hàm chấp nhận một giá trị. Trong CPS, thay vì một hàm trả về một giá trị, thay vào đó, nó áp dụng hàm đại diện cho sự tiếp tục hiện tại với chức năng "được trả về" của hàm.

Ví dụ, hãy xem xét chức năng sau:

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

Điều này có thể được thể hiện trong CPS như sau:

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

Nó xấu, và thường chậm, nhưng nó có một số lợi thế nhất định:

  • Việc chuyển đổi có thể hoàn toàn tự động. Vì vậy, không cần phải viết (hoặc xem) mã ở dạng CPS.
  • Kết hợp với thunking và trampolining , nó có thể được sử dụng để cung cấp tối ưu hóa cuộc gọi đuôi trong các ngôn ngữ không cung cấp tối ưu hóa cuộc gọi đuôi. (Tối ưu hóa cuộc gọi đuôi của các hàm đệ quy trực tiếp có thể được thực hiện thông qua các phương tiện khác, chẳng hạn như chuyển đổi cuộc gọi đệ quy thành một vòng lặp. Nhưng đệ quy gián tiếp không phải là tầm thường để chuyển đổi theo cách này.)
  • Với CPS, các phần tiếp theo trở thành một đối tượng hạng nhất. Vì các phần tiếp theo là bản chất của điều khiển, điều này cho phép hầu như bất kỳ toán tử điều khiển nào được triển khai như một thư viện mà không cần bất kỳ sự hỗ trợ đặc biệt nào từ ngôn ngữ. Ví dụ, goto, ngoại lệ và phân luồng hợp tác đều có thể được mô hình hóa bằng cách sử dụng các phần tiếp theo.

TCO

Dường như với tôi rằng lý do duy nhất liên quan đến đệ quy đuôi (hay gọi chung là đuôi) là cho mục đích tối ưu hóa cuộc gọi đuôi (TCO). Vì vậy, tôi nghĩ rằng một câu hỏi tốt hơn để hỏi là "mã năng suất chuyển đổi của tôi có thể tối ưu hóa cuộc gọi đuôi không?".

Nếu chúng ta một lần nữa xem xét CPS, một trong những đặc điểm của nó là mã được thể hiện trong CPS chỉ bao gồm các lệnh gọi đuôi. Vì mọi thứ đều là cuộc gọi đuôi, chúng tôi không cần lưu điểm trả về ngăn xếp. Vì vậy, tất cả các mã ở dạng CPS phải được tối ưu hóa cuộc gọi, phải không?

Vâng, không hoàn toàn. Bạn thấy đấy, mặc dù có vẻ như chúng ta đã loại bỏ ngăn xếp, tất cả những gì chúng ta đã làm chỉ là thay đổi cách chúng ta đại diện cho nó. Ngăn xếp bây giờ là một phần của việc đóng cửa đại diện cho sự tiếp nối. Vì vậy, CPS không kỳ diệu làm cho tất cả các cuộc gọi đuôi của chúng tôi được tối ưu hóa.

Vì vậy, nếu CPS không thể tạo ra mọi thứ TCO, thì có một biến đổi cụ thể cho đệ quy trực tiếp có thể không? Không, không nói chung. Một số thu hồi là tuyến tính, nhưng một số thì không. Thu hồi phi tuyến tính (ví dụ, cây) đơn giản là phải duy trì một lượng trạng thái khác nhau ở đâu đó.


hơi khó hiểu khi trong phần phụ " TCO ", khi bạn nói "tối ưu hóa cuộc gọi đuôi", bạn thực sự có nghĩa là "với việc sử dụng bộ nhớ liên tục". Việc sử dụng bộ nhớ động không phải là hằng số vẫn không phủ nhận thực tế là các cuộc gọi thực sự là đuôi và không có sự tăng trưởng không giới hạn trong việc sử dụng ngăn xếp . SICP gọi các tính toán như vậy là "lặp đi lặp lại", vì vậy nói rằng "mặc dù đó là TCO, nhưng nó vẫn không làm cho nó lặp đi lặp lại" có thể là một từ ngữ tốt hơn (đối với tôi).
Will Ness

@WillNess Chúng tôi vẫn có một ngăn xếp cuộc gọi, nó chỉ được thể hiện khác nhau. Cấu trúc không thay đổi chỉ vì chúng ta đang sử dụng heap, thay vì ngăn xếp phần cứng . Rốt cuộc, có rất nhiều cấu trúc dữ liệu dựa trên bộ nhớ heap động có "stack" trong tên của chúng.
Nathan Davis

điểm duy nhất ở đây là một số ngôn ngữ có giới hạn cứng khi sử dụng ngăn xếp cuộc gọi.
Will Ness
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.