Tại sao Trampolines hoạt động?


104

Tôi đã làm một số JavaScript chức năng. Tôi đã nghĩ rằng Tối ưu hóa cuộc gọi đuôi đã được thực hiện, nhưng hóa ra tôi đã sai. Vì vậy, tôi đã phải tự dạy mình về Trampolining . Sau khi đọc một chút ở đây và những nơi khác, tôi đã có thể có được những điều cơ bản và xây dựng tấm bạt lò xo đầu tiên của mình:

/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/

function loopy(x){
    if (x<10000000){ 
        return function(){
            return loopy(x+1)
        }
    }else{
        return x;
    }
};

function trampoline(foo){
    while(foo && typeof foo === 'function'){
        foo = foo();
    }
    return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};

alert(trampoline(loopy(0)));

Vấn đề lớn nhất của tôi là tôi không biết tại sao nó lại hoạt động. Tôi có ý tưởng chạy lại hàm trong một vòng lặp while thay vì sử dụng vòng lặp đệ quy. Ngoại trừ, về mặt kỹ thuật, chức năng cơ sở của tôi đã có một vòng lặp đệ quy. Tôi không chạy loopychức năng cơ bản , nhưng tôi đang chạy chức năng bên trong nó. Điều gì ngăn chặn foo = foo()gây ra một tràn ngăn xếp? Và không phải là foo = foo()đột biến về mặt kỹ thuật, hay tôi đang thiếu một cái gì đó? Có lẽ nó chỉ là một điều ác cần thiết. Hoặc một số cú pháp tôi đang thiếu.

Thậm chí có một cách để hiểu nó? Hoặc nó chỉ là một số hack mà bằng cách nào đó hoạt động? Tôi đã có thể vượt qua mọi thứ khác, nhưng điều này khiến tôi hoang mang.


5
Có, nhưng đó vẫn là đệ quy. loopykhông tràn vì nó không tự gọi .
tkausl

4
"Tôi đã nghĩ rằng TCO đã được thực hiện, nhưng hóa ra tôi đã sai." Nó đã được ít nhất là trong V8 trong hầu hết các cảnh. Chẳng hạn, bạn có thể sử dụng nó trong bất kỳ phiên bản Node nào gần đây bằng cách yêu cầu Node kích hoạt nó trong V8: stackoverflow.com/a/30369729/157247 Chrome đã có nó (đằng sau một lá cờ "thử nghiệm") kể từ Chrome 51.
TJ Crowder

125
Động năng từ người sử dụng được chuyển thành năng lượng đàn hồi khi trampoline chùng xuống, sau đó trở lại động năng khi nó bật lại.
Immibis

66
@immibis, Thay mặt cho tất cả những người đã đến đây mà không kiểm tra trang web Stack Exchange này là gì, cảm ơn bạn.
dùng1717828

4
@jpaugh bạn có nghĩa là "nhảy"? ;-)
Hulk

Câu trả lời:


89

Lý do bộ não của bạn nổi loạn chống lại chức năng loopy()là vì nó thuộc loại không nhất quán :

function loopy(x){
    if (x<10000000){ 
        return function(){ // On this line it returns a function...
            // (This is not part of loopy(), this is the function we are returning.)
            return loopy(x+1)
        }
    }else{
        return x; // ...but on this line it returns an integer!
    }
};

Khá nhiều ngôn ngữ thậm chí không cho phép bạn làm những việc như thế này, hoặc ít nhất yêu cầu gõ nhiều hơn để giải thích làm thế nào điều này được cho là có ý nghĩa. Bởi vì nó thực sự không. Hàm và số nguyên là các loại đối tượng hoàn toàn khác nhau.

Vì vậy, hãy đi qua vòng lặp while, cẩn thận:

while(foo && typeof foo === 'function'){
    foo = foo();
}

Ban đầu, foobằng loopy(0). Là loopy(0)gì Vâng, nó ít hơn 10000000, vì vậy chúng tôi nhận được function(){return loopy(1)}. Đó là một giá trị trung thực và đó là một chức năng, vì vậy vòng lặp tiếp tục.

Bây giờ chúng tôi đến foo = foo(). foo()cũng giống như loopy(1). Vì 1 vẫn còn ít hơn 10000000, nên trả về function(){return loopy(2)}, sau đó chúng ta gán cho foo.

foovẫn là một chức năng, vì vậy chúng tôi tiếp tục ... cho đến khi cuối cùng foo bằng function(){return loopy(10000000)}. Đó là một chức năng, vì vậy chúng tôi thực hiện foo = foo()thêm một lần nữa, nhưng lần này, khi chúng tôi gọi loopy(10000000), x không dưới 10000000 vì vậy chúng tôi chỉ nhận lại x. Vì 10000000 cũng không phải là một hàm, điều này cũng kết thúc vòng lặp while.


1
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
yannis

Nó thực sự chỉ là một loại tổng. Đôi khi được gọi là một biến thể. Các ngôn ngữ động hỗ trợ chúng khá dễ dàng vì mọi giá trị được gắn thẻ, trong khi các ngôn ngữ được nhập tĩnh hơn sẽ yêu cầu bạn chỉ định hàm trả về một biến thể. Trampolines có thể dễ dàng có trong C ++ hoặc Haskell, ví dụ.
GManNickG

2
@GManNickG: Vâng, đó là những gì tôi muốn nói là "gõ nhiều hơn nữa." Trong C, bạn sẽ phải khai báo liên kết, khai báo một cấu trúc gắn thẻ liên kết, đóng gói và giải nén cấu trúc ở hai đầu, đóng gói và giải nén liên kết ở hai đầu và (có thể) tìm ra ai sở hữu bộ nhớ mà cấu trúc đó sinh sống . C ++ rất ít mã hơn thế, nhưng về mặt khái niệm thì nó không phức tạp hơn C và nó vẫn dài dòng hơn Javascript của OP.
Kevin

Chắc chắn, tôi không tranh luận về điều đó, tôi chỉ nghĩ rằng sự nhấn mạnh mà bạn dành cho nó là kỳ lạ hoặc không có ý nghĩa là một chút mạnh mẽ. :)
GManNickG

173

Kevin ngắn gọn chỉ ra cách đoạn mã đặc biệt này hoạt động (cùng với lý do tại sao nó khá khó hiểu), nhưng tôi muốn thêm một số thông tin về trampolines cách nói chung công việc.

Không có tối ưu hóa cuộc gọi đuôi (TCO), mỗi lệnh gọi hàm sẽ thêm khung ngăn xếp vào ngăn xếp thực thi hiện tại. Giả sử chúng ta có chức năng in ra đếm ngược các số:

function countdown(n) {
  if (n === 0) {
    console.log("Blastoff!");
  } else {
    console.log("Launch in " + n);
    countdown(n - 1);
  }
}

Nếu chúng ta gọi countdown(3), hãy phân tích xem ngăn xếp cuộc gọi sẽ trông như thế nào nếu không có TCO.

> countdown(3);
// stack: countdown(3)
Launch in 3
// stack: countdown(3), countdown(2)
Launch in 2
// stack: countdown(3), countdown(2), countdown(1)
Launch in 1
// stack: countdown(3), countdown(2), countdown(1), countdown(0)
Blastoff!
// returns, stack: countdown(3), countdown(2), countdown(1)
// returns, stack: countdown(3), countdown(2)
// returns, stack: countdown(3)
// returns, stack is empty

Với TCO, mỗi lệnh gọi đệ quy countdownvị trí đuôi (không còn gì để làm ngoài việc trả về kết quả của cuộc gọi) để không có khung ngăn xếp nào được phân bổ. Không có TCO, ngăn xếp thổi lên thậm chí hơi lớn n.

Trampolining vượt qua giới hạn này bằng cách chèn một trình bao bọc xung quanh countdownchức năng. Sau đó, countdownkhông thực hiện các cuộc gọi đệ quy và thay vào đó ngay lập tức trả về một hàm để gọi. Đây là một ví dụ thực hiện:

function trampoline(firstHop) {
  nextHop = firstHop();
  while (nextHop) {
    nextHop = nextHop()
  }
}

function countdown(n) {
  trampoline(() => countdownHop(n));
}

function countdownHop(n) {
  if (n === 0) {
    console.log("Blastoff!");
  } else {
    console.log("Launch in " + n);
    return () => countdownHop(n-1);
  }
}

Để hiểu rõ hơn về cách thức hoạt động của tính năng này, hãy xem ngăn xếp cuộc gọi:

> countdown(3);
// stack: countdown(3)
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(3)
Launch in 3
// return next hop from countdownHop(3)
// stack: countdown(3), trampoline
// trampoline sees hop returned another hop function, calls it
// stack: countdown(3), trampoline, countdownHop(2)
Launch in 2
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(1)
Launch in 1
// stack: countdown(3), trampoline
// stack: countdown(3), trampoline, countdownHop(0)
Blastoff!
// stack: countdown(3), trampoline
// stack: countdown(3)
// stack is empty

Tại mỗi bước countdownHopchức năng từ bỏ quyền kiểm soát trực tiếp của những gì xảy ra tiếp theo, thay vì trở về một hàm để gọi đó là mô tả những gì nó sẽ giống như để xảy ra tiếp theo. Hàm trampoline sau đó lấy cái này và gọi nó, sau đó gọi bất kỳ hàm nào trả về, và cứ thế cho đến khi không có "bước tiếp theo". Điều này được gọi là trampolining vì luồng điều khiển "nảy" giữa mỗi lệnh gọi đệ quy và triển khai trampoline, thay vì chức năng trực tiếp lặp lại. Bằng cách từ bỏ quyền kiểm soát ai thực hiện cuộc gọi đệ quy, chức năng của tấm bạt lò xo có thể đảm bảo ngăn xếp không quá lớn. Lưu ý bên lề: việc thực hiện này trampolinebỏ qua các giá trị trả về cho đơn giản.

Nó có thể là khó khăn để biết liệu đây là một ý tưởng tốt. Hiệu suất có thể bị ảnh hưởng do mỗi bước phân bổ một đóng cửa mới. Tối ưu hóa thông minh có thể làm cho điều này khả thi, nhưng bạn không bao giờ biết. Trampolining chủ yếu hữu ích để vượt qua các giới hạn đệ quy cứng, ví dụ khi triển khai ngôn ngữ đặt kích thước ngăn xếp cuộc gọi tối đa.


18

Có lẽ sẽ dễ hiểu hơn nếu tấm bạt lò xo được thực hiện với loại trả về chuyên dụng (thay vì lạm dụng chức năng):

class Result {}
// poor man's case classes
class Recurse extends Result {
    constructor(a) { this.arg = a; }
}
class Return extends Result {
    constructor(v) { this.value = v; }
}

function loopy(x) {
    if (x<10000000)
        return new Recurse(x+1);
    else
        return new Return(x);
}

function trampoline(fn, x) {
    while (true) {
        const res = fn(x);
        if (res instanceof Recurse)
            x = res.arg;
        else if (res instanceof Return)
            return res.value;
    }
}

alert(trampoline(loopy, 0));

Tương phản điều này với phiên bản của bạn trampoline, trong đó trường hợp đệ quy là khi hàm trả về một hàm khác và trường hợp cơ sở là khi nó trả về một thứ khác.

Điều gì ngăn chặn foo = foo()gây ra một tràn ngăn xếp?

Nó không gọi chính nó nữa. Thay vào đó, nó trả về một kết quả (theo cách thực hiện của tôi, theo nghĩa đen là a Result) cho biết liệu có nên tiếp tục đệ quy hay có thoát ra hay không.

Và không phải là foo = foo()đột biến về mặt kỹ thuật, hay tôi đang thiếu một cái gì đó? Có lẽ nó chỉ là một điều ác cần thiết.

Vâng, đây chính xác là cái ác cần thiết của vòng lặp. Người ta có thể viết trampolinemà không bị đột biến, nhưng nó sẽ yêu cầu đệ quy lại:

function trampoline(fn, x) {
    const res = fn(x);
    if (res instanceof Recurse)
        return trampoline(fn, res.arg);
    else if (res instanceof Return)
        return res.value;
}

Tuy nhiên, nó cho thấy ý tưởng về chức năng của tấm bạt lò xo thậm chí còn tốt hơn.

Điểm của trampoling là trừu tượng hóa cuộc gọi đệ quy đuôi từ hàm muốn sử dụng đệ quy thành giá trị trả về và thực hiện đệ quy thực tế ở một nơi duy nhất - trampolinechức năng, sau đó có thể được tối ưu hóa ở một nơi duy nhất để sử dụng một vòng.


foo = foo()là đột biến theo nghĩa sửa đổi trạng thái cục bộ, nhưng tôi thường xem xét việc gán lại vì bạn không thực sự sửa đổi đối tượng hàm bên dưới, bạn đang thay thế nó bằng hàm (hoặc giá trị) mà nó trả về.
JAB

@JAB Có, tôi không có ý ám foochỉ việc thay đổi giá trị có chứa, chỉ có biến được sửa đổi. Một whilevòng lặp yêu cầu một số trạng thái có thể thay đổi nếu bạn muốn nó chấm dứt, trong trường hợp này là biến foohoặc x.
Bergi

Tôi đã làm một cái gì đó như thế này một lúc trước trong câu trả lời này cho câu hỏi Stack Overflow về tối ưu hóa cuộc gọi đuôi, trampolines, v.v.
Joshua Taylor

2
Phiên bản của bạn không có đột biến đã chuyển đổi một cuộc gọi đệ quy fnthành cuộc gọi đệ quy thành trampoline- Tôi không chắc đó là một cải tiến.
Michael Anderson

1
@MichaelAnderson Nó chỉ có nghĩa là để chứng minh sự trừu tượng. Tất nhiên một tấm bạt lò xo đệ quy không hữu ích.
Bergi
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.