Trambolinler neden işe yarıyor?


104

Bazı fonksiyonel JavaScriptler yapıyorum. Tail-Call Optimizasyonunun uygulandığını düşünmüştüm , fakat anlaşıldığı kadarıyla hatalıydım. Bu yüzden kendime Trampolining öğretmek zorunda kaldım . Burada ve başka yerlerde biraz okuduktan sonra, temelleri indirip ilk trambolinimi yapmayı başardım:

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

En büyük sorunum, bunun neden işe yaradığını bilmiyorum. Özyinelemeli bir döngü kullanmak yerine, işlevi bir süre döngüsü içinde yeniden çalıştırma fikrini alıyorum. Bunun dışında, teknik olarak benim temel fonksiyonum zaten özyinelemeli bir döngüye sahip. Temel loopyişlevi çalıştırmıyorum , ancak içindeki işlevi çalıştırıyorum. Ne durduran foo = foo()bir bellek taşması gelen? Ve değil foo = foo()teknik mutasyona veya bir şey eksik? Belki de sadece gerekli bir kötülük. Ya da bazı sözdizimini özlüyorum.

Bunu anlamanın bir yolu var mı? Yoksa bir şekilde işe yarayan sadece bir kesmek mi? Her şeyden kurtulmamı başardım, ama bu beni şaşkına çevirdi.


5
Evet, ama bu hala özyinelemede. loopytaşmadığı için kendisini çağırmaz .
tkausl

4
“TCO’nun uygulandığını düşünmüştüm, ancak ortaya çıktığı gibi yanılmışım.” Çoğu skandada V8'de olmuştur. Sen V8 bunu sağlamak için Düğüm anlatarak Düğüm herhangi güncel sürümünde mesela kullanabilirsiniz: stackoverflow.com/a/30369729/157247 Chrome'un Chrome 51. beri (bir "deneysel" bayrağı arkasında) vardı
TJ Crowder

125
Kullanıcının kinetik enerjisi, trambolin sarktıkça elastik potansiyel enerjiye, daha sonra geri teperken kinetik enerjiye dönüşür.
immibis

66
@ immibis, Buraya hangi Stack Exchange sitesinin olduğunu kontrol etmeden buraya gelen herkes adına, teşekkür ederim.
user1717828

4
@jpaugh "atlamalı" mı demek istedin? ;-)
Hulk

Yanıtlar:


89

Beyin fonksiyonu isyan olmasının sebebi loopy()bir ait olmasıdır tutarsız türü :

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

Oldukça fazla dil, bunun gibi şeyleri yapmanıza bile izin vermez veya en azından bunun nasıl bir anlam ifade etmesi gerektiğini açıklamak için çok daha fazla yazmayı talep eder. Çünkü gerçekten değil. İşlevler ve tam sayılar tamamen farklı türden nesnelerdir.

Öyleyse döngü boyunca dikkatli bir şekilde geçelim:

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

Başlangıçta, fooeşittir loopy(0). Nedir loopy(0)? 10000000'den az, öyleyse anladık function(){return loopy(1)}. Bu gerçek bir değer ve bu bir işlev, bu yüzden döngü devam ediyor.

Şimdi biz geldik foo = foo(). foo()aynıdır loopy(1). 1 hala 10000000'den az olduğu için, bu function(){return loopy(2)}daha sonra atadığımız değere döner foo.

foohala bir fonksiyondur, bu yüzden devam ediyoruz ... en sonunda eşit olana kadar function(){return loopy(10000000)}. Bu bir fonksiyondur, bu yüzden foo = foo()bir kez daha yaparız , ama bu sefer, aradığımızda loopy(10000000), x 10000000'den az değildir, bu yüzden sadece x'i geri alırız. 10000000 de bir işlev olmadığından, bu süre döngüsü de sona erer.


1
Yorumlar uzun tartışmalar için değildir; bu konuşma sohbete taşındı .
yannis,

Bu gerçekten sadece bir miktar türü. Bazen bir değişken olarak bilinir. Dinamik diller bunları kolayca destekler, çünkü her değer etiketlenir, daha statik yazılan diller ise fonksiyonun bir değişken döndürdüğünü belirtmenizi ister. Örneğin trambolinler C ++ veya Haskell'de kolayca mümkündür.
GManNickG 12:16

2
@GManNickG: Evet, demek istediğim "daha çok yazarak". C'de bir sendika ilan etmeniz, birliği etiketleyen, bir yapıyı her iki uçtan paketleyen ve paketten çıkaran, bir yapıyı her iki uçtan paketleyen ve paketten açan bir yapı tanımlamanız ve (muhtemelen) yapının yaşadığı hafızanın kim olduğunu bulmanız gerekir. . C ++ bundan daha az kodludur, ancak kavramsal olarak C'den daha az karmaşık değildir ve OP'nin Javascript'inden daha ayrıntılıdır.
Kevin,

Tabii ki, buna itiraz etmiyorum, sadece onun üzerinde çok garip ya da anlam ifade etmeme konusundaki vurgunun biraz güçlü olduğunu düşünüyorum. :)
GManNickG 12:16

173

Kevin, bu özel kod snippet'inin nasıl çalıştığını (neden oldukça anlaşılmaz olduğu ile birlikte) kısaca belirtiyor, ancak genel olarak trambolinlerin nasıl çalıştığı hakkında bazı bilgiler eklemek istedim .

Kuyruk çağrısı optimizasyonu (TCO) olmadan, her işlev çağrısı geçerli yürütme yığına bir yığın çerçeve ekler . Diyelim ki sayıları geri saydırabilecek bir işleve sahibiz:

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

Eğer countdown(3)ararsak, çağrı yığınının TCO olmadan nasıl görüneceğini analiz edelim.

> 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 ile, her özyinelemeli çağrı için countdownise kuyruk pozisyonunda hiçbir yığın çerçevesi tahsis edilir (çağrının sonucu döndüren dışında yapmak şey kalmadı). TCO olmadan, yığın hafifçe bile olsa genişler n.

Tramplenleme, countdownfonksiyonun etrafına bir sarmalayıcı takarak bu kısıtlamanın üstesinden gelir . Ardından, countdownözyinelemeli aramalar yapmaz ve hemen aranacak bir işlevi döndürür. İşte örnek bir uygulama:

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

Bunun nasıl çalıştığını daha iyi anlamak için çağrı yığınına bakalım:

> 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

Her At adım countdownHopfonksiyonu terk yerine o ne anlatır çağırmak için bir işlev dönen, bundan sonra ne olur doğrudan kontrolünü gibi sonra ne. Trambolin fonksiyonu o zaman bu alır ve onu çağırır, o zaman ne olursa olsun fonksiyonu çağıran o hayır "Bir sonraki adım" kalmayıncaya kadar böyle devam döndürür ve. Buna trambolin işlemi denir, çünkü kontrolün akışı her yinelemeli çağrı ile doğrudan yinelenen işlev yerine, trambolin uygulaması arasında "sıçrar". Özyinelemeli çağrı yapan kim üzerindeki kontrolünü bırakarak , trambolin işlevi yığının çok büyük olmamasını sağlayabilir. Not: bu uygulama, trampolinesadelik için değer döndüren ihmalleri içermektedir.

Bunun iyi bir fikir olup olmadığını bilmek zor olabilir. Yeni bir kapanış sağlayan her adım nedeniyle performans düşebilir. Akıllıca optimizasyonlar bunu mümkün kılabilir, ancak asla bilemezsiniz. Trambolin oluşturma, örneğin, bir dil uygulaması maksimum arama yığını boyutu belirlediğinde, sabit özyineleme sınırlarını aşmada yararlıdır.


18

Belki de trambolinin özel bir dönüş türüyle uygulanıp uygulanmadığını anlamak kolaylaşır (bir işlevi kötüye kullanmak yerine):

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

Bunun aksine trampoline, işlev başka bir işlev döndürdüğünde özyineleme durumunun olduğu ve temel durum ise başka bir şey döndürdüğü zamanki sürümünüze zıtlık kazandırın .

Ne durduran foo = foo()bir bellek taşması gelen?

Artık kendisini aramıyor. Bunun yerine, Resultözyinelemeye devam edip etmeyeceğini veya dağılıp atılmayacağını belirten bir sonuç (benim uygulamamda, a ).

Ve değil foo = foo()teknik mutasyona veya bir şey eksik? Belki de sadece gerekli bir kötülük.

Evet, bu tam olarak gerekli döngünün kötülük olduğunu. Biri de trampolinemutasyon olmadan yazabilir , ancak tekrar özyinelemeyi gerektirir:

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

Yine de, trambolin işlevinin daha iyi ne yaptığı fikrini gösteriyor.

Trampoling noktası olduğunu dışarı soyutlayarak bir dönüş değeri haline Özyinelemeyi kullanmak isteyen işlevinden kuyruk özyinelemeli çağrı ve sadece tek bir yerde gerçek özyinelemeye yapıyor - trampolinedaha sonra tek bir yerde optimize edilebilir fonksiyon, bir kullanımı döngü.


foo = foo()yerel durumu değiştirme anlamında bir mutasyondur, ancak genel olarak, temel fonksiyon nesnesini gerçekten değiştirmediğiniz zaman yeniden atama işleminin yerine, onu döndürdüğü işlev (veya değer) ile değiştirdiğinizi düşünürdüm.
JAB

@JAB Evet, değeri fooiçeren değeri değiştirmek istemedim , sadece değişken değiştirildi. Bir whiledöngü sonlandırılmasını istiyorsanız bazı değişken durumları gerektirir, bu durumda değişken fooveya x.
Bergi

Bir süre önce böyle bir şey yaptım, bu cevabınızı kuyruk çağrısı optimizasyonu, trambolinler vb. Hakkında bir Stack Overflow sorusuna cevapladım.
Joshua Taylor

2
Mutasyon olmadan sürümünüz bir özyinelemeli çağrı dönüştüğü fnbir özyinelemeli çağrı içine trampoline- Bunun bir gelişme olduğundan emin değilim.
Michael Anderson,

1
@ MichaelAnderson Sadece soyutlamayı göstermek içindir. Elbette özyinelemeli bir trambolin işe yaramaz.
Bergi
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.