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 · functionOdalar 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 · undefinedSolucan 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ı · objectDö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 · nullNerede kırılır
Çanta benzetmesinin yanlış öğreteceği yerler:
Çanta sırtta durmaz.
Çanta solucanın kendi malı değil, depodaki bir odaya giden bir bağ. Aynı odada doğan solucanlar aynı odaya bağlıdır (s.6). Gerçek bir çantayı iki kişi aynı anda sırtında taşıyamaz, burada taşırlar.
Çanta tek kat değil.
Solucan doğduğu odaya bağlı, o oda da bir dış odaya. Zincir en dıştaki odaya kadar gider. Solucan bir adı önce kendi odasında, sonra sırayla dış odalarda arar (s.1).
Çantaya ne girdiğini dil söylemez, motor seçer.
ECMAScript’e göre solucan bütün odaya bağlıdır. Motor, hiçbir solucanın kullanmadığı bir değişkeni çantaya hiç koymayabilir. Bu bir optimizasyon, dil kuralı değil. Node 22 (V8) ile denedik:
function makeWorm() { const big = { load: "ağır yük" }; globalThis.watch = new WeakRef(big); const other = () => big.load; return () => "big kullanmıyorum"; } let worm = makeWorm(); setTimeout(() => { gc(); console.log(watch.deref()); }); // $ node --expose-gc -e '...' // → { load: 'ağır yük' }Dönen solucan big’e hiç bakmıyor. Ama aynı odada doğan other bakıyor, ve V8 o odadaki solucanlara ortak bir çanta veriyor. other hiç dışarı çıkmasa bile big yaşıyor. const other satırını silince çıktı undefined oluyor: kimse kullanmadığı için motor big’i çantaya koymadı. Nesneye giden yol kalmadı, çöp toplayıcı onu aldı. Başka motorlar başka seçebilir.
eval her şeyi çantaya koydurur.
Solucanın içinde doğrudan eval varsa motor hangi adın isteneceğini önceden bilemez. Odadaki her şeyi tutar. Aynı denemede return (code) => eval(code); yazınca big toplanmadı.
this çantada değil.
Normal fonksiyonda this nasıl çağrıldığına bakar, doğduğu yere değil. Ok fonksiyonunda (=>) this doğduğu yerden gelir. Çanta benzetmesi bu farkı göstermez.
Solucan seçmez.
"Solucan ihtiyacı olanı alır" dersek canlı bir seçim varmış gibi durur. Seçen yok. Kuralı dil koyar, gerisini motor kodu okuyarak çıkarır.
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