Yazılımcı Masalları

Masal 3

Solucanın çantası

Bir fonksiyon, işi bittikten sonra doğduğu yeri nasıl hatırlar?

Merhaba, ben Kaşif Oğuz. Bugün Motor Dairesi’ndeyim. Burada sırtında çanta taşıyan bir solucan varmış.

Önce şunu oku Çevirgen Solucan · function
Kaydır, sayfa çevrilsin

Odalar iç içe.

Motor Dairesi’nde her fonksiyon bir oda açar. Odalar iç içe durur. İçerideki odadan dışarıdakileri görürsün. Dışarıdan içeriye bakamazsın.

const island = "Katmanya";
function house() {
  const room = "mutfak";
  const cupboard = () => console.log(island, room);
  cupboard();
}
house();
console.log(room);
// → Katmanya mutfak
// → ReferenceError: room is not defined

Dolap mutfağı da adayı da görüyor. Ama evin dışından room diye sorunca motor hata veriyor.

Burada Gelmeyen Hayalet (undefined) bile gelmiyor. Hayalet, var olan ama değeri olmayan bir yer için gelir. room dışarıda hiç yok.

Buna scope (kapsam) diyoruz: bir adın görülebildiği odalar.

Bu sahnedeki yaratık Gelmeyen Hayalet · undefined

Solucan doğduğu yere bakar.

Bir soru takıldı aklıma: solucan bir adı ararken çağrıldığı odaya mı bakıyor, doğduğu odaya mı?

const where = "doğduğu yer";
function worm() { return where; }
function elsewhere() {
  const where = "çağrıldığı yer";
  return worm();
}
console.log(elsewhere());
// → doğduğu yer

worm başka bir odada çağrıldı. O odada da where vardı. Solucan ona bakmadı.

Solucan, yazıldığı yerin odalarına bakar. Kodu okurken hangi odada durduğunu görüyorsan, neyi göreceğini de biliyorsun.

Solucan solucan doğurursa.

Bir fonksiyon, sonuç olarak başka bir fonksiyon döndürebilir. Solucanın kuyruğundan yeni bir solucan çıkar.

function makeGreeter(greeting) {
  return function (name) {
    return greeting + ", " + name;
  };
}
const hello = makeGreeter("Merhaba");
console.log(hello("Oğuz"));
// → Merhaba, Oğuz

makeGreeter çağrılınca tabak yığınının tepesine bir tabak kondu. return dedi, tabak kalktı. Onun işi bitti.

Ama hello çağrılınca greeting hâlâ "Merhaba". Oda kapanmış olmalıydı. Yavru solucan bu değeri nereden buldu?

Cevap sırtında: doğduğu odaya bağlı bir çanta. Fonksiyon ile bu bağın ikisine birlikte closure diyoruz.

Sayaç.

Çantanın en bilinen kullanımı bir sayaç.

function makeCounter() {
  let count = 0;
  return () => ++count;
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), a(), b());
// → 1 2 3 1

a her çağrılınca count bir artıyor ve önceki değeri unutulmuyor.

b ise 1’den başlıyor. Çünkü makeCounter her çağrıldığında yeni bir oda açılıyor, içinde yeni bir count oluyor. a ile b iki ayrı odaya bağlı, iki ayrı çanta taşıyor.

Dışarıdan count’a kimse dokunamıyor. Sayacı artırmanın tek yolu solucanı çağırmak.

Çanta kopya değil.

Çantanın içinde ne var diye düşündüm. Doğduğu anda değerlerin fotoğrafını mı çekiyor?

let color = "kırmızı";
const tell = () => color;
color = "mavi";
console.log(tell());
// → mavi

Solucan "kırmızı" varken doğdu. Sonra renk değişti. Solucan "mavi" dedi.

Çantada değerin kopyası yok. Çanta değişkenin kendisine açılıyor. Değişken değişirse solucan yeni değeri görüyor. Sayaçta olduğu gibi solucan değişkeni değiştirirse, o değişkene bakan herkes yeni değeri görüyor.

Aynı çantayı taşıyan iki solucan.

Aynı odada iki solucan doğarsa ne olur?

function makeCounter() {
  let count = 0;
  return { up: () => ++count, peek: () => count };
}
const c = makeCounter();
c.up(); c.up();
console.log(c.peek());
// → 2

makeCounter bu sefer bir Çekmece Kaplumbağası döndürüyor. Kaplumbağanın iki çekmecesinde iki solucan var: up ve peek.

up sayacı iki kez artırdı. peek ona hiç dokunmadı, ama 2 gördü. İkisi aynı odada doğdu, ikisi aynı count’a bağlı.

Çekmecedeki solucana metot diyoruz. Bu kaplumbağanın count diye bir çekmecesi yok. Sayı dışarıdan görünmüyor, yalnızca iki solucan ona ulaşıyor.

Bu sahnedeki yaratık Çekmece Kaplumbağası · object

Döngüdeki tuzak.

Bunu bilen herkes bir kez bu tuzağa düşmüştür. Ben de düştüm.

const withVar = [];
for (var i = 0; i < 3; i++) withVar.push(() => i);
const withLet = [];
for (let j = 0; j < 3; j++) withLet.push(() => j);
console.log(withVar.map((f) => f()));
console.log(withLet.map((f) => f()));
console.log(i);
// → [ 3, 3, 3 ]
// → [ 0, 1, 2 ]
// → 3

İki döngü de üç solucan doğuruyor. Biri "3, 3, 3" diyor, öteki "0, 1, 2".

var döngü için oda açmaz; i dışarıdaki odada tek bir değişkendir. Son satır bunu gösteriyor: döngü bitti, i hâlâ orada ve 3. Üç solucan da aynı i’ye bağlı. Çağırdığımızda döngü bitmiş, i çoktan 3 olmuştu.

let ile döngü her turda yeni bir oda açar ve o tura ait yeni bir j koyar. Yeni turun j’si bir önceki turun değeriyle başlar, sonra artar. Her solucan kendi turunun j’sine bağlı kalır.

Aynı tuzak setTimeout ile de kurulur: var ile üç kez 3 yazar, let ile 0, 1, 2.

Çanta nerede durur.

Motor Dairesi’nde iki yer var. Tabak yığını (çağrı yığını): çağrılan her fonksiyon tepeye bir tabak koyar, return deyince tabak kalkar. Depo (öbek): uzun yaşayacak şeylerin durduğu yer. Çekmece Kaplumbağaları burada yaşar.

makeCounter dönünce tabağı kalktı. Ama count tabakla birlikte gidemezdi, çünkü bir solucan hâlâ ona bağlı.

Motor bunu kod çalışmadan önce görür. Bir solucanın ihtiyaç duyduğu değişkeni tabağa değil, depoya koyar. Çantanın ipi depodaki o rafa gider.

Bu bir motor düzeni. V8 gibi motorlar böyle çalışıyor. ECMAScript değişkenlerin bellekte nerede durduğunu söylemez. Yalnızca her odanın bir ortamı (environment record) olduğunu ve her fonksiyonun doğduğu ortama bir bağ tuttuğunu söyler.

Haritada bu kat Motor Dairesi · +1

Çanta ne zaman bırakılır.

Depo sonsuz değil. Motorda bir çöpçü var: çöp toplayıcı (garbage collector). Kimsenin ulaşamadığı şeyleri depodan kaldırır.

Bir çanta, ona bağlı bir solucana hâlâ ulaşılabildiği sürece depoda kalır. Solucana giden son yol kesilince solucan da çantası da çöp olur.

function makeWorm() {
  const bag = { load: "ağır yük" };
  globalThis.watch = new WeakRef(bag);
  return () => bag.load;
}
let worm = makeWorm();
worm = null;
setTimeout(() => { gc(); console.log(watch.deref()); });
// $ node --expose-gc -e '...'
// → undefined

worm = null satırında Sandalye Hayaleti gelip boş sandalyeyi bıraktı: bu değişken artık bilerek boş. Solucana başka yol yok.

WeakRef uzaktan bakan bir dürbün. Yükü görür ama onu depoda tutmaz. gc() çöpçüyü hemen çağırır. Bu yalnızca deneme için var, --expose-gc bayrağıyla açılıyor.

worm = null satırını silince çıktı şu oluyor:

// → { load: 'ağır yük' }

Solucan yaşıyor, çanta da yaşıyor. Gerçek bir programda çöpçüyü çağıramazsın, ne zaman geleceğine motor karar verir. Senin elinde olan, solucana giden yolu kesmek. Uzun yaşayan solucanlar (örneğin bir olay dinleyicisi) çantalarındaki her şeyi de yaşatır.

Bu sahnedeki yaratık Sandalye Hayaleti · null

Nerede kırılır

Çanta benzetmesinin yanlış öğreteceği yerler:

Dürüst not.

Bütün kod parçalarını yalnızca Node 22.22’de (V8 motoru) çalıştırdık. Firefox ve Safari’nin motorlarını denemedik.

s.8’deki tabak ve depo ayrımı V8’in nasıl çalıştığına dayanıyor. Dil kuralı bunu söylemiyor; başka bir motor değişkenleri başka bir yerde tutabilir.

"Nerede kırılır"daki ortak çanta davranışı kendi denememizde görüldü. V8’in bunu her sürümde aynı yaptığını doğrulamadık.

Çöp toplamayı gc() ile zorladık. Gerçek bir programda çöpçünün ne zaman geleceği belli değil. ECMAScript, WeakRef’in baktığı şeyin ne zaman toplanacağına söz vermez.

Çevirgen Solucan’ın kartı henüz doğrulayıcıdan geçmedi.

Sıra sende

Sayacı biraz büyüt: makeCounter’a sayacı sıfırlayan bir reset solucanı ekle. Sonra kendine sor: reset’i ayrı bir makeCounter çağrısında yazsaydın sayacı sıfırlayabilir miydi?

Kendi kodunda uzun yaşayan bir solucan bul: bir olay dinleyicisi, bir zamanlayıcı. Çantasında ne taşıyor?

Seninkinin çantasında ne var?

Kaynak notu. MDN, “Closures” (closure tanımı ve döngü örneği) · MDN, “Memory management” (ulaşılabilirlik ve çöp toplama) · MDN, “WeakRef” (toplanma zamanının garanti olmaması) · ECMAScript: Environment Records, fonksiyonun [[Environment]] alanı (OrdinaryFunctionCreate), for (let …) için CreatePerIterationEnvironment, ok fonksiyonlarında [[ThisMode]] lexical. Kod parçaları Node 22.22 ile çalıştırıldı.

Fikirler ve hikâyeler Oğuz’dan. Metinleri ve çizimleri yapay zekâyla birlikte üretiyoruz; her sayfayı Oğuz okuyup onaylıyor. Bir DemirCraftCo işi.

Başa dön