İçeriğe geç
academia.sh

Ders 07 / 16

Boş Değer Geçirmemek

Boşluğun sözleşmede görünmesinin ölçülmesi: boş değer döndüren bir çağrı zincirinde hatanın kaç çağrı sonra ortaya çıktığı, boşluğu sonuç türünde gösteren sürümde aynı mesafenin sıfıra inmesi ve boş değeri argüman olarak geçiren imzaların boş toplamayla değiştirilmesi.

İçindekiler

Buraya kadarki kararlar kodun iç yapısı üzerineydi: adlar, soyutlama düzeyleri, biçim, dallanma ve seçenek yapısı. Bir fonksiyonun dışarıya söylediği şey henüz ele alınmadı. Önceki dersteki tarife fonksiyonu bilinmeyen bir tür için hata atıyordu; aynı sorunun yaygın çözümü boş bir değer döndürmektir.

Boş değer döndüren bir imza çağırana hiçbir şey söylemez. tasiyiciBul(bolgeKodu) imzasına bakan kişi sonucun her zaman bir taşıyıcı olduğunu varsayar; boş dönebileceğini ancak gerçekleştirimi okuyarak öğrenir. Varsayım yanlış çıktığında hata, boşluğun üretildiği yerde değil kullanıldığı yerde ortaya çıkar. Aradaki mesafe ölçülebilir.

Boş Değerin Yolculuğu

Yönlendirme modülü bir gönderi için taşıyıcı seçer, aktarma sayısını belirler ve teslim tarihini hesaplar. Her fonksiyon çağrıldığında adını bir ize yazıyor; bu iz ölçüm için kullanılacak.

mkdir -p bos sozlesme
// bos/rota.mjs — tasiyici bulunamazsa bos deger dondurulur
export const IZ = [];
const iz = (ad) => IZ.push(ad);

const TASIYICILAR = { 1: { ad: "sehirici", teslimGunu: 1 }, 2: { ad: "bolgesel", teslimGunu: 3 },
  3: { ad: "uzak", teslimGunu: 5 } };

export function tasiyiciBul(bolgeKodu) {
  iz("tasiyiciBul");
  return TASIYICILAR[bolgeKodu] ?? null;
}

export function rotaSec(gonderi) {
  iz("rotaSec");
  return { tasiyici: tasiyiciBul(gonderi.bolgeKodu), aktarmaSayisi: gonderi.bolgeKodu > 2 ? 1 : 0 };
}

export function aktarmaGecikmesi(rota) {
  iz("aktarmaGecikmesi");
  return rota.aktarmaSayisi * 2;
}

export function tasiyiciGecikmesi(rota) {
  iz("tasiyiciGecikmesi");
  return rota.tasiyici.teslimGunu;
}

export function teslimTarihi(rota, bugun) {
  iz("teslimTarihi");
  return bugun + aktarmaGecikmesi(rota) + tasiyiciGecikmesi(rota);
}

export function teslimatPlani(gonderi, bugun) {
  iz("teslimatPlani");
  const rota = rotaSec(gonderi);
  return { rota, tarih: teslimTarihi(rota, bugun) };
}

export function siparisiIsle(gonderi, bugun) {
  iz("siparisiIsle");
  return `${gonderi.kod}: ${teslimatPlani(gonderi, bugun).tarih}. gun`;
}

Tanımlı olmayan bir bölge kodu geldiğinde tasiyiciBul boş döner, rotaSec bu boşluğu bir alan olarak paketler ve döndürür, teslimatPlani rotayı alır, teslimTarihi iki gecikme fonksiyonunu çağırır ve hata bunlardan ikincisinde ortaya çıkar. Boşluk, üretildiği yerden çıkıp bir veri yapısının içine yerleşerek yolculuk eder.

Sözleşmede Görünen Boşluk

İkinci sürümde tek bir şey değişiyor: taşıyıcı arayan fonksiyonun sonuç türü. Artık ya bir taşıyıcı ya da bulunamama bilgisi dönüyor ve ikisi birbirinden ayırt edilebiliyor. Çağıran taraf sonucu kullanmadan önce hangisi olduğuna bakmak zorunda.

// sozlesme/rota.mjs — boslugun sonuc turunde gorunmesi
export const IZ = [];
const iz = (ad) => IZ.push(ad);

const TASIYICILAR = { 1: { ad: "sehirici", teslimGunu: 1 }, 2: { ad: "bolgesel", teslimGunu: 3 },
  3: { ad: "uzak", teslimGunu: 5 } };

// Sonuc iki bicimden biridir: { bulundu: true, tasiyici } ya da { bulundu: false, bolgeKodu }.
export function tasiyiciSec(bolgeKodu) {
  iz("tasiyiciSec");
  const tasiyici = TASIYICILAR[bolgeKodu];
  return tasiyici === undefined ? { bulundu: false, bolgeKodu } : { bulundu: true, tasiyici };
}

export function rotaSec(gonderi) {
  iz("rotaSec");
  const sonuc = tasiyiciSec(gonderi.bolgeKodu);
  if (sonuc.bulundu === false) {
    throw new RangeError(`bolge ${sonuc.bolgeKodu} icin tasiyici tanimli degil`);
  }
  return { tasiyici: sonuc.tasiyici, aktarmaSayisi: gonderi.bolgeKodu > 2 ? 1 : 0 };
}

export function aktarmaGecikmesi(rota) {
  iz("aktarmaGecikmesi");
  return rota.aktarmaSayisi * 2;
}

export function tasiyiciGecikmesi(rota) {
  iz("tasiyiciGecikmesi");
  return rota.tasiyici.teslimGunu;
}

export function teslimTarihi(rota, bugun) {
  iz("teslimTarihi");
  return bugun + aktarmaGecikmesi(rota) + tasiyiciGecikmesi(rota);
}

export function teslimatPlani(gonderi, bugun) {
  iz("teslimatPlani");
  const rota = rotaSec(gonderi);
  return { rota, tarih: teslimTarihi(rota, bugun) };
}

export function siparisiIsle(gonderi, bugun) {
  iz("siparisiIsle");
  return `${gonderi.kod}: ${teslimatPlani(gonderi, bugun).tarih}. gun`;
}

Aşağı akış — gecikme hesapları, teslim tarihi, plan ve özet — iki sürümde birebir aynı. Değişen tek şey boşluğun nerede karşılandığıdır.

Ölçüm

Ölçer aynı geçersiz gönderiyi iki sürüme de verir ve üç şeyi kaydeder: çağrı izi, boşluğun üretildiği çağrı ile hatanın görüldüğü çağrı arasındaki mesafe, hata anındaki yığıt izinde (stack trace) modülün hangi fonksiyonlarının bulunduğu.

// olc.mjs — bos degerin uretildigi cagri ile hatanin goruldugu cagri arasindaki mesafe
import { IZ as bosIz, siparisiIsle as bosIsle } from "./bos/rota.mjs";
import { IZ as sozIz, siparisiIsle as sozIsle } from "./sozlesme/rota.mjs";

const gonderi = { kod: "GN-4172", bolgeKodu: 9 };

function olc(baslik, isle, iz, ureten) {
  console.log(baslik);
  try {
    console.log(`  sonuc: ${isle(gonderi, 12)}`);
  } catch (hata) {
    const cerceve = hata.stack.split("\n").slice(1)
      .map((s) => s.match(/at (\w+)/)?.[1]).filter((a) => iz.includes(a));
    console.log(`  cagri izi: ${iz.join(" -> ")}`);
    console.log(`  boslugu ureten cagri #${iz.indexOf(ureten)}, hatanin goruldugu cagri #${iz.length - 1},` +
      ` aradaki cagri = ${iz.length - 1 - iz.indexOf(ureten)}`);
    console.log(`  yigit izindeki modul cerceveleri: ${cerceve.join(", ")}`);
    console.log(`  hata: ${hata.constructor.name}: ${hata.message}`);
  }
}

olc("bos deger donduren surum", bosIsle, bosIz, "tasiyiciBul");
olc("sozlesmede gorunen surum", sozIsle, sozIz, "tasiyiciSec");
bos deger donduren surum
  cagri izi: siparisiIsle -> teslimatPlani -> rotaSec -> tasiyiciBul -> teslimTarihi -> aktarmaGecikmesi -> tasiyiciGecikmesi
  boslugu ureten cagri #3, hatanin goruldugu cagri #6, aradaki cagri = 3
  yigit izindeki modul cerceveleri: tasiyiciGecikmesi, teslimTarihi, teslimatPlani, siparisiIsle
  hata: TypeError: Cannot read properties of null (reading 'teslimGunu')
sozlesmede gorunen surum
  cagri izi: siparisiIsle -> teslimatPlani -> rotaSec -> tasiyiciSec
  boslugu ureten cagri #3, hatanin goruldugu cagri #3, aradaki cagri = 0
  yigit izindeki modul cerceveleri: rotaSec, teslimatPlani, siparisiIsle
  hata: RangeError: bolge 9 icin tasiyici tanimli degil

Mesafe üçten sıfıra indi. Sayının anlamı, hatayı arayan kişinin kat etmesi gereken yoldur: boş değer döndüren sürümde hata tasiyiciGecikmesi içinde görünüyor ve o fonksiyonda hiçbir kusur yok. Yığıt izi de yardım etmiyor, çünkü boşluğu üreten tasiyiciBul çoktan dönmüş; izde adı bile geçmiyor. Arayan kişi, listelenen dört çerçevenin hiçbirinde bulunmayan bir nedeni bulmak zorunda.

Hata iletileri de ölçümün bir parçası. Birincisi bir tür hatasıdır ve alan hakkında hiçbir şey söylemez; ikincisi bölge kodunu ve eksik olan şeyi adıyla söyler.

Boş Değeri Geçirmek

Aynı sorunun ters yönü, boş değerin argüman olarak geçirilmesidir. İndirim hesabı buna örnek: indirim yoksa boş değer geçmek, imzada iki anlam üretir — “indirim nesnesi” ve “indirim yok”.

// bos/indirim.mjs — indirim ya da kampanya yoksa bos deger geciriliyor
export function ucretHesapla(temelUcret, indirim, kampanya) {
  const oran = indirim === null ? 0 : indirim.oran;
  const ek = kampanya === null ? 0 : kampanya.ekIndirim;
  return temelUcret * (1 - oran - ek);
}

export const ORNEKLER = [
  ["indirimsiz", ucretHesapla(100, null, null)],
  ["sozlesmeli", ucretHesapla(100, { oran: 0.15 }, null)],
  ["iki indirim", ucretHesapla(100, { oran: 0.15 }, { ekIndirim: 0.05 })],
];

Boşluğun doğal karşılığı bir boş toplamadır. İndirim sayısı sıfır, bir ya da daha çok olabiliyorsa parametre tekil bir nesne değil bir listedir; indirim yokluğu boş liste olarak temsil edilir ve aynı işlemleri destekler.

// sozlesme/indirim.mjs — boslugu bos dizi tasir, imza bos deger kabul etmez
export function ucretHesapla(temelUcret, indirimler) {
  const toplam = indirimler.reduce((t, i) => t + i.oran, 0);
  return temelUcret * (1 - toplam);
}

export const ORNEKLER = [
  ["indirimsiz", ucretHesapla(100, [])],
  ["sozlesmeli", ucretHesapla(100, [{ oran: 0.15 }])],
  ["iki indirim", ucretHesapla(100, [{ oran: 0.15 }, { oran: 0.05 }])],
];
// indirim-olc.mjs — iki imzanin ayni sonuclari verdigi ve bos deger sayisi
import { ORNEKLER as bosOrnek } from "./bos/indirim.mjs";
import { ORNEKLER as sozOrnek } from "./sozlesme/indirim.mjs";

for (const [i, [ad, deger]] of bosOrnek.entries()) {
  const oteki = sozOrnek[i][1];
  console.log(`${ad.padEnd(12)} bos=${deger.toFixed(2)}  sozlesme=${oteki.toFixed(2)}` +
    `  ${deger.toFixed(2) === oteki.toFixed(2) ? "ayni" : "FARKLI"}`);
}
indirimsiz   bos=100.00  sozlesme=100.00  ayni
sozlesmeli   bos=85.00  sozlesme=85.00  ayni
iki indirim  bos=80.00  sozlesme=80.00  ayni
for d in bos sozlesme; do echo "$d/indirim.mjs: $(grep -o null $d/indirim.mjs | wc -l | tr -d ' ') bos deger"; done
bos/indirim.mjs: 5 bos deger
sozlesme/indirim.mjs: 0 bos deger

Aynı üç sonuç, beş boş değer yerine sıfır boş değerle üretiliyor. Kazanç yalnız sayıda değil: boş toplamalı imza yeni bir indirim türü eklendiğinde değişmez, boş değerli imza ise her yeni indirim için bir parametre ve bir denetim daha ister.

Boşluğu Görünür Kılmanın Yolları

Ölçüm hangi tekniğin seçileceğini söylemez; üç seçenek de mesafeyi sıfıra indirir ve farkları başka yerdedir.

Erken hata. Boşluk bir programlama hatasının belirtisiyse, üretildiği yerde atılır. rotaSec bunu yapıyor: tanımlı olmayan bölge kodu bir veri kusurudur, hesaba devam etmek anlamsızdır. Bu seçenek çağıran tarafa karar bırakmaz.

Boş toplama. Boşluk “hiç yok” anlamındaysa ve işlem sıfır ögeyle de anlamlıysa boş liste, boş eşleme ya da sıfır kullanılır. İndirim örneği budur; çağıran taraf hiçbir denetim yazmaz, çünkü boşluk zaten desteklenen bir durumdur.

Açık sonuç türü. Boşluk beklenen bir sonuçsa ve ne yapılacağına çağıran taraf karar vermeliyse, sonuç iki biçimden birini alır ve hangisi olduğu okunabilir. tasiyiciSec bunu yapıyor: bir çağıran hata atmayı, başka bir çağıran varsayılan taşıyıcıya düşmeyi seçebilir.

Üçünün ortak yanı, boşluğun imzanın parçası olmasıdır. Boş değer döndürmek ise boşluğu imzadan gizler; gerçekleştirimi okumayan hiç kimse varlığını bilmez.

Özet

  • Boş değer döndüren bir imza boşluğu gizler; çağıran taraf varsayımını gerçekleştirimi okumadan doğrulayamaz.
  • Ölçümde boşluk üretildikten üç çağrı sonra hata verdi; hata tasiyiciGecikmesi içinde göründü ve boşluğu üreten fonksiyon yığıt izinde yer almıyordu.
  • Boşluğu sonuç türünde gösteren sürümde mesafe sıfıra indi ve hata iletisi tür hatası yerine bölge kodunu adıyla anan bir alan hatasına dönüştü.
  • Boş değer argüman olarak geçirildiğinde her parametre bir denetim ister; boş toplamalı imza aynı üç sonucu beş boş değer yerine sıfır boş değerle üretti.
  • Boşluğu görünür kılmanın üç yolu erken hata, boş toplama ve açık sonuç türüdür; üçü de boşluğu imzanın parçası yapar.

Sonraki Adım

ucretHesapla imzası boş değerlerden kurtuldu, ama benzer bir sorun başka bir kılıkta geri geliyor. Ücretlendirme kitaplığında iade gönderileri de hesaplanıyor ve iki hesap büyük ölçüde ortak; aradaki farkı taşımak için imzaya bir doğruluk değeri eklemek akla yakın görünür. Böyle bir parametre çağrı yerinde tek bir sözcük bile söylemez ve fonksiyonun içinde bir dallanma açar. Sonraki ders bunun bedelini sınamayla ölçer: bayraklı tek fonksiyonun ve bölünmüş iki fonksiyonun sınanması için kaç durum gerekiyor.

İ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