Trampolines क्यों काम करते हैं?


104

मैं कुछ कार्यात्मक जावास्क्रिप्ट कर रहा हूँ। मैंने सोचा था कि टेल-कॉल ऑप्टिमाइज़ेशन लागू किया गया था, लेकिन जैसा कि यह पता चला है कि मैं गलत था। इस प्रकार, मैं अपने आप को पढ़ाने के लिए मिला है ट्रेम्पोलिनिंग । यहाँ और कहीं और पढ़ने के बाद, मैं मूल बातें नीचे लाने में सक्षम हुआ और अपनी पहली ट्रम्पोलिन का निर्माण किया:

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

4
"मुझे लगा था कि TCO लागू हो गया था, लेकिन जैसा कि यह पता चला है कि मैं गलत था।" यह सबसे scenaros में V8 में कम से कम किया गया है। आप इसे वी 8 में यह सक्षम करने के लिए नोड बताकर नोड के किसी भी हाल के संस्करण में उदाहरण के लिए उपयोग कर सकते हैं: stackoverflow.com/a/30369729/157247 Chrome की यह था (एक "प्रायोगिक" ध्वज के पीछे) क्रोम 51. के बाद से
टीजे Crowder

125
उपयोगकर्ता से गतिज ऊर्जा को लोचदार संभावित ऊर्जा में बदल दिया जाता है, जैसे कि ट्रैंपोलिन सैग्स, फिर वापस गतिज ऊर्जा के रूप में यह रिबाउंड होता है।
इबिबिस

66
@immibis, जो भी स्टैक एक्सचेंज साइट यह जाँच के बिना यहाँ आए सभी की ओर से, धन्यवाद था।
user1717828 15

4
@jpaugh का मतलब "होपिंग" था? ;-)
हल्क

जवाबों:


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अभी भी एक समारोह है, इसलिए हम जा रहे हैं ... जब तक कि अंततः फू के बराबर नहीं है function(){return loopy(10000000)}। यह एक फ़ंक्शन है, इसलिए हम foo = foo()एक बार और करते हैं , लेकिन इस बार, जब हम कॉल करते हैं loopy(10000000), तो x 10000000 से कम नहीं होता है, इसलिए हमें सिर्फ x वापस मिलता है। चूंकि 10000000 भी एक फ़ंक्शन नहीं है, इसलिए यह लूप को भी समाप्त करता है।


1
टिप्पणियाँ विस्तारित चर्चा के लिए नहीं हैं; इस वार्तालाप को बातचीत में स्थानांतरित कर दिया गया है ।
यानिस १२'१६ को

यह वास्तव में सिर्फ एक योग प्रकार है। कभी-कभी एक संस्करण के रूप में जाना जाता है। गतिशील भाषाएं उन्हें आसानी से समर्थन करती हैं क्योंकि हर मूल्य को टैग किया जाता है, जबकि अधिक सांख्यिकीय रूप से टाइप की जाने वाली भाषाओं के लिए आपको फ़ंक्शन को एक वेरिएंट निर्दिष्ट करना होगा। उदाहरण के लिए, C ++ या हास्केल में आसानी से Trampolines संभव हैं।
GManNickG

2
@GManNickG: हाँ, यही मेरा मतलब है "बहुत अधिक टाइपिंग से।" C में आपको एक संघ की घोषणा करनी होगी, एक ऐसी संरचना की घोषणा करें जो संघ को टैग करती है, या तो अंत में संरचना को पैक, पैक करती है और या तो अंत में यूनियन को पैक और अनपैक करती है, और (संभवतः) यह पता लगाती है कि कौन स्मृति का मालिक है जो संरचना का निवास करता है । C ++ की तुलना में बहुत कम कोड है, लेकिन यह वैचारिक रूप से C से कम जटिल नहीं है, और यह अभी भी OP की जावास्क्रिप्ट से अधिक क्रिया है।
केविन

निश्चित रूप से, मैं यह चुनाव नहीं लड़ रहा हूं, मुझे लगता है कि आप इस पर जोर देते हैं कि यह अजीब है या समझ में नहीं आता है। :)
GManNickG

173

केविन ने स्पष्ट रूप से बताया कि यह विशेष कोड स्निपेट कैसे काम करता है (साथ में यह काफी समझ से बाहर क्यों है), लेकिन मैं सामान्य काम में कैसे trampolines के बारे में कुछ जानकारी जोड़ना चाहता था ।

टेल-कॉल ऑप्टिमाइज़ेशन (TCO) के बिना, प्रत्येक फ़ंक्शन कॉल वर्तमान निष्पादन स्टैक में एक स्टैक फ़्रेम जोड़ता है । मान लें कि हमारे पास संख्याओं की उलटी गिनती के लिए एक समारोह है:

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

यदि हम कॉल करते हैं countdown(3), तो विश्लेषण करें कि कॉल स्टैक बिना 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

इस प्रतिबंध को 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);
  }
}

यह कैसे काम करता है, इसकी बेहतर समझ पाने के लिए, आइए कॉल स्टैक देखें:

> 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समारोह छोड़ दिया आगे क्या होता है, बजाय एक समारोह लौटने कॉल करने के लिए का वर्णन करता है यह क्या होगा के प्रत्यक्ष नियंत्रण की तरह आगे क्या होने। ट्रम्पोलिन फ़ंक्शन तब इसे लेता है और इसे कॉल करता है, फिर जो भी फ़ंक्शन देता है उसे कॉल करता है , और इसी तरह जब तक "अगला चरण" नहीं होता है। इसे ट्रम्पोलिनिंग कहा जाता है क्योंकि नियंत्रण का प्रवाह प्रत्येक पुनरावर्ती कॉल और ट्रम्पोलिन कार्यान्वयन के बीच "बाउंस" होता है, बजाय फ़ंक्शन सीधे आवर्ती के बजाय। पुनरावर्ती कॉल कौन करता है पर नियंत्रण का परित्याग करके , ट्रैम्पोलिन फ़ंक्शन सुनिश्चित कर सकता है कि स्टैक बहुत बड़ा नहीं मिलता है। साइड नोट: यह कार्यान्वयन 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()स्टैक ओवरफ्लो पैदा करने से क्या रोकना है?

यह खुद को और अधिक नहीं कहता है। इसके बजाय, यह एक परिणाम देता है (मेरे कार्यान्वयन में, शाब्दिक रूप से 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;
}

फिर भी, यह विचार दिखाता है कि ट्रम्पोलिन फ़ंक्शन क्या बेहतर करता है।

ट्रम्पोलिंग का बिंदु उस फ़ंक्शन से पूंछ-पुनरावर्ती कॉल को अमूर्त कर रहा है जो एक वापसी मूल्य में पुनरावृत्ति का उपयोग करना चाहता है, और केवल एक ही स्थान पर वास्तविक पुनरावृत्ति कर रहा है - trampolineफ़ंक्शन, जिसे तब एक ही स्थान पर उपयोग करने के लिए अनुकूलित किया जा सकता है पाश।


foo = foo()स्थानीय स्थिति को संशोधित करने के अर्थ में उत्परिवर्तन है, लेकिन मैं आम तौर पर उस पुनर्विचार पर विचार करूंगा क्योंकि आप वास्तव में अंतर्निहित फ़ंक्शन ऑब्जेक्ट को संशोधित नहीं कर रहे हैं, आप इसे फ़ंक्शन (या मान) के साथ बदल रहे हैं।
JAB

@JAB हाँ, मेरा मतलब यह नहीं है कि जो मूल्य fooसमाहित है, केवल परिवर्तनशील है। एक whileलूप के लिए कुछ परिवर्तनशील अवस्था की आवश्यकता होती है यदि आप इसे समाप्त करना चाहते हैं, तो इस स्थिति में चर fooया x
बरगी

मैं इस तरह से कुछ समय पहले पूंछ कॉल अनुकूलन, trampolines, आदि के बारे में एक स्टैक ओवरफ्लो सवाल के जवाब में किया था
यहोशू टेलर

2
उत्परिवर्तन के बिना आपके संस्करण ने एक पुनरावर्ती कॉल को पुनरावर्ती कॉल में fnबदल दिया है trampoline- मुझे यकीन नहीं है कि यह एक सुधार है।
माइकल एंडरसन

1
@MichaelAnderson यह केवल अमूर्तता प्रदर्शित करने के लिए है। बेशक एक पुनरावर्ती ट्रम्पोलिन उपयोगी नहीं है।
बरगी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.