İçeriğe geç
academia.sh

Ders 10 / 19

Ağaç Sarsma

Çizgenin modül düzeyinden ad düzeyine indirilmesi; canlı dışa aktarımların yayılımı, yan etkili modülün elenememesi, paket bildirimindeki yan etki bildiriminin üç ayarı ve elemeyi engelleyen kaynak yazımları.

İçindekiler

Bir önceki ders parçaların hangi modülleri taşıyacağını belirledi. Belirlemediği şey, o modüllerin içinde nelerin taşınacağıdır. Bağımlılık çizgesi bir modülün gerekli olduğunu söyler; içindeki hangi adın kullanıldığını söylemez. Kuzey Yamaç Ölçüm İstasyonu uygulamasının birim modülü altı ad dışa aktarıyorsa ve bunların ikisi hiçbir yerden çağrılmıyorsa, çizge bunu göremez.

Ağaç sarsma (tree shaking), çizgeyi modül düzeyinden ad düzeyine indirip erişilemeyen dışa aktarımları çıktıdan düşürme işidir. Modüller, Araçlar ve Ekosistem kursundaki paketleyici dersi bu dönüşümün üç koşulunu saymıştı. Bu ders koşulları çalıştırarak sınar: birinin ihlal edildiği bir kaynak ağaç kurar, elemenin ne zaman durduğunu ölçer ve paketin bunu bildirimle nasıl mümkün kıldığını gösterir.

Ad Düzeyinde Canlılık

Çizgede düğümler modüller değil, modül#ad çiftleridir. Bir çift canlı ise çıktıda kalır; değilse düşer.

Yayılım giriş noktasından başlar. Giriş noktasının dışa aktarımları koşulsuz canlıdır; onları isteyen şey derlemenin dışındadır. Canlı bir dışa aktarımın gövdesinde geçen her ad da canlıdır: gövde bir içe aktarım adına dokunuyorsa, o adın kaynak modüldeki karşılığı canlanır. Yeniden dışa aktarımlar zinciri uzatır — toplayıcı bir dosyadaki export { a } from "./x.js" satırı, a istendiğinde x.js içindeki a adını canlandırır, ötekilere dokunmaz.

Dinamik içe aktarım bu zincirin bir yerinde belirsizlik yaratır. import() çağrısının sonucu bir ad alanı nesnesidir ve hangi adların kullanılacağı çağrı yerinden her zaman okunamaz. Temkinli davranış, dinamik hedefin bütün dışa aktarımlarını canlı saymaktır.

Kaynak Ağacın Gerçek Yüzeyi

Şimdiye kadarki kaynak ağaç yalnız kullanılan adları taşıyordu. Gerçek bir yardımcı modül kümesi böyle olmaz: tüketicilerinin hepsinin ihtiyaç duymadığı bir yüzey sunar, adlar tek bir toplayıcı dosyadan dağıtılır ve içlerinden biri en üst düzeyde iş yapar.

Aşağıdaki blok, Modül Paketleme ve Kod Bölme derslerinde kurulan kaynak/ ağacını sürdürür; giriş noktası oradan gelir ve o derslerin kurulum blokları önce çalıştırılmalıdır.

mkdir -p kaynak

cat > kaynak/birim.js <<'EOF'
export const SICAKLIK_ALT = -60;
export const SICAKLIK_UST = 60;
export const sinirdaMi = (c) => c >= SICAKLIK_ALT && c <= SICAKLIK_UST;
export const fahrenhayt = (c) => c * 9 / 5 + 32;
export const milimetreCiva = (hpa) => hpa * 0.75006157584566;
export const ruzgarSogumasi = (c, hiz) =>
  13.12 + 0.6215 * c - 11.37 * hiz ** 0.16 + 0.3965 * c * hiz ** 0.16;
EOF

cat > kaynak/tarih.js <<'EOF'
const iki = (s) => String(s).padStart(2, "0");
export const saatDakika = (d) => iki(new Date(d).getUTCHours()) + ":" + iki(new Date(d).getUTCMinutes());
export const gunAnahtari = (d) => new Date(d).toISOString().slice(0, 10);
export const haftaAnahtari = (d) => {
  const t = new Date(d);
  const gun = Math.floor((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 86400000);
  return t.getUTCFullYear() + "-H" + iki(Math.ceil((gun + 1) / 7));
};
EOF

cat > kaynak/tahmin.js <<'EOF'
import { SICAKLIK_ALT, SICAKLIK_UST } from "./birim.js";

const gecerli = (o) => o.sicaklik > SICAKLIK_ALT && o.sicaklik < SICAKLIK_UST;

export const egilim = (olcumler) => {
  const dizi = olcumler.filter(gecerli);
  if (dizi.length < 2) return 0;
  return (dizi.at(-1).sicaklik - dizi[0].sicaklik) / (dizi.length - 1);
};
EOF

cat > kaynak/kayit.js <<'EOF'
export const bicimciler = new Map();
export const kaydet = (ad, islev) => bicimciler.set(ad, islev);
EOF

cat > kaynak/olcek-kaydi.js <<'EOF'
import { kaydet } from "./kayit.js";
import { fahrenhayt } from "./birim.js";

kaydet("fahrenhayt", fahrenhayt);
EOF

cat > kaynak/arac.js <<'EOF'
import "./olcek-kaydi.js";
export { sinirdaMi, fahrenhayt, milimetreCiva, ruzgarSogumasi } from "./birim.js";
export { saatDakika, gunAnahtari, haftaAnahtari } from "./tarih.js";
export { egilim } from "./tahmin.js";
export { bicimciler } from "./kayit.js";
EOF

cat > kaynak/bicim.js <<'EOF'
import { saatDakika, sinirdaMi, fahrenhayt, bicimciler } from "./arac.js";

export function olcumSatiri(o) {
  const uyari = sinirdaMi(o.sicaklik) ? "" : " (sinir disi)";
  const ikinci = bicimciler.has("fahrenhayt")
    ? " / " + bicimciler.get("fahrenhayt")(o.sicaklik).toFixed(1) + " F" : "";
  return saatDakika(o.damga) + "  " + o.sicaklik.toFixed(1) + " C" + ikinci + "  %" + o.nem + uyari;
}
export const ikiliBirim = (o) => o.sicaklik.toFixed(1) + " C / " + fahrenhayt(o.sicaklik).toFixed(1) + " F";
EOF

cat > paket.json <<'EOF'
{
  "name": "kuzey-yamac",
  "type": "module",
  "sideEffects": ["./kaynak/olcek-kaydi.js"]
}
EOF

Üç yapı eklendi. Birim ve tarih modülleri kullanılmayan adlar taşıyor. Toplayıcı dosya adları tek yerden dağıtıyor ve içine bir de çıplak içe aktarım koyuyor. Ölçek kaydı modülü hiçbir şey dışa aktarmıyor; yalnız en üst düzeyde bir kayda yazıyor ve biçimlendirme modülü çıktısını o kayda bakarak değiştiriyor.

Çözümleyici

Aşağıdaki betik modülleri satır satır ayrıştırır, canlılığı yayar ve üç ayrı yan etki politikası altında çıktı boyutunu hesaplar.

// sarsma.mjs — ad duzeyinde canlilik cozumlemesi ve uc yan etki politikasi.
import { readFileSync } from "node:fs";
import { dirname, join, relative } from "node:path";

const GIRIS = "kaynak/giris-tarayici.js";
const BASLANGIC = /^(import\b|export\b|const\b|let\b|function\b|class\b)/;

function ayristir(yol) {
  const satirlar = readFileSync(yol, "utf8").split("\n");
  const coz = (b) => relative(".", join(dirname(yol), b));
  const iceAktarim = new Map();          // yerel ad -> { modul, dis ad }
  const ciplak = [];                     // yalniz yan etki icin ice aktarilan moduller
  const parca = [];                      // { ad, modul, dis, govde, disaAktarim }
  for (let i = 0; i < satirlar.length; i++) {
    const s = satirlar[i];
    let e;
    if ((e = s.match(/^import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/))) {
      for (const p of e[1].split(",").map((x) => x.trim()).filter(Boolean)) {
        const [dis, yerel = dis] = p.split(/\s+as\s+/);
        iceAktarim.set(yerel, { modul: coz(e[2]), dis });
      }
    } else if ((e = s.match(/^import\s*["']([^"']+)["']/))) {
      ciplak.push(coz(e[1]));
    } else if ((e = s.match(/^export\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/))) {
      for (const p of e[1].split(",").map((x) => x.trim()).filter(Boolean)) {
        parca.push({ ad: p, modul: coz(e[2]), dis: p, govde: "", disaAktarim: true,
          boyut: Buffer.byteLength(p) + 2 });
      }
    } else if ((e = s.match(/^(?:export\s+)?(?:const|let|function|async function|class)\s+(\w+)/))) {
      const govde = [s];
      while (i + 1 < satirlar.length && !BASLANGIC.test(satirlar[i + 1])) govde.push(satirlar[++i]);
      const metin = govde.join("\n").replace(/\s+$/, "");
      parca.push({ ad: e[1], govde: metin, disaAktarim: s.startsWith("export"),
        boyut: Buffer.byteLength(metin) + 1 });
    } else if (s.trim() && !s.trim().startsWith("//")) {
      parca.push({ ad: null, govde: s, disaAktarim: false, boyut: Buffer.byteLength(s) + 1 });
    }
  }
  return { iceAktarim, ciplak, parca, boyut: Buffer.byteLength(readFileSync(yol)) };
}

const modul = new Map();
const kuyruk = [GIRIS];
while (kuyruk.length > 0) {
  const yol = kuyruk.shift();
  if (modul.has(yol)) continue;
  const m = ayristir(yol);
  modul.set(yol, m);
  const dinamik = [...readFileSync(yol, "utf8").matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g)]
    .map(([, b]) => relative(".", join(dirname(yol), b)));
  kuyruk.push(...[...m.iceAktarim.values()].map((i) => i.modul), ...m.ciplak, ...dinamik,
    ...m.parca.filter((p) => p.modul).map((p) => p.modul));
  m.dinamik = dinamik;
}

function coz(politika) {                 // politika: "yok" | "yansiz" | "liste"
  const LISTE = JSON.parse(readFileSync("paket.json", "utf8")).sideEffects;
  const canli = new Set();               // "modul#ad"
  const tutulan = new Set();
  const is = [];

  function govdeyiTara(yol, govde) {
    const m = modul.get(yol);
    for (const [yerel, kaynak] of m.iceAktarim)
      if (new RegExp("\\b" + yerel + "\\b").test(govde)) is.push([kaynak.modul, kaynak.dis]);
    for (const p of m.parca)
      if (p.ad && new RegExp("\\b" + p.ad + "\\b").test(govde) && !govde.startsWith("export"))
        is.push([yol, p.ad]);
  }

  function tut(yol) {
    if (tutulan.has(yol)) return;
    tutulan.add(yol);
    const m = modul.get(yol);
    for (const p of m.parca) if (!p.disaAktarim) govdeyiTara(yol, p.govde);
    for (const c of m.ciplak) {
      const korunur = politika === "yok" || (politika === "liste" && LISTE.includes("./" + c));
      if (korunur) tut(c);
    }
    for (const d of m.dinamik) {
      tut(d);
      for (const p of modul.get(d).parca) if (p.disaAktarim) is.push([d, p.ad]);
    }
    if (politika === "yok") {
      for (const i of m.iceAktarim.values()) tut(i.modul);
      for (const p of m.parca) if (p.modul) tut(p.modul);
    }
  }

  function canlandir(yol, ad) {
    const anahtar = yol + "#" + ad;
    if (canli.has(anahtar)) return;
    canli.add(anahtar);
    tut(yol);
    const p = modul.get(yol).parca.find((x) => x.ad === ad && x.disaAktarim);
    if (!p) return;
    if (p.modul) is.push([p.modul, p.dis]);
    else govdeyiTara(yol, p.govde);
  }

  tut(GIRIS);
  for (const p of modul.get(GIRIS).parca) if (p.disaAktarim) is.push([GIRIS, p.ad]);
  while (is.length > 0) canlandir(...is.pop());

  let toplam = 0;
  const dusen = [], oluAd = [];
  for (const [yol, m] of modul) {
    if (!tutulan.has(yol)) { dusen.push(yol); continue; }
    let bayt = m.boyut;
    for (const p of m.parca)
      if (p.disaAktarim && !canli.has(yol + "#" + p.ad)) {
        bayt -= p.boyut;
        oluAd.push(yol.replace("kaynak/", "") + "#" + p.ad);
      }
    toplam += bayt;
  }
  return { toplam, dusen, oluAd };
}

const ham = [...modul.values()].reduce((t, m) => t + m.boyut, 0);
console.log("cizgedeki modul:", modul.size, " ham boyut:", ham, "B");
for (const politika of ["yok", "yansiz", "liste"]) {
  const r = coz(politika);
  console.log("-- sideEffects: " + politika + " --");
  console.log("  cikti boyutu : " + r.toplam + " B  (kazanc " + (ham - r.toplam) + " B)");
  console.log("  dusen modul  : " + (r.dusen.map((y) => y.replace("kaynak/", "")).join(", ") || "-"));
  console.log("  olu ad       : " + (r.oluAd.sort().join(", ") || "-"));
}
$ node sarsma.mjs
cizgedeki modul: 13  ham boyut: 4188 B
-- sideEffects: yok --
  cikti boyutu : 3546 B  (kazanc 642 B)
  dusen modul  : -
  olu ad       : arac.js#egilim, arac.js#gunAnahtari, arac.js#haftaAnahtari, arac.js#milimetreCiva, arac.js#ruzgarSogumasi, birim.js#milimetreCiva, birim.js#ruzgarSogumasi, tahmin.js#egilim, tarih.js#haftaAnahtari
-- sideEffects: yansiz --
  cikti boyutu : 3231 B  (kazanc 957 B)
  dusen modul  : olcek-kaydi.js, tahmin.js
  olu ad       : arac.js#egilim, arac.js#gunAnahtari, arac.js#haftaAnahtari, arac.js#milimetreCiva, arac.js#ruzgarSogumasi, birim.js#milimetreCiva, birim.js#ruzgarSogumasi, kayit.js#kaydet, tarih.js#haftaAnahtari
-- sideEffects: liste --
  cikti boyutu : 3408 B  (kazanc 780 B)
  dusen modul  : tahmin.js
  olu ad       : arac.js#egilim, arac.js#gunAnahtari, arac.js#haftaAnahtari, arac.js#milimetreCiva, arac.js#ruzgarSogumasi, birim.js#milimetreCiva, birim.js#ruzgarSogumasi, tarih.js#haftaAnahtari

Üç politika, aynı kaynak ağaçtan üç ayrı çıktı üretiyor. Ölü ad listesi hepsinde ortak bir çekirdek taşıyor: milimetre cıva, rüzgâr soğuması, hafta anahtarı ve eğilim hesabı hiçbir canlı gövdede geçmiyor. Toplayıcı dosyadaki yeniden dışa aktarım satırları da onlarla birlikte düşüyor — adı taşıyan satır, adın kendisi ölünce anlamsızdır.

Yan Etki Engeli

Politikalar arasındaki farkı yaratan tek dosya ölçek kaydıdır. Bu modülün dışa aktarımı yoktur; çizgeye yalnız çıplak bir içe aktarımla girer ve en üst düzeyinde bir işlev çağırır. Ad düzeyi çözümlemesi burada tıkanır: canlanacak bir ad yoktur, ama modülün çalışması bir başkasının davranışını değiştirir.

Sağlaması bir çalıştırmayla yapılır. Aşağıdaki blok, elemenin uygulandığı bir kopya üretir ve aynı çağrıyı iki çıktı üzerinde koşturur. sed -i seçeneği BSD ve GNU sürümlerinde farklı davranır; yedek uzantısı vererek her ikisinde de aynı sonuç alınır.

#!/usr/bin/env bash
# Yan etkili modul elenmis bir kopya uretilir, ayni cagri iki cikti uzerinde kosturulur.
rm -rf elenmis && mkdir elenmis && cp kaynak/*.js elenmis/
sed -i.yedek -e '/^import ".\/olcek-kaydi.js";$/d' -e '/from ".\/tahmin.js"/d' elenmis/arac.js
rm -f elenmis/*.yedek elenmis/olcek-kaydi.js elenmis/tahmin.js

OLCUM='{ damga: "2026-02-11T06:00:00Z", sicaklik: -4.2, nem: 72 }'
node --input-type=module -e "
import { olcumSatiri } from './kaynak/bicim.js';
console.log('korunmus:', olcumSatiri(${OLCUM}));
"
node --input-type=module -e "
import { olcumSatiri } from './elenmis/bicim.js';
console.log('elenmis :', olcumSatiri(${OLCUM}));
"
korunmus: 06:00  -4.2 C / 24.4 F  %72
elenmis : 06:00  -4.2 C  %72

İkinci satırda Fahrenhayt sütunu yoktur. Eleme bir hata üretmedi, bir istisna fırlatmadı; çıktıyı sessizce değiştirdi. Bu, ağaç sarsmanın en tehlikeli kusur biçimidir: derleme başarılı görünür, testlerin kapsamadığı bir davranış kaybolur.

Bu yüzden araç, yan etki durumu bildirilmemiş bir modülü kendiliğinden düşürmez. Bilgiyi paketin bildirimindeki sideEffects alanı verir ve üç ayarı vardır.

Alan yoksa araç temkinli davranır: hiçbir modülü tümüyle düşürmez, yalnız ölü dışa aktarım bildirimlerini siler. Çıktı 3546 bayttır ve doğrudur, ama tahmin modülü içi boşalmış hâlde çıktıda kalır.

Alan false ise yazar, paketteki hiçbir modülün en üst düzeyde iş yapmadığına söz vermiş olur. Araç ölçek kaydını ve tahmin modülünü düşürür, kayıt modülündeki kaydet adı da ölür. Çıktı 3231 bayta iner — ve yukarıdaki ikinci satırın davranışını üretir. Bu paket için false bir yalandır.

Alan bir liste ise yazar hangi dosyaların yan etkili olduğunu tek tek söyler. Ölçek kaydı korunur, tahmin modülü düşer. Çıktı 3408 bayttır: doğruluğu bozmadan elde edilebilen en küçük çıktı.

Alan bu yüzden bir ayar değil, bir taahhüttür. Yanlış doldurulduğunda araç yanlış eleme yapar ve arıza, elemenin yapıldığı yerde değil, kaybolan davranışın kullanıldığı yerde görünür.

Elemeyi Durduran Öteki Yazımlar

Yan etki tek engel değildir. Aşağıdaki yazımların hepsi ad düzeyi izlemeyi kırar ve aracı modülün tamamını tutmaya zorlar.

Ad alanı nesnesinin bütün olarak dolaştırılması. import * as arac from "./arac.js" yazımından sonra nesne bir işleve verilirse, hangi adların okunacağı statik olarak bilinemez. Adlar tek tek içe aktarıldığında bu belirsizlik doğmaz.

Hesaplanmış erişim. arac[secilenAd] biçiminde bir okuma, çalışma zamanında belirlenen bir ada karşılık gelir. Böyle bir erişimi gören araç, o modüldeki bütün adları canlı saymak zorundadır.

Çağrı tabanlı modül biçimi. CommonJS’te dışa aktarım nesnesi çalışma zamanında değiştirilebilir; hangi adın var olduğu değerlendirme sonuna kadar kesin değildir. Statik ayrıştırmayla elde edilen liste eksik olabileceği için eleme uygulanmaz.

Değerlendirme anında iş yapan bildirimler. En üst düzeydeki bir çağrının sonucu bir sabite bağlanıyorsa, o çağrının yan etkisi olup olmadığı bilinmez. Araçlar bu belirsizliği kaynağa konan saflık açıklamalarıyla aşar; açıklama da bildirim alanı gibi bir taahhüttür.

Toplayıcı dosyalar. Toplayıcının kendisi zararsızdır — örnekteki toplayıcının ölü yeniden dışa aktarımları düştü. Zararlı olan, toplayıcının yan etkili bir modülü çıplak içe aktarımla çizgeye sokmasıdır: o tek satır, toplayıcıyı içe aktaran her tüketiciye o modülü taşır.

Bu liste, kütüphane yazarına dört maddelik somut bir sorumluluk bırakır: en üst düzeyde iş yapmamak, adları doğrudan dışa aktarmak, yan etkili dosyaları bildirimde listelemek ve toplayıcıya çıplak içe aktarım koymamak.

Özet

  • Ağaç sarsma çizgeyi modül#ad düzeyine indirir; canlılık giriş noktasının dışa aktarımlarından başlayıp gövdelerde geçen adlar üzerinden yayılır.
  • Yeniden dışa aktarımlar canlılığı ada göre iletir; dinamik içe aktarım hedefinin bütün dışa aktarımları temkinli olarak canlı sayılır.
  • Dışa aktarımı olmayıp en üst düzeyde iş yapan bir modül ad düzeyi çözümlemesiyle elenemez; elenirse arıza sessizdir — örnekte Fahrenhayt sütunu hatasız biçimde kaybolur.
  • Paket bildirimindeki yan etki alanı üç sonuç üretir: alan yoksa 3546 bayt ve modül düşmez, false ise 3231 bayt ve davranış bozulur, doğru liste ise 3408 bayt ve davranış korunur.
  • Ad alanı nesnesinin dolaştırılması, hesaplanmış erişim, çağrı tabanlı modül biçimi ve değerlendirme anında iş yapan bildirimler elemeyi durdurur.

Sonraki Adım

Çizge şimdiye kadar yalnız betik dosyalarından kuruldu. Oysa bir arayüzün çıktısı yalnız koddan oluşmaz: ölçüm listesinin biçem dosyası, istasyon simgesi, arayüzün yazı tipi ve tarayıcıya olduğu gibi kopyalanacak birkaç statik dosya da çıktıya girer. Bunların bir kısmı koddan içe aktarılır ve çizgenin düğümü hâline gelir; bir kısmı hiç içe aktarılmaz ama yine de yayımlanır. Sonraki ders bu ikinci akışı ele alır: varlıkların boru hattından geçirilmesi, küçük olanların gövdeye gömülmesi, büyük olanların ayrı dosya olarak yazılması ve kaynaktaki adların çıktıdaki adlara bağlanması.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat