özyineleme - javascript: özyinelemeli anonim fonksiyon?


Diyelim ki temel bir özyinelemeli fonksiyonum var:

function recur(data) {
    data = data+1;
    var nothing = function() {
        recur(data);
    }
    nothing();
}

Bunu, ... gibi anonim bir fonksiyonum varsa nasıl yapabilirim?

(function(data){
    data = data+1;
    var nothing = function() {
        //Something here that calls the function?
    }
    nothing();
})();

Bu fonksiyonu çağıran fonksiyonu çağırmanın bir yolunu istiyorum... Bir yerlerde (neresi olduğunu hatırlamıyorum) size çağrılan bir fonksiyonun adını söyleyebilen betikler gördüm, ama şu anda bu bilgilerin hiçbirini hatırlayamıyorum.

Cevap #1

Bir işlevi bir değer olarak oluştururken ve bir "function declaration" ifadesi olarak değilken bile işlevinize bir ad verebilirsiniz. Başka bir deyişle:

(function foo() { foo(); })();

yığın patlatan bir özyinelemeli fonksiyondur. Şimdi, bunu söyledikten sonra, genel olarak bunu yapmak istemeyebilirsiniz çünkü çeşitli Javascript uygulamalarıyla ilgili bazı garip problemler vardır. (not — bu oldukça eski bir yorumdur; Kangax’ın blog yazısında tarif edilen bazı/birçok/tüm problemler daha modern tarayıcılarda düzeltilmiş olabilir.)

Böyle bir isim verdiğinizde, isim fonksiyon dışında görünmez (aslında görünmemesi gerekir; bu tuhaflıklardan biridir). Lisp'teki "letrec" gibi bir şeydir.

arguments.callee için ise, bu "strict" modda yasaklanmıştır ve genel olarak kötü bir şey olarak kabul edilir, çünkü bazı optimizasyonları zorlaştırır. Ayrıca beklenenden çok daha yavaştır.

düzenle — Kendi kendini çağırabilen "anonim" bir işlevin etkisine sahip olmak istiyorsanız, bunu şöyle yapabilirsiniz (işlevi bir geri arama olarak veya buna benzer bir şekilde geçiriyorsanız):

asyncThingWithCallback(params, (function() {
  function recursive() {
    if (timeToStop())
      return whatever();
    recursive(moreWork);
  }
  return recursive;
})());

Bunun yaptığı şey, güzel, güvenli, IE'de bozulmamış bir deklarasyon ifadesiyle bir fonksiyon tanımlamak, adının global ad alanını kirletmeyeceği bir yerel fonksiyon oluşturmak. Sarmalayıcı (gerçekten anonim) fonksiyon sadece o yerel fonksiyonu döndürür.

Cevap #2

U kombinatörü

Bir fonksiyonu kendisine bir argüman olarak geçirerek, bir fonksiyon adını kullanmak yerine parametresini kullanarak yineleyebilir! Bu nedenle U'ya verilen fonksiyon, fonksiyona (kendisine) bağlanacak en az bir parametreye sahip olmalıdır.

Aşağıdaki örnekte, bir çıkış koşulumuz yok, bu yüzden bir yığın taşması olana kadar sonsuz döngüye gireceğiz

const U = f => f (f) // call function f with itself as an argument

U (f => (console.log ('stack overflow imminent!'), U (f)))

Sonsuz özyinelemeyi çeşitli teknikler kullanarak durdurabiliriz. Burada, anonim fonksiyonumuzun başka bir anonim fonksiyon döndürmesini yazacağım; bu fonksiyon bir girdi bekliyor; bu durumda, bir sayı. Bir sayı sağlandığında, eğer 0'dan büyükse özyinelemeye devam edeceğiz, aksi takdirde 0 döndüreceğiz.

const log = x => (console.log (x), x)

const U = f => f (f)

// when our function is applied to itself, we get the inner function back
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
// returns: (x => x > 0 ? U (f) (log (x - 1)) : 0)
// where f is a reference to our outer function

// watch when we apply an argument to this function, eg 5
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0) (5)
// 4 3 2 1 0

Burada hemen göze çarpmayan şey, fonksiyonumuz ilk olarak kendisine U kombinatörü kullanılarak uygulandığında, ilk girdi için bekleyen bir fonksiyon döndürmesidir. Bunu bir isim verirsek, lambdalar (anonim fonksiyonlar) kullanarak etkili bir şekilde özyinelemeli fonksiyonlar oluşturabiliriz.

const log = x => (console.log (x), x)

const U = f => f (f)

const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)

countDown (5)
// 4 3 2 1 0

countDown (3)
// 2 1 0

Ama bu sadece doğrudan özyineleme değildir - kendi adını kullanarak kendini çağıran bir fonksiyon. Bizim countDown tanımımız gövdesi içinde kendisine referans vermiyor ve yine de özyineleme mümkündür

// direct recursion references itself by name
const loop = (params) => {
  if (condition)
    return someValue
  else
    // loop references itself to recur...
    return loop (adjustedParams)
}

// U combinator does not need a named reference
// no reference to `countDown` inside countDown's definition
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)

Mevcut bir fonksiyondan U kombinatörü kullanarak kendine referansı nasıl kaldırılır

Burada, kendisine referans kullanan bir özyinelemeli fonksiyonu alıp, kendine referans yerine U kombinatörünü kullanan bir fonksiyona nasıl dönüştüreceğinizi göstereceğim

const factorial = x =>
  x === 0 ? 1 : x * factorial (x - 1)
  
console.log (factorial (5)) // 120

Şimdi içteki factorial referansını değiştirmek için U kombinatörünü kullanma

const U = f => f (f)

const factorial = U (f => x =>
  x === 0 ? 1 : x * U (f) (x - 1))

console.log (factorial (5)) // 120

Temel değiştirme deseni budur. Bunu zihninizde not edin, bir sonraki bölümde benzer bir strateji kullanacağız

// self reference recursion
const foo =         x => ...   foo (nextX) ...

// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)

Y kombinator

ilgili: U ve Y kombinatorleri ayna benzetmesi kullanılarak açıklandı

Önceki bölümde, kendi kendine başvuran özyinelemeyi, U kombinatörünü kullanarak isimsiz bir fonksiyona dayanmayan özyinelemeli bir fonksiyona nasıl dönüştürebileceğimizi gördük. Ancak, her zaman fonksiyonu kendisine ilk argüman olarak geçirmeyi hatırlamak biraz can sıkıcı. Yine de, Y-kombinatörü, U-kombinatörü üzerine inşa edilir ve bu sıkıcı kısmı ortadan kaldırır. Bu iyi bir şeydir çünkü karmaşıklığı kaldırmak/azaltmak, fonksiyonlar oluşturma amacımızın temel sebebidir.

Öncelikle, kendi Y-birleştiricimizi türetelim

// standard definition
const Y = f => f (Y (f))

// prevent immediate infinite recursion in applicative order language (JS)
const Y = f => f (x => Y (f) (x))

// remove reference to self using U combinator
const Y = U (h => f => f (x => U (h) (f) (x)))

Şimdi, kullanımının U-birleştirici ile nasıl karşılaştırıldığını göreceğiz. Dikkat edin, tekrarlamak için U (f) yerine basitçe f() çağırabiliriz

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

Y (f => (console.log ('stack overflow imminent!'),  f ()))

Şimdi Y kullanarak countDown programını göstereceğim - programların neredeyse aynı olduğunu göreceksiniz ama Y kombinatorü işleri biraz daha temiz tutuyor

const log = x => (console.log (x), x)

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const countDown = Y (f => x => x > 0 ? f (log (x - 1)) : 0)

countDown (5)
// 4 3 2 1 0

countDown (3)
// 2 1 0

Ve şimdi factorial'i de göreceğiz

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const factorial = Y (f => x =>
  x === 0 ? 1 :  x * f (x - 1))

console.log (factorial (5)) // 120

Gördüğünüz gibi, f kendisi özyinelemenin mekanizması haline geliyor. Özyineleme yapmak için, onu sıradan bir fonksiyon gibi çağırıyoruz. Farklı argümanlarla birden fazla kez çağırabiliriz ve sonuç yine doğru olacaktır. Ve sıradan bir fonksiyon parametresi olduğu için, ona istediğimiz ismi verebiliriz, aşağıda olduğu gibi recur gibi -

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const fibonacci = Y (recur => n =>
  n < 2 ? n : recur (n - 1) +  (n - 2))

console.log (fibonacci (10)) // 55

1’den fazla parametreyle U ve Y kombinatoryası

Yukarıdaki örneklerde, döngü kurarak ve bir argüman geçirerek hesaplamamızın "durumunu" nasıl takip edebileceğimizi gördük. Peki ya ek bir durumu takip etmemiz gerekirse?

Bir Array gibi bileşik veriler kullanabilir iz...

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const fibonacci = Y (f => ([a, b, x]) =>
  x === 0 ? a : f ([b, a + b, x - 1]))

// starting with 0 and 1, generate the 7th number in the sequence
console.log (fibonacci ([0, 1, 7])) 
// 0 1 1 2 3 5 8 13

Ama bu kötü çünkü iç durumu (sayaclar a ve b) açığa çıkarıyor. İstediğimiz cevabı almak için sadece fibonacci (7) çağırabilseydik hoş olurdu.

Kavramsallaştırılmış fonksiyonlar (tek parametreli fonksiyon dizileri) hakkında bildiklerimizi kullanarak, Y tanımımızı değiştirmemize ya da bileşik veri veya ileri dil özelliklerine dayanmak zorunda kalmadan amacımıza kolayca ulaşabiliriz.

Aşağıda fibonacci tanımına yakından bakın. Anında 0 ve 1 değerlerini sırasıyla a ve b değişkenlerine uyguluyoruz. Şimdi fibonacci, sadece son argümanın sağlanmasını bekliyor ve bu argüman x değişkenine bağlanacak. Özyineleme yaptığımızda, f (a) (b) (x) şeklinde çağırmalıyız (f (a,b,x) değil) çünkü fonksiyonumuz currying uygulanmış formdadır.

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const fibonacci = Y (f => a => b => x =>
  x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)

console.log (fibonacci (7)) 
// 0 1 1 2 3 5 8 13

Bu tür bir desen, her türlü fonksiyonu tanımlamak için faydalı olabilir. Aşağıda, Y kombinatörü kullanılarak tanımlanmış iki fonksiyonu (range ve reduce) ve reduce'ün bir türevi olan map'i göreceğiz.

const U = f => f (f)

const Y = U (h => f => f (x => U (h) (f) (x)))

const range = Y (f => acc => min => max =>
  min > max ? acc : f ([...acc, min]) (min + 1) (max)) ([])

const reduce = Y (f => g => y => ([x,...xs]) =>
  x === undefined ? y : f (g) (g (y) (x)) (xs))
  
const map = f =>
  reduce (ys => x => [...ys, f (x)]) ([])
  
const add = x => y => x + y

const sq = x => x * x

console.log (range (-2) (2))
// [ -2, -1, 0, 1, 2 ]

console.log (reduce (add) (0) ([1,2,3,4]))
// 10

console.log (map (sq) ([1,2,3,4]))
// [ 1, 4, 9, 16 ]

HEPSİ ANONİM, AMAN TANRIM

Burada saf fonksiyonlarla çalıştığımız için, herhangi bir isimli fonksiyonu tanımıyla değiştirebiliriz. Fibonacci'yi alıp isimli fonksiyonları ifadeleriyle değiştirdiğimizde ne olduğunu izleyin

/* const U = f => f (f)
 *
 * const Y = U (h => f => f (x => U (h) (f) (x)))
 *
 * const fibonacci = Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
 *
 */

/*
 * given fibonacci (7)
 *
 * replace fibonacci with its definition
 * Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
 *
 * replace Y with its definition
 * U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
//
 * replace U with its definition
 * (f => f (f)) U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
 */

let result =
  (f => f (f)) (h => f => f (x => h (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
  
console.log (result) // 13

Ve işte bu - yalnızca anonim fonksiyonlar kullanılarak özyinelemeli olarak hesaplanan fibonacci (7)

Cevap #3

İnsanlar yorumlarda Y kombinatoründen bahsetti, ama kimse bunu bir cevap olarak yazmadı.

Y kombinatoryası JavaScript'te şu şekilde tanımlanabilir: (bağlantı için steamer25'e teşekkürler)

var Y = function (gen) {
  return (function(f) {
    return f(f);
  }(function(f) {
    return gen(function() {
      return f(f).apply(null, arguments);
    });
  }));
}

Ve anonim fonksiyonunuzu geçirmek istediğinizde:

(Y(function(recur) {
  return function(data) {
    data = data+1;
    var nothing = function() {
      recur(data);
    }
    nothing();
  }
})());

Bu çözüm hakkında not edilmesi gereken en önemli şey, onu kullanmamanız gerektiğidir.

Cevap #4

Bunun yerine bir "anonim nesne" kullanmak en basit yol olabilir:

({
  do: function() {
    console.log("don't run this ...");
    this.do();
  }
}).do();

Küresel alanınız tamamen kirlenmemiş. Oldukça basit. Ve nesnenin küresel olmayan durumundan kolayca faydalanabilirsiniz.

Sözdizimini daha özlü hale getirmek için ES6 nesne yöntemlerini de kullanabilirsiniz.

({
  do() {
    console.log("don't run this ...");
    this.do();
  }
}).do();

Cevap #5

Bunu bir inline fonksiyon olarak yapmazdım. Bu, iyi zevkin sınırlarını zorluyor ve size gerçekten herhangi bir fayda sağlamıyor.

Gerçekten gerekiyorsa, Fabrizio'nun cevabında olduğu gibi arguments.callee vardır. Ancak bu genellikle tavsiye edilmez ve ECMAScript Beşinci Sürüm'ün 'strict mode'unda yasaktır. ECMA 3 ve strict olmayan mod ortadan kalkmasa da, strict modda çalışmak daha fazla olası dil optimizasyonları vadeder.

Ayrıca adlandırılmış bir satır içi fonksiyon da kullanılabilir:

(function foo(data){
    data++;
    var nothing = function() {
        foo(data);
    }
    nothing();
})();

Ancak, isimlendirilmiş inline fonksiyon ifadelerinden de kaçınılması en iyisidir, çünkü IE'nin JScript'i onlara bazı kötü şeyler yapar. Yukarıdaki örnekte foo IE'de yanlışlıkla üst scope'u kirletir ve üstteki foo, foo içindeki foo ile ayrı bir örnektir.

Bunu bir satır içi anonim fonksiyona koymanın amacı nedir? Eğer sadece üst kapsamı kirletmekten kaçınmak istiyorsanız, elbette ilk örneğinizi başka bir kendi kendini çağıran anonim fonksiyonun (ad alanının) içine gizleyebilirsiniz. Her döngüde nothing’ın yeni bir kopyasını gerçekten oluşturmanız gerekiyor mu? İki basit birbirine özyinelemeli fonksiyon içeren bir ad alanı ile daha iyi iş yapabilirsiniz.

İlgili makaleler