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;
ucretKurallarlabirinci biçimi,oranliikinci 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.