ทำไม Trampolines ถึงทำงาน


104

ฉันใช้จาวาสคริปต์ที่ใช้งานได้บางอย่าง ฉันคิดว่าTail-Call Optimisationได้รับการใช้งานแล้ว แต่พอกลับกลายเป็นว่าฉันผิด ดังนั้นฉันเคยสอนตัวเองtrampolining หลังจากอ่านหนังสือเล็กน้อยที่นี่และที่อื่น ๆ ฉันก็สามารถทำให้พื้นฐานและสร้างแทรมโพลีนตัวแรกได้:

/*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)));

ปัญหาที่ใหญ่ที่สุดของฉันคือฉันไม่รู้ว่าทำไมงานนี้ ฉันได้รับความคิดของการทำซ้ำฟังก์ชันในขณะที่ลูปแทนที่จะใช้ลูปแบบวนซ้ำ ยกเว้นในทางเทคนิคฟังก์ชั่นฐานของฉันมีวงวนซ้ำอยู่แล้ว ฉันไม่ได้เรียกใช้loopyฟังก์ชันพื้นฐานแต่ฉันกำลังเรียกใช้ฟังก์ชันภายในอยู่ อะไรคือfoo = foo()สาเหตุที่ทำให้กองซ้อนล้น และfoo = foo()เทคนิคก็ไม่ได้กลายพันธุ์หรือว่าฉันทำอะไรหายไป? บางทีมันอาจเป็นเพียงความชั่วร้ายที่จำเป็น หรือไวยากรณ์บางอย่างฉันขาดหายไป

มีวิธีแม้แต่ที่จะเข้าใจหรือไม่? หรือว่าเป็นแค่แฮ็คบางอันที่ใช้งานได้จริง? ฉันสามารถทำให้ทุกอย่างผ่านไปได้ แต่สิ่งนี้ทำให้ฉันงงงวย


5
ใช่ แต่นั่นยังคงเรียกซ้ำ loopyไม่ล้นเพราะมันไม่ได้เรียกตัวเองว่า
tkausl

4
"ฉันคิดว่า TCO ได้รับการติดตั้งแล้ว แต่พอปรากฎว่าฉันผิด" อย่างน้อยก็ใน V8 ในสถานการณ์ส่วนใหญ่ คุณสามารถใช้มันได้ใน Node เวอร์ชันล่าสุดโดยบอก Node ให้เปิดใช้งานใน V8: stackoverflow.com/a/30369729/157247 Chrome มี Chrome อยู่ (อยู่หลังสถานะ "ทดลอง") ตั้งแต่ Chrome 51
TJ Crowder

125
พลังงานจลน์จากผู้ใช้ถูกเปลี่ยนเป็นพลังงานที่มีความยืดหยุ่นเช่นเดียวกับแทรมโพลีนแทกเตอร์จากนั้นกลับไปเป็นพลังงานจลน์ขณะที่เด้งกลับมา
immibis

66
@immibis ในนามของทุกคนที่มาที่นี่โดยไม่ตรวจสอบว่าเว็บไซต์นี้เป็น Stack Exchange แบบใดขอขอบคุณ
user1717828

4
@jpaugh คุณหมายถึง "hopping" หรือไม่? ;-)
Hulk

คำตอบ:


89

สาเหตุที่สมองของคุณต่อต้านการทำงานloopy()นั่นคือมันเป็นประเภทที่ไม่สอดคล้องกัน :

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!
    }
};

หลายภาษามีหลายภาษาที่ไม่ยอมให้คุณทำสิ่งนี้หรืออย่างน้อยก็ต้องการการพิมพ์มากกว่านี้เพื่ออธิบายว่ามันควรจะมีเหตุผลแบบใด เพราะมันไม่จริง ฟังก์ชั่นและจำนวนเต็มเป็นวัตถุประเภทต่าง ๆ โดยสิ้นเชิง

งั้นลองดูในขณะที่วนรอบอย่างระมัดระวัง:

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

ในขั้นต้นเท่ากับfoo loopy(0)คือloopy(0)อะไร ดีก็น้อยกว่า 10000000 function(){return loopy(1)}เพื่อให้เราได้รับ นั่นคือคุณค่าที่แท้จริงและเป็นฟังก์ชั่นดังนั้นลูปก็จะดำเนินต่อไป

foo = foo()ตอนนี้เรามา เป็นเช่นเดียวกับfoo() loopy(1)ตั้งแต่วันที่ 1 ยังคงน้อยกว่า 10000000, ที่ส่งกลับซึ่งเรากำหนดให้แล้วfunction(){return loopy(2)}foo

fooก็ยังคงเป็นฟังก์ชั่นเพื่อให้เราเก็บไป ... จนกระทั่ง foo function(){return loopy(10000000)}ในที่สุดก็จะมีค่าเท่ากับ นั่นคือฟังก์ชั่นเราจึงทำfoo = foo()อีกครั้ง แต่คราวนี้เมื่อเราโทรloopy(10000000)x ไม่น้อยกว่า 10,000,000 ดังนั้นเราจึงได้ x กลับมา เนื่องจาก 10,000,000 ยังไม่ใช่ฟังก์ชั่นสิ่งนี้จึงสิ้นสุดในขณะที่ลูปเช่นกัน


1
ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
yannis

มันเป็นแค่ผลรวม บางครั้งเรียกว่าตัวแปร ภาษาไดนามิกช่วยสนับสนุนพวกเขาได้ง่ายเพราะทุกค่าถูกติดแท็กในขณะที่ภาษาที่พิมพ์แบบคงที่จะต้องให้คุณระบุฟังก์ชั่นส่งกลับตัวแปร Trampolines สามารถทำได้อย่างง่ายดายใน C ++ หรือ Haskell
GManNickG

2
@GManNickG: ใช่นั่นคือสิ่งที่ฉันหมายถึงโดย "พิมพ์มากขึ้น" ใน C คุณจะต้องประกาศยูเนี่ยนประกาศโครงสร้างที่แท็กสหภาพแพ็คและแกะโครงสร้างที่ปลายทั้งสองแพ็คและแกะสหภาพที่ปลายด้านใดด้านหนึ่งและ (อาจจะ) คิดออกว่าใครเป็นเจ้าของหน่วยความจำที่อาศัยโครงสร้าง . C ++ น่าจะเป็นโค้ดที่น้อยกว่านั้นมาก แต่มันก็ไม่ได้มีความซับซ้อนน้อยกว่า C และมันก็ยังละเอียดกว่าจาวาสคริปต์ของ OP
เควิน

แน่นอนว่าฉันไม่ได้แข่งขันว่าฉันแค่คิดว่าคุณให้ความสำคัญกับความแปลกหรือไม่สมเหตุสมผล :)
GManNickG

173

Kevin ชี้ให้เห็นอย่างชัดเจนว่าข้อมูลโค้ดนี้ทำงานอย่างไร (รวมถึงสาเหตุที่เข้าใจไม่ได้) แต่ฉันต้องการเพิ่มข้อมูลบางอย่างเกี่ยวกับวิธีการใช้แทรมโพลีนในงานทั่วไป

หากไม่มีการเพิ่มประสิทธิภาพ tail-call (TCO) การเรียกใช้ฟังก์ชันทุกครั้งจะเพิ่มเฟรมสแต็กไปยังสแต็กการเรียกใช้ปัจจุบัน สมมติว่าเรามีฟังก์ชั่นในการพิมพ์ตัวเลขนับถอยหลัง:

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

ถ้าเราโทรcountdown(3)มาลองวิเคราะห์ว่า call stack จะมีลักษณะอย่างไรโดยไม่มี 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

ด้วย TCO การเรียกแบบเรียกซ้ำแต่ละครั้งจะcountdownอยู่ในตำแหน่งท้าย (ไม่มีอะไรเหลือให้ทำนอกจากส่งคืนผลลัพธ์ของการโทร) ดังนั้นจึงไม่มีการจัดสรรเฟรมสแต็ก โดยไม่ต้อง TCO nกองพัดขึ้นแม้มีขนาดใหญ่ขึ้นเล็กน้อย

Trampolining รับข้อ จำกัด นี้โดยการใส่เสื้อคลุมรอบcountdownฟังก์ชั่น จากนั้นcountdownจะไม่ทำการโทรซ้ำและส่งคืนฟังก์ชันที่จะเรียกแทนทันที นี่คือตัวอย่างการใช้งาน:

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);
  }
}

เพื่อให้เข้าใจวิธีการทำงานได้ดีขึ้นลองดูที่ call stack:

> 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

ในแต่ละขั้นตอนcountdownHopฟังก์ชั่นจะยกเลิกการควบคุมโดยตรงว่าจะเกิดอะไรขึ้นต่อไปแทนที่จะส่งคืนฟังก์ชันที่จะเรียกที่อธิบายถึงสิ่งที่มันต้องการจะเกิดขึ้นต่อไป ฟังก์ชั่นแทรมโพลีนจะรับสิ่งนี้และเรียกมันจากนั้นเรียกฟังก์ชั่นอะไรก็ตามที่ส่งคืนและต่อไปเรื่อย ๆ จนกว่าจะไม่มี "ขั้นตอนต่อไป" สิ่งนี้เรียกว่า trampolining เนื่องจากการไหลของการควบคุม "ตีกลับ" ระหว่างการเรียกซ้ำและการใช้แทรมโพลีนแทนการทำซ้ำการทำงานโดยตรง โดยละทิ้งการควบคุมมากกว่าการที่จะทำให้การเรียก recursive ฟังก์ชัน trampoline สามารถมั่นใจได้สแต็คไม่ได้มีขนาดใหญ่เกินไป หมายเหตุด้านข้าง: การนำไปใช้ของtrampolineละเว้นการส่งคืนค่าเพื่อความเรียบง่าย

มันอาจเป็นเรื่องยุ่งยากที่จะรู้ว่านี่เป็นความคิดที่ดีหรือไม่ ประสิทธิภาพอาจประสบเนื่องจากแต่ละขั้นตอนจัดสรรการปิดใหม่ การเพิ่มประสิทธิภาพที่ชาญฉลาดสามารถทำให้สิ่งนี้เป็นจริงได้ แต่คุณไม่มีทางรู้ การส่งแทรมโพลีนเป็นประโยชน์สำหรับการ จำกัด การเรียกซ้ำอย่างหนักเช่นเมื่อการนำภาษาไปใช้กำหนดขนาดสแต็กการโทรสูงสุด


18

อาจเข้าใจได้ง่ายขึ้นหากใช้แทรมโพลีนโดยใช้ชนิดส่งคืนเฉพาะ (แทนที่จะใช้ฟังก์ชั่นที่ไม่เหมาะสม):

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));

ตรงกันข้ามสิ่งนี้กับเวอร์ชันของtrampolineคุณซึ่งกรณีการเรียกซ้ำคือเมื่อฟังก์ชันส่งคืนฟังก์ชันอื่นและกรณีพื้นฐานคือเมื่อฟังก์ชันส่งคืนสิ่งอื่น

อะไรคือfoo = foo()สาเหตุที่ทำให้กองซ้อนล้น

มันไม่ได้เรียกตัวเองอีกต่อไป แต่จะส่งกลับผลลัพธ์ (ในการติดตั้งของฉันอย่างแท้จริง a Result) ที่สื่อถึงว่าจะดำเนินการเรียกซ้ำหรือดำเนินต่อไป

และfoo = foo()เทคนิคก็ไม่ได้กลายพันธุ์หรือว่าฉันทำอะไรหายไป? บางทีมันอาจเป็นเพียงความชั่วร้ายที่จำเป็น

ใช่นี่เป็นความชั่วร้ายที่จำเป็นของลูป หนึ่งสามารถเขียนtrampolineโดยไม่มีการกลายพันธุ์เช่นกัน แต่มันจะต้องเรียกซ้ำอีกครั้ง:

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;
}

แต่ถึงกระนั้นมันก็แสดงให้เห็นถึงแนวคิดของฟังก์ชั่นแทรมโพลีนที่ดียิ่งขึ้น

จุด trampoling คือการสรุปการเรียกแบบเรียกซ้ำจากฟังก์ชั่นที่ต้องการใช้การเรียกซ้ำเป็นค่าที่ส่งคืนและทำการเรียกซ้ำที่เกิดขึ้นจริงในที่เดียวเท่านั้น - trampolineฟังก์ชั่นซึ่งสามารถปรับให้เหมาะสมในที่เดียวเพื่อใช้ ห่วง


foo = foo()คือการกลายพันธุ์ในแง่ของการแก้ไขสถานะท้องถิ่น แต่โดยทั่วไปฉันคิดว่าการกำหนดใหม่เนื่องจากคุณไม่ได้แก้ไขวัตถุฟังก์ชันพื้นฐานคุณกำลังแทนที่ด้วยฟังก์ชัน (หรือค่า) ที่ส่งคืน
JAB

@JAB ใช่ฉันไม่ได้ตั้งใจจะแปลความหมายค่าที่fooมีอยู่เฉพาะตัวแปรที่ถูกแก้ไข whileห่วงต้องบางรัฐไม่แน่นอนถ้าคุณต้องการที่จะยุติในกรณีนี้ตัวแปรหรือfoo x
Bergi

ผมทำอะไรเช่นนี้ในขณะที่กลับมาอยู่ในคำตอบนี้จะเป็นคำถามที่กองมากเกินเกี่ยวกับการเพิ่มประสิทธิภาพสายหาง, trampolines ฯลฯ
โจชัวเทย์เลอร์

2
เวอร์ชันที่ไม่มีการกลายพันธุ์ของคุณได้แปลงการโทรซ้ำแบบเรียกซ้ำfnไปเป็นการเรียกซ้ำtrampoline- ฉันไม่แน่ใจว่าเป็นการปรับปรุง
Michael Anderson

1
@MichaelAnderson มันหมายถึงการแสดงให้เห็นถึงสิ่งที่เป็นนามธรรมเท่านั้น แน่นอนว่าแทรมโพลีนแบบเรียกซ้ำไม่มีประโยชน์
Bergi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.