İçeriğe geç
academia.sh

Ders 09 / 25

Türetilmiş Değerler

Başka değerlerden hesaplanabilen sonucun durumda tutulmaması; tek doğruluk kaynağı ölçütü, türetmeyi duruma yazmanın bedelleri, bellemenin yeniden hesap sayısına etkisi, kimlik kararlılığının aşağı akışta ürettiği fark ve bellemenin kendi maliyeti.

İçindekiler

Önceki ders yan etki kaydını dış dünyayla eşzamanlama aracı olarak kurdu ve son cümlesinde bir uyarı bıraktı: en sık kurulduğu yer dış dünya değildir. Süzgeç panelinde seçili tür değiştiğinde ölçüm listesini süzen, sonucu ayrı bir yuvaya yazan ve böylece ikinci bir oluşturma tetikleyen kod yaygındır. Bu ders o kalıbın neden yanlış olduğunu, yerine ne konduğunu ve hesabın gerçekten pahalı olduğu durumda ne yapıldığını kurar.

Ayrım tek bir soruya iner: bu değer, elimdeki başka değerlerden hesaplanabiliyor mu? Hesaplanabiliyorsa o bir durum değil, bir türetilmiş değerdir (derived value).

Tek Doğruluk Kaynağı

Ölçüm istasyonu sayfasında beş değer dolaşır: sunucudan gelen ölçüm listesi, süzgeç panelinde seçili tür, tablonun sıralandığı sütun, süzülmüş ölçüm listesi ve tablonun altındaki “42 ölçümden 17’si gösteriliyor” sayısı.

İlk üçü durumdur; hiçbiri diğerlerinden hesaplanamaz. Son ikisi ise tümüyle ilk üçünden çıkar: süzülmüş liste ölçümlerin ve seçili türün bir işlevidir, sayı da süzülmüş listenin uzunluğudur. Bu iki değeri ayrı yuvalarda tutmak, aynı bilgiyi iki yerde saklamak olur.

Aynı bilginin tek bir yerde tutulmasına tek doğruluk kaynağı (single source of truth) denir. Ölçüt sadedir: bir değeri saklamadan önce onu hesaplayıp hesaplayamayacağın sorulur. Hesaplanabiliyorsa saklanmaz.

Bu ölçütün gizli bir sonucu vardır. Süzülmüş liste durumda tutulmadığında, ölçüm listesine yeni bir satır eklendiğinde süzülmüş listeyi güncellemeyi unutmak olanaksızdır; süzülmüş liste zaten yoktur, her oluşturmada yeniden hesaplanır. Türetilmiş değeri saklayan tasarımda ise güncelleme sorumluluğu programcıya geçer ve unutulan her yol bir tutarsızlık üretir.

Türetmeyi Duruma Yazmanın Üç Bedeli

Türetilmiş bir değeri bir yan etkiyle hesaplayıp duruma yazan kalıp üç ayrı bedel öder.

Fazladan oluşturma. Sıra şudur: kullanıcı türü değiştirir, durum güncellenir, gövde çalışır, ağaç belgeye yazılır, yan etki çalışır, süzülmüş listeyi duruma yazar, gövde yeniden çalışır, ağaç yeniden yazılır. Tek bir tıklama iki tam tur üretir.

Tutarsız ara kare. Bu iki turun arasında ekranda bir kare vardır ve o karede seçili tür yenidir ama liste eskidir. Kullanıcı “sıcaklık” seçmişken bir an nem ölçümlerini görür. Bu kare kısa olduğu için gözden kaçar; yavaş bir aygıtta kaçmaz.

Ayrışma. Ölçüm listesi başka bir yoldan da değişebilir — sunucudan yeni veri gelir, bir satır silinir. Yan etkinin bağımlılık listesi bu yolu kapsamıyorsa süzülmüş liste eski kalır ve iki kaynak kalıcı olarak ayrışır.

Doğru biçim, hesabı gövdenin içine koymaktır. Gövde her oluşturmada süzülmüş listeyi yeniden hesaplar; ayrı yuva, ayrı oluşturma, ara kare ve ayrışma birden ortadan kalkar. Kalan tek soru hesabın maliyetidir.

Belleme

Süzme ve sıralama, birkaç yüz satır için ölçülemeyecek kadar ucuzdur. Onbinlerce satır, her satır için bir tarih ayrıştırma ya da bir birim dönüşümü söz konusu olduğunda hesap görünür hale gelir. Bu durumda hesabın kendisi değil, gereksiz tekrarı hedef alınır.

Türetilmiş değerin, girdileri değişmediği sürece bir önceki sonucun saklanıp döndürülmesine belleme (memoization) denir. Programlama Temelleri kursunda özyinelemeli hesapları hızlandırmak için “not alma” adıyla tanıtılan tekniğin aynısıdır; buradaki fark, saklanan girdinin bir bağımlılık listesi olmasıdır.

// belleme.mjs — turetilmis degerin bellenmesi ve yeniden hesap sayisi
const OLCUMLER = [
  { kod: "KY-01", tur: "sicaklik", deger: -4.2 },
  { kod: "KY-02", tur: "nem", deger: 68 },
  { kod: "KY-03", tur: "sicaklik", deger: 1.5 },
  { kod: "KY-04", tur: "ruzgar", deger: 12.4 },
  { kod: "KY-05", tur: "sicaklik", deger: -0.8 },
];

let hesap = 0;
function suzVeSirala(olcumler, tur) {
  hesap++;
  return olcumler
    .filter((o) => tur === "hepsi" || o.tur === tur)
    .sort((a, b) => a.deger - b.deger)
    .map((o) => o.kod);
}

function bellenmis(yuvalar, i, hesapla, bagimliliklar) {
  const y = yuvalar[i];
  const ayni = y && y.b.length === bagimliliklar.length
    && y.b.every((v, k) => Object.is(v, bagimliliklar[k]));
  if (ayni) return y.d;
  const d = hesapla();
  yuvalar[i] = { b: bagimliliklar, d };
  return d;
}

// Turu degistirmeyen olusturmalar da vardir: panel acilip kapanmasi durumu degistirir.
const TURLAR = [
  ["hepsi", false], ["hepsi", true], ["hepsi", false],
  ["sicaklik", false], ["sicaklik", true], ["nem", true],
];

for (const belle of [false, true]) {
  hesap = 0;
  const yuvalar = [];
  console.log(belle ? "bellenmis:" : "her olusturmada hesaplanan:");
  for (const [tur, acik] of TURLAR) {
    const liste = belle
      ? bellenmis(yuvalar, 0, () => suzVeSirala(OLCUMLER, tur), [OLCUMLER, tur])
      : suzVeSirala(OLCUMLER, tur);
    console.log(`  tur=${tur.padEnd(8)} panel=${String(acik).padEnd(5)} liste=${liste.join(",")}`);
  }
  console.log(`  6 olusturmada ${hesap} hesap`);
}
her olusturmada hesaplanan:
  tur=hepsi    panel=false liste=KY-01,KY-05,KY-03,KY-04,KY-02
  tur=hepsi    panel=true  liste=KY-01,KY-05,KY-03,KY-04,KY-02
  tur=hepsi    panel=false liste=KY-01,KY-05,KY-03,KY-04,KY-02
  tur=sicaklik panel=false liste=KY-01,KY-05,KY-03
  tur=sicaklik panel=true  liste=KY-01,KY-05,KY-03
  tur=nem      panel=true  liste=KY-02
  6 olusturmada 6 hesap
bellenmis:
  tur=hepsi    panel=false liste=KY-01,KY-05,KY-03,KY-04,KY-02
  tur=hepsi    panel=true  liste=KY-01,KY-05,KY-03,KY-04,KY-02
  tur=hepsi    panel=false liste=KY-01,KY-05,KY-03,KY-04,KY-02
  tur=sicaklik panel=false liste=KY-01,KY-05,KY-03
  tur=sicaklik panel=true  liste=KY-01,KY-05,KY-03
  tur=nem      panel=true  liste=KY-02
  6 olusturmada 3 hesap

İki bölümün listeleri satır satır aynıdır; belleme sonucu değiştirmez, yalnız üretim sayısını değiştirir. Altı oluşturmanın üçünde tür bir öncekiyle aynıdır — panelin açılıp kapanması bir oluşturma üretir ama süzgeci ilgilendirmez — ve bellenmiş biçim bu üç turda saklanan sonucu döndürmüştür.

Belleme yuvası tek bir girdi tutar. Son turdan sonra yeniden “hepsi” seçilseydi hesap baştan yapılırdı, çünkü aradaki turlar yuvayı doldurmuştur. Bu bir eksiklik değil bilinçli bir sınırdır: sınırsız bir önbellek her ayrı girdi bileşimi için bellek tutar ve tahliye politikası gerektirir. Bir oluşturmadan diğerine geçen tek adımlık bir pencere, tipik kullanımı zaten karşılar.

Kimlik Kararlılığı

Bellemenin ikinci gerekçesi hızdan değil kimlikten gelir ve daha az bilinir. Türetilmiş değer bir dizi ya da nesneyse, her oluşturmada yeniden üretilen sonucun içeriği aynı, kimliği farklıdır. Bu fark aşağıya doğru yayılır: sonucu alan bileşenin bağımlılık listesi her oluşturmada değişmiş görünür.

// kimlik.mjs — turetilmis degerin kimliginin asagi akista etkisi
const OLCUMLER = [
  { kod: "KY-01", tur: "sicaklik" },
  { kod: "KY-02", tur: "nem" },
  { kod: "KY-03", tur: "sicaklik" },
];

const sicakOlanlar = (kaynak) => kaynak.filter((o) => o.tur === "sicaklik");

function bellenmis(yuvalar, i, hesapla, bag) {
  const y = yuvalar[i];
  if (y && y.b.length === bag.length && y.b.every((v, k) => Object.is(v, bag[k]))) return y.d;
  const d = hesapla();
  yuvalar[i] = { b: bag, d };
  return d;
}

for (const belle of [false, true]) {
  const yuvalar = [];
  let oncekiListe = null;   // cocuk bilesenin bagimlilik listesinde tuttugu deger
  let etkiSayisi = 0;
  console.log(belle ? "bellenmis turetme:" : "her olusturmada yeniden turetme:");

  for (let n = 1; n <= 4; n++) {          // dort olusturma, girdiler hic degismiyor
    const liste = belle
      ? bellenmis(yuvalar, 0, () => sicakOlanlar(OLCUMLER), [OLCUMLER])
      : sicakOlanlar(OLCUMLER);
    const ayniNesne = Object.is(liste, oncekiListe);
    const derinAyni = oncekiListe !== null
      && liste.length === oncekiListe.length
      && liste.every((o, k) => o === oncekiListe[k]);
    if (!ayniNesne) etkiSayisi++;         // cocugun yan etkisi yeniden calisir
    console.log(
      `  olusturma ${n}: ayni nesne=${String(ayniNesne).padEnd(5)}`,
      `icerik ayni=${oncekiListe === null ? "-    " : String(derinAyni).padEnd(5)}`,
      `cocuk etkisi calisti=${!ayniNesne}`,
    );
    oncekiListe = liste;
  }
  console.log(`  4 olusturmada cocuk etkisi ${etkiSayisi} kez calisti`);
}
her olusturmada yeniden turetme:
  olusturma 1: ayni nesne=false icerik ayni=-     cocuk etkisi calisti=true
  olusturma 2: ayni nesne=false icerik ayni=true  cocuk etkisi calisti=true
  olusturma 3: ayni nesne=false icerik ayni=true  cocuk etkisi calisti=true
  olusturma 4: ayni nesne=false icerik ayni=true  cocuk etkisi calisti=true
  4 olusturmada cocuk etkisi 4 kez calisti
bellenmis turetme:
  olusturma 1: ayni nesne=false icerik ayni=-     cocuk etkisi calisti=true
  olusturma 2: ayni nesne=true  icerik ayni=true  cocuk etkisi calisti=false
  olusturma 3: ayni nesne=true  icerik ayni=true  cocuk etkisi calisti=false
  olusturma 4: ayni nesne=true  icerik ayni=true  cocuk etkisi calisti=false
  4 olusturmada cocuk etkisi 1 kez calisti

Dört oluşturmanın hiçbirinde girdi değişmemiştir. Bellenmemiş biçimde “içerik aynı” sütunu her satırda doğru, “aynı nesne” sütunu her satırda yanlıştır: liste her seferinde eşdeğer ama farklı bir dizidir. Aşağıdaki bileşenin yan etkisi bunu bir değişiklik sayar ve dört kez çalışır. Bellenmiş biçimde bir kez çalışır.

Buna kimlik kararlılığı (referential stability) denir ve türetilmiş değer bir bileşen sınırından geçiyorsa bellemenin asıl gerekçesidir. Karşılaştırmanın kimlik karşılaştırması olması bir tasarım tercihidir: derin karşılaştırma büyük yapılarda hesabın kendisinden pahalıya gelir. Bedeli, kimliği korumanın çağıranın sorumluluğuna geçmesidir.

Aynı gerekçe işlevler için de geçerlidir. Gövde içinde yazılan bir geri çağırım her oluşturmada yeni bir işlev nesnesidir; aşağıya verildiğinde her seferinde değişmiş görünür. İşlevi bellemek, dizi bellemekle aynı sorunu çözer.

Bellemenin Kendi Maliyeti

Belleme bedava değildir. Her bellenmiş değer bir yuva tutar, her oluşturmada bir liste karşılaştırması yapılır ve saklanan sonuç bellekte kalır. Süzülmüş üç ögelik bir dizi için bu maliyet hesabın kendisinden büyüktür.

Üç ölçüt karar için yeterlidir. Hesap gerçekten pahalıysa — ölçülmüş, tahmin edilmemiş — bellenir. Sonuç bir bileşen sınırından geçiyor ve aşağıda bir bağımlılık listesine giriyorsa bellenir. İkisi de yoksa bellenmez.

Ölçmeden belleme, okunması zor bir gövde ve ölçülemeyen bir kazanç üretir. Tarayıcı ve Web Platformu kursundaki başarım kaydı, hangi hesabın gerçekten pahalı olduğunu söyleyen tek kaynaktır; bağımlılık listeleri o kayda bakılarak eklenir.

Özet

  • Başka değerlerden hesaplanabilen bir değer durum değildir; saklanmaz, her oluşturmada türetilir.
  • Türetmeyi bir yan etkiyle duruma yazmak fazladan bir oluşturma, tutarsız bir ara kare ve kaynakların ayrışma riskini üretir.
  • Belleme, bağımlılık listesi değişmediği sürece bir önceki sonucu döndürür; sonucu değiştirmez, yeniden hesap sayısını azaltır.
  • Belleme yuvası tek adımlık bir pencere tutar; eski bir girdiye dönmek hesabı yeniden yaptırır.
  • Kimlik kararlılığı, türetilmiş bir dizi ya da işlev bileşen sınırından geçtiğinde aşağıdaki bağımlılık listelerinin gereksiz yere değişmesini önler.
  • Belleme yuva, karşılaştırma ve bellek maliyeti taşır; ölçülmemiş bir hesap için eklenmez.

Sonraki Adım

Türetilmiş değerler her oluşturmada yeniden hesaplanır, durum ise oluşturmalar arasında yaşar ve değiştiğinde yeni bir oluşturma başlatır. Bu iki kategori arasında bir boşluk vardır. Bir zamanlayıcının kimliği, bir isteğin sıra numarası, bir ögenin bir önceki konumu, tabloyu kaydıran düğümün kendisi — hepsinin oluşturmalar arasında yaşaması gerekir, ama hiçbirinin değişmesi ekranda bir değişiklik üretmez. Bunları duruma yazmak her değişimde gereksiz bir oluşturma tetikler; yerel değişkende tutmak ise ikinci oluşturmada kaybetmek demektir. Sonraki ders bu üçüncü kategoriyi tanımlar.

İ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