İçeriğe geç
academia.sh

Ders 11 / 14

Yüksek Mertebeden Fonksiyonlar

Davranışın parametreleşmesi: aynı ücretlendirme kuralının bayrak parametresiyle, arama tablosuyla ve fonksiyon parametresiyle yazılması, yeni bir kural eklendiğinde üç sürümde dokunulan satırın ve çekirdekteki karar noktasının sayılması.

İçindekiler

Önceki iki ders veriyi ele aldı: ücret hesabı girdiden çıktıya bir eşlemeye indirgendi, tarife değiştirilemez hâle getirildi. Davranış ise sabit kaldı. ucretHesapla tek bir kural kümesini kendi gövdesinde taşıyor; sözleşmeli indirim, hacim indirimi, gece teslimi eki ve müşteriye özel anlaşmalar eklendikçe o gövde büyüyecek.

Bu ders davranışın kendisini parametreye çevirir. Aynı ücretlendirme mantığı üç ayrı yolla yazılır: bayrak parametresiyle, ad-oran tablosuyla ve fonksiyon parametresiyle. Sonra gerçek bir gereksinim gelir — gece teslimi eki — ve her üç sürümde dokunulan satır ile çekirdekte biriken karar noktası sayılır.

Ortak Çekirdek

Üç sürüm de aynı temel ücretten başlar. Tarife verisi ve indirimsiz hesap tek bir dosyada durur; kurallar bu hesabın üzerine gelir.

// tarife.mjs — tarife verisi ve indirimsiz temel ucret
export const TARIFE = {
  ad: "yurtici-standart",
  asgariUcret: 4990,
  kademeler: [
    { ustAgirlikGram: 1000, ucret: 4990 },
    { ustAgirlikGram: 5000, ucret: 6490 },
    { ustAgirlikGram: 15000, ucret: 10900 },
  ],
  bolgeKatsayisi: { 1: 1.0, 2: 1.15, 3: 1.35 },
};

export const GONDERILER = [
  { kod: "TR-4471", agirlikGram: 800, bolge: 1 },
  { kod: "TR-4472", agirlikGram: 3200, bolge: 2 },
  { kod: "TR-4473", agirlikGram: 12400, bolge: 3 },
];

export function temelUcret(gonderi, tarife) {
  const kademe = tarife.kademeler.find((k) => gonderi.agirlikGram <= k.ustAgirlikGram)
    ?? tarife.kademeler.at(-1);
  return Math.max(tarife.asgariUcret,
    Math.round(kademe.ucret * tarife.bolgeKatsayisi[gonderi.bolge]));
}

Değişen kısım kurallardır: hangi indirimin hangi gönderiye uygulanacağı. Bu karar üç ayrı yerde verilebilir.

Aynı Davranışın Üç Yazılışı

Birinci yol kararı bayrak parametresine bırakır. Çağıran hangi kuralın işleyeceğini mantıksal değerlerle bildirir, kuralın kendisi çekirdeğin gövdesindedir.

// bayrak.mjs — hangi kuralin isleyecegi bayrak parametreleriyle secilir
import { temelUcret } from "./tarife.mjs";

export function ucretBayrakli(gonderi, tarife, secenek = {}) {
  let ucret = temelUcret(gonderi, tarife);
  if (secenek.sozlesmeli) ucret = Math.round(ucret * 0.88);
  if (secenek.hacim) ucret = Math.round(ucret * 0.95);
  return ucret;
}

İkinci yol kuralları bir arama tablosunda toplar. Çekirdek artık her kural için ayrı bir dal taşımaz; adı tablodan arar, bulduğu oranı uygular.

// tablo.mjs — kurallar ad-oran tablosunda tutulur, adla secilir
import { temelUcret } from "./tarife.mjs";

const ORANLAR = {
  sozlesmeli: 0.88,
  hacim: 0.95,
};

export function ucretTabloyla(gonderi, tarife, adlar = []) {
  let ucret = temelUcret(gonderi, tarife);
  for (const ad of adlar) ucret = Math.round(ucret * (ORANLAR[ad] ?? 1));
  return ucret;
}

Üçüncü yol kuralı bir değere değil, doğrudan bir fonksiyona çevirir ve o fonksiyonu parametre olarak alır.

// kural.mjs — kural bir fonksiyondur, cagirana parametre olarak gecirilir
import { temelUcret } from "./tarife.mjs";

export const sozlesmeli = (ucret) => Math.round(ucret * 0.88);
export const hacim = (ucret) => Math.round(ucret * 0.95);

export function ucretKurallarla(gonderi, tarife, kurallar = []) {
  return kurallar.reduce((ucret, kural) => kural(ucret, gonderi),
    temelUcret(gonderi, tarife));
}

Üçüncü sürümdeki ucretKurallarla bir yüksek mertebeden fonksiyondur (higher-order function): Programlama Temelleri kursunda tanımlandığı gibi, ya bir fonksiyonu parametre olarak alan ya da bir fonksiyon döndüren fonksiyon. Burada iki kez geçer — hem ucretKurallarla kural listesini parametre olarak alır, hem de gövdesindeki reduce kendisine verilen birleştirme fonksiyonunu çağırır.

Aradaki fark biçimsel değildir. Bayraklı sürümde ne yapılacağı çekirdekte yazılıdır, çağıran yalnızca hangisinin açık olduğunu söyler. Tablolu sürümde ne yapılacağı yine kitaplığın içindedir, ama tek bir veri yapısında toplanmıştır. Kurallı sürümde ne yapılacağı çekirdeğin dışındadır; çekirdek yalnızca sırayla uygulamayı bilir.

Yeni Bir Kural Eklendiğinde

Gereksinim şu: gece teslimi seçilen gönderilere sabit bir ek uygulanacak, ek 5 kilogramın altındaki gönderilerde 1500 kuruş, üstündekilerde 2500 kuruş olacak. Bu kural bir çarpan değildir; toplanan bir tutardır ve gönderinin ağırlığına bakar.

// bayrak2.mjs — bayrakli surume gece teslimi eki eklendi
import { temelUcret } from "./tarife.mjs";

export function ucretBayrakli(gonderi, tarife, secenek = {}) {
  let ucret = temelUcret(gonderi, tarife);
  if (secenek.sozlesmeli) ucret = Math.round(ucret * 0.88);
  if (secenek.hacim) ucret = Math.round(ucret * 0.95);
  if (secenek.gece) ucret += gonderi.agirlikGram > 5000 ? 2500 : 1500;
  return ucret;
}
// tablo2.mjs — tablolu surume gece teslimi eki eklendi
import { temelUcret } from "./tarife.mjs";

const ORANLAR = {
  sozlesmeli: { oran: 0.88, ek: 0, agirEk: 0 },
  hacim: { oran: 0.95, ek: 0, agirEk: 0 },
  gece: { oran: 1, ek: 1500, agirEk: 2500 },
};

export function ucretTabloyla(gonderi, tarife, adlar = []) {
  let ucret = temelUcret(gonderi, tarife);
  for (const ad of adlar) {
    const k = ORANLAR[ad] ?? { oran: 1, ek: 0, agirEk: 0 };
    ucret = Math.round(ucret * k.oran);
    ucret += gonderi.agirlikGram > 5000 ? k.agirEk : k.ek;
  }
  return ucret;
}
// kural2.mjs — kurallı surume gece teslimi eki eklendi
import { temelUcret } from "./tarife.mjs";

export const sozlesmeli = (ucret) => Math.round(ucret * 0.88);
export const hacim = (ucret) => Math.round(ucret * 0.95);
export const gece = (ucret, g) => ucret + (g.agirlikGram > 5000 ? 2500 : 1500);

export function ucretKurallarla(gonderi, tarife, kurallar = []) {
  return kurallar.reduce((ucret, kural) => kural(ucret, gonderi),
    temelUcret(gonderi, tarife));
}

Üç sürümün de aynı sonucu ürettiği koşturularak doğrulanır. Ölçüm dosyası altı çağrıyı yan yana koyar: önce eski üç sürüm, sonra gece eki eklenmiş üç sürüm.

// olcum.mjs — uc surum ayni sonucu mu veriyor, yeni kural eklenince ne degisiyor
import { TARIFE, GONDERILER } from "./tarife.mjs";
import { ucretBayrakli } from "./bayrak.mjs";
import { ucretTabloyla } from "./tablo.mjs";
import { ucretKurallarla, sozlesmeli, hacim } from "./kural.mjs";
import { ucretBayrakli as bayrakYeni } from "./bayrak2.mjs";
import { ucretTabloyla as tabloYeni } from "./tablo2.mjs";
import { ucretKurallarla as kuralYeni, gece } from "./kural2.mjs";

const her = (f) => GONDERILER.map(f).join(", ");
const satir = (ad, deger) => console.log(`${ad.padEnd(20)} ${deger}`);

satir("bayrak / once", her((g) => ucretBayrakli(g, TARIFE, { sozlesmeli: true, hacim: true })));
satir("tablo  / once", her((g) => ucretTabloyla(g, TARIFE, ["sozlesmeli", "hacim"])));
satir("kural  / once", her((g) => ucretKurallarla(g, TARIFE, [sozlesmeli, hacim])));

const secenek = { sozlesmeli: true, hacim: true, gece: true };
const adlar = ["sozlesmeli", "hacim", "gece"];
satir("bayrak / gece ile", her((g) => bayrakYeni(g, TARIFE, secenek)));
satir("tablo  / gece ile", her((g) => tabloYeni(g, TARIFE, adlar)));
satir("kural  / gece ile", her((g) => kuralYeni(g, TARIFE, [sozlesmeli, hacim, gece])));

// Yalnizca bir cagirani ilgilendiren kural: kitaplikta hicbir dosya degismez.
const ilkGonderi = (ucret, g) => (g.kod === "TR-4471" ? Math.round(ucret * 0.5) : ucret);
satir("kural  / cagirandan", her((g) => kuralYeni(g, TARIFE, [sozlesmeli, hacim, gece, ilkGonderi])));
bayrak / once        4171, 6239, 12302
tablo  / once        4171, 6239, 12302
kural  / once        4171, 6239, 12302
bayrak / gece ile    5671, 7739, 14802
tablo  / gece ile    5671, 7739, 14802
kural  / gece ile    5671, 7739, 14802
kural  / cagirandan  2836, 7739, 14802

İlk altı satır üç sürümün davranışça denk olduğunu gösterir. Son satır yalnızca kurallı sürümde yazılabilen bir şeyi gösterir: yalnızca TR-4471 kodlu gönderiyi ilgilendiren bir indirim, ölçüm dosyasında iki satırla tanımlanıp geçirilmiştir. Kitaplıkta hiçbir dosya değişmemiştir. Bayraklı ya da tablolu sürümde aynı kuralın çalışması için tek bir müşteriye ait bir koşulun kitaplığın içine yazılması gerekirdi.

Dokunulan Satırın Sayılması

Gece eki eklenirken her sürümde ne kadar yer değiştiğini diff sayar. İki ölçüt ayrı tutulur: dosyanın tamamında dokunulan satır ve çekirdekte — yani dışa verilen ücretlendirme fonksiyonunun kendisinde — dokunulan satır. Üçüncü ölçüt, çekirdekte biriken karar noktası sayısıdır.

# Cekirdek: dosyadaki ilk `export function` satirindan dosya sonuna kadar olan bolum.
# Ilk satirdaki dosya adi yorumu olcume katilmaz.
cekirdek() { awk '/^export function/,0' "$1"; }
karar() { cekirdek "$1" | grep -oE '\bif\b|\bfor\b|\?\?|\?' | wc -l | tr -d ' '; }
for s in bayrak tablo kural; do
  toplam=$(diff <(tail -n +2 "$s.mjs") <(tail -n +2 "${s}2.mjs") | grep -c '^[<>]')
  ic=$(diff <(cekirdek "$s.mjs") <(cekirdek "${s}2.mjs") | grep -c '^[<>]')
  printf '%-7s dokunulan: %2d  cekirdekte: %d  karar noktasi: %s -> %s\n' \
    "$s" "$toplam" "$ic" "$(karar "$s.mjs")" "$(karar "${s}2.mjs")"
done
bayrak  dokunulan:  1  cekirdekte: 1  karar noktasi: 2 -> 4
tablo   dokunulan: 11  cekirdekte: 6  karar noktasi: 2 -> 3
kural   dokunulan:  1  cekirdekte: 0  karar noktasi: 0 -> 0

Toplam satır sayısı yanıltıcı olabilir: bayraklı sürüm de kurallı sürüm de tek satırla büyümüştür. Ayrım ikinci sütundadır. Bayraklı sürümde o satır çekirdeğin gövdesine girmiştir; karar noktası ikiden dörde çıkmıştır ve her yeni kural bu sayıyı artırmaya devam edecektir. Kurallı sürümde çekirdek hiç değişmemiştir; eklenen satır çekirdeğin dışında, bağımsız bir fonksiyondur ve çekirdeğin karar noktası sıfırda kalır.

Tablolu sürüm en pahalı olanıdır: on bir satır dokunulmuş, çekirdeğin gövdesinde altı satır değişmiştir. Nedeni tablonun şemasıdır. Tablo yalnızca oran tutabildiği için, toplanan bir ek gelince her satıra iki alan daha eklenmiştir — sozlesmeli ile hacim bu alanları hiç kullanmasa da. Arama tablosu, tuttuğu değerlerin hepsi aynı biçimde olduğu sürece ucuzdur; yeni bir kural yeni bir biçim getirdiğinde bedel tablonun tamamına yayılır.

Fonksiyon Döndüren Fonksiyon

Yüksek mertebeliliğin ikinci biçimi, fonksiyon döndürmektir. Kuralların hepsi birkaç kalıptan türediği için, kuralları elle yazmak yerine üreten fonksiyonlar tanımlanabilir.

// uretici.mjs — fonksiyon donduren fonksiyon: yuksek mertebenin ikinci yuzu
import { TARIFE, GONDERILER } from "./tarife.mjs";
import { ucretKurallarla } from "./kural2.mjs";

export const oranli = (oran) => (ucret) => Math.round(ucret * oran);
export const sabitEk = (kurus) => (ucret) => ucret + kurus;
export const kosullu = (kosul, kural) => (ucret, g) => (kosul(g) ? kural(ucret, g) : ucret);

const agir = (g) => g.agirlikGram > 5000;
const kurallar = [
  oranli(0.88),
  oranli(0.95),
  kosullu(agir, sabitEk(2500)),
  kosullu((g) => !agir(g), sabitEk(1500)),
];

console.log(GONDERILER.map((g) => ucretKurallarla(g, TARIFE, kurallar)).join(", "));
5671, 7739, 14802

Sonuç, elle yazılmış üç kuralın ürettiği değerlerle birebir aynıdır. Fark üretim biçimindedir: üç satırlık üç üreticiden — oranli, sabitEk ve kosullu — sınırsız sayıda kural kurulabilir. kosullu ayrıca bir kuralı başka bir kuralla sarmalar; aldığı kuralı çağırıp çağırmamaya karar verir. Kuralların bu biçimde birbirinin üstüne yığılabilmesi bir sonraki dersin konusudur.

Dolaylılığın Bedeli

Fonksiyon parametresi bedelsiz değildir. “Sözleşmeli bir müşterinin gönderisinde ne oluyor” sorusunun yanıtı bayraklı sürümde tek dosyadadır: bayrak2.mjs okunur, üç dal görülür, iş biter. Kurallı sürümde yanıt iki dosyaya dağılmıştır — kuralın tanımı kural2.mjs içinde, hangi kuralların hangi sırayla uygulandığı olcum.mjs içindedir. Okunacak dosya sayısı birden ikiye çıkar ve çağrı yerlerinin sayısı arttıkça sıranın nerede kurulduğu sorusu her çağrı yerine ayrı ayrı sorulur.

Ölçüt bu yüzden kuralın sayısı ve kaynağıdır. Kural kümesi kapalı ve kısa ise — iki üç sabit seçenek, hepsi kitaplığın kendi bilgisi — bayrak daha az dolaylılıkla aynı işi görür. Kural kümesi açık ise, yani kitaplığın önceden bilemeyeceği kurallar çağıranlardan gelecekse, fonksiyon parametresi çekirdeği hiç değiştirmeden bunu karşılayan tek sürümdür.

Özet

  • Yüksek mertebeden fonksiyon, bir fonksiyonu parametre olarak alan ya da bir fonksiyon döndüren fonksiyondur; ucretKurallarla birinci biçimi, oranli ikinci biçimi gösterir.
  • Aynı davranış üç yolla yazıldığında sonuçlar birebir aynıdır: üç gönderi için 4171, 6239 ve 12302.
  • Gece eki eklendiğinde bayraklı sürümde çekirdekte 1 satır değişir ve karar noktası 2’den 4’e çıkar; kurallı sürümde çekirdekte 0 satır değişir ve karar noktası 0’da kalır.
  • Arama tablosu, yeni kural tablonun şemasına uymadığında en pahalı sürümdür: 11 satır dokunulmuş, çekirdekte 6 satır değişmiştir.
  • Yalnızca bir çağıranı ilgilendiren kural, kurallı sürümde iki satırla tanımlanıp geçirilir ve kitaplıkta hiçbir dosya değişmez.
  • Fonksiyon parametresinin bedeli dolaylılıktır: bir gönderiye ne olduğunu anlamak için okunacak dosya sayısı birden ikiye çıkar.

Sonraki Adım

Bu dersin kuralları tek tek uygulandı; reduce her birini sırayla çağırdı ve ara sonuçlar hiçbir yerde adlandırılmadı. Kural sayısı arttıkça asıl soru sıranın kendisi olur: indirimin ekten önce mi sonra mı işlediği, asgari ücret tabanının nerede uygulandığı sonucu değiştirir. Sonraki ders bu ardışık uygulamayı bir boru hattı olarak kurar: adımlar adlandırılır, her adım tek tek sınanır ve iki adımın yeri değiştirildiğinde kaç gönderinin ücretinin kaç kuruş kaydığı sayılır.

İ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