Ders 09 / 14
Saf Fonksiyonlar
Yan etkisizlik ve referans saydamlığı: çağrının değeriyle değiştirilmesinin koşturularak sınanması, saf olmayan sürümde davranışın nasıl kaydığı ve iki sürümün testi için gereken düzeneğin satır sayısıyla karşılaştırılması.
İçindekiler
Bir önceki ders nesne sorumluluğunu tamamladı: veri ile onu değiştiren davranış aynı birimde durur, dışarısı duruma değil davranışa seslenir. O konu boyunca birim nesneydi. Durum vardı, onu koruyan davranış vardı ve ikisi bir arada tutuluyordu.
Bir başka model durumu birimden tamamen çıkarır. Veri üretildikten sonra değiştirilmez, davranış girdiden çıktıya bir eşlemeye indirgenir, yan etki programın sınırına itilir. Bu konu o modeli kurar; ilk soru en küçük parçasındadır. Bir fonksiyonun saf olması ne demektir ve saflık, iddia edilmek yerine nasıl koşturulup sınanır?
Saf Fonksiyonun İki Koşulu
Saf fonksiyon (pure function) Programlama Temelleri kursunda iki koşulla tanımlanmıştı: aynı girdi için her zaman aynı çıktıyı verir ve yan etki (side effect) üretmez. Yan etki, fonksiyonun döndürdüğü değerin dışında kalan her gözlenebilir değişikliktir: kendi dışındaki bir değişkeni değiştirmek, dosyaya yazmak, ağ isteği açmak, ekrana yazdırmak.
Gönderi ücretlendirme kitaplığının çekirdeği bu tanıma uyar. Ücret, gönderinin ağırlığından ve bölgesinden, tarifenin kademelerinden ve bölge katsayılarından hesaplanır.
// ucret.mjs — gonderi ucretlendirmenin cekirdegi: girdiden ciktiya bir esleme 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 }, ]; // Ucret kurus cinsinden tam sayidir; kayan noktali para hesabi yapilmaz. export function ucretHesapla(gonderi, tarife) { const kademe = tarife.kademeler.find((k) => gonderi.agirlikGram <= k.ustAgirlikGram) ?? tarife.kademeler.at(-1); const katsayi = tarife.bolgeKatsayisi[gonderi.bolge]; return Math.max(tarife.asgariUcret, Math.round(kademe.ucret * katsayi)); }
İki koşul da fonksiyonun imzasından okunabilir. Tarife bir parametredir, modül düzeyinde tutulan bir değer değildir; fonksiyon hiçbir yere yazmaz, yalnızca döndürür.
Aynı hesap, sık rastlanan bir biçimde de yazılabilir. Aşağıdaki sürümde tarife dışarıdan alınmaz, aylık hacim modül düzeyinde biriktirilir ve her çağrı bir kayıt satırı üretir.
// kirli-ucret.mjs — ayni hesap, ama aylik hacim sayaci modul duzeyinde tutuluyor import { TARIFE } from "./ucret.mjs"; const HACIM_ESIGI = 20000; let aylikHacim = 0; export function ucretHesaplaKirli(gonderi) { const kademe = TARIFE.kademeler.find((k) => gonderi.agirlikGram <= k.ustAgirlikGram) ?? TARIFE.kademeler.at(-1); const temel = Math.max(TARIFE.asgariUcret, Math.round(kademe.ucret * TARIFE.bolgeKatsayisi[gonderi.bolge])); aylikHacim += temel; console.log(`[kayit] ${gonderi.kod} ucretlendirildi`); return Math.round(temel * (aylikHacim > HACIM_ESIGI ? 0.9 : 1)); } export const sifirla = () => { aylikHacim = 0; };
İkinci sürüm de doğru sonuçlar üretir. Hacim indirimi gerçek bir iş kuralıdır, kayıt satırı
gerçek bir gereksinimdir. Aradaki fark davranışın doğruluğunda değil, davranışın nereye
bağlı olduğundadır: ucretHesaplaKirli çağrısının sonucu, kendisinden önce kaç çağrı
yapıldığına bağlıdır.
Referans Saydamlığı
Bu bağlılığın ölçülebilir bir sınavı vardır. Referans saydamlığı (referential transparency), bir ifadenin, ürettiği değerle değiştirilmesi durumunda programın davranışının değişmemesidir. Sınav yordamı doğrudan bu tanımdan çıkar: aynı çağrı üç kez yazılır, sonra çağrı bir kez hesaplanıp sonucu üç yere gömülür, iki koşumun sonuçları karşılaştırılır.
// saydamlik.mjs — cagriyi degeriyle degistirmek davranisi degistiriyor mu import { TARIFE, GONDERILER, ucretHesapla } from "./ucret.mjs"; import { ucretHesaplaKirli, sifirla } from "./kirli-ucret.mjs"; const g = GONDERILER[1]; const cagriyla = [ucretHesapla(g, TARIFE), ucretHesapla(g, TARIFE), ucretHesapla(g, TARIFE)]; const deger = ucretHesapla(g, TARIFE); const gomulu = [deger, deger, deger]; console.log(`saf / cagriyla : ${cagriyla.join(", ")}`); console.log(`saf / gomulu : ${gomulu.join(", ")}`); console.log(`saf / esit mi : ${cagriyla.join() === gomulu.join()}`); sifirla(); const kCagriyla = [ucretHesaplaKirli(g), ucretHesaplaKirli(g), ucretHesaplaKirli(g)]; sifirla(); const kDeger = ucretHesaplaKirli(g); const kGomulu = [kDeger, kDeger, kDeger]; console.log(`kirli / cagriyla : ${kCagriyla.join(", ")}`); console.log(`kirli / gomulu : ${kGomulu.join(", ")}`); console.log(`kirli / esit mi : ${kCagriyla.join() === kGomulu.join()}`);
saf / cagriyla : 7463, 7463, 7463 saf / gomulu : 7463, 7463, 7463 saf / esit mi : true [kayit] TR-4472 ucretlendirildi [kayit] TR-4472 ucretlendirildi [kayit] TR-4472 ucretlendirildi [kayit] TR-4472 ucretlendirildi kirli / cagriyla : 7463, 7463, 6717 kirli / gomulu : 7463, 7463, 7463 kirli / esit mi : false
Çıktıda iki ayrı sapma vardır. Birincisi döndürülen değerlerdedir: saf olmayan sürümde üçüncü çağrı, biriken hacim eşiği aştığı için 7463 yerine 6717 döndürür. Değişiklik gönderide değil, çağrı geçmişindedir. İkincisi kayıt satırlarındadır. Toplam dört satır basılmıştır: üç çağrılı koşumdan üç, tek çağrılı koşumdan bir. İfadeyi değeriyle değiştirmek yalnızca sonucu değil, programın ürettiği kayıt sayısını da değiştirmiştir.
Referans saydamlığının pratikteki karşılığı üç maddedir. Bir çağrının sonucu önbelleğe alınabilir, çünkü sonraki çağrılar aynı değeri verir. Çağrılar yeniden sıralanabilir ya da gereksizse hiç yapılmayabilir, çünkü sıra sonucu etkilemez. Bir hata raporundaki girdi yeterlidir; hatayı yeniden üretmek için çağrı geçmişini kurmak gerekmez. Saf olmayan sürümde bu üç maddenin hiçbiri geçerli değildir.
Saflığın Testte Görünen Bedeli
Aradaki fark en somut biçimde testte görünür. İki sürüm için aynı üç iddia yazılır: üç gönderinin ücreti sırasıyla 4990, 7463 ve 14715 olmalıdır.
// saf.test.mjs — uc gonderi, uc iddia; kurulum yalnizca girdinin kendisi import test from "node:test"; import assert from "node:assert/strict"; import { TARIFE, GONDERILER, ucretHesapla } from "./ucret.mjs"; test("ucret kademe ve bolge katsayisiyla hesaplanir", () => { assert.equal(ucretHesapla(GONDERILER[0], TARIFE), 4990); assert.equal(ucretHesapla(GONDERILER[1], TARIFE), 7463); assert.equal(ucretHesapla(GONDERILER[2], TARIFE), 14715); });
// kirli.test.mjs — ayni uc iddia; sayaci ve kayit akisini testin denetlemesi gerekiyor import test, { beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; import { GONDERILER } from "./ucret.mjs"; import { ucretHesaplaKirli, sifirla } from "./kirli-ucret.mjs"; const gercekLog = console.log; beforeEach(() => { sifirla(); console.log = () => {}; }); afterEach(() => { console.log = gercekLog; }); test("bir kilograma kadar, birinci bolge", () => { assert.equal(ucretHesaplaKirli(GONDERILER[0]), 4990); }); test("bes kilograma kadar, ikinci bolge", () => { assert.equal(ucretHesaplaKirli(GONDERILER[1]), 7463); }); test("on bes kilograma kadar, ucuncu bolge", () => { assert.equal(ucretHesaplaKirli(GONDERILER[2]), 14715); });
node --test --test-reporter=tap 2>&1 | grep -E '^# (tests|pass|fail)' for f in saf.test.mjs kirli.test.mjs; do printf '%-15s %2d satir\n' "$f" "$(awk 'END{print NR}' "$f")" done
# tests 4 # pass 4 # fail 0 saf.test.mjs 10 satir kirli.test.mjs 28 satir
Her iki dosya da geçer, ama aynı üç iddia bir dosyada 10, diğerinde 28 satır tutar. Fazlalık iddialarda değil düzenekte (fixture) birikir. Saf olmayan sürümün testi üç şeyi yapmak zorundadır: her denemeden önce modül düzeyindeki sayacı sıfırlamak, kayıt akışını susturup sonra geri koymak ve üç iddiayı ayrı denemelere bölmek. Üçüncüsü isteğe bağlı değildir; iddialar tek gövdede kalırsa biriken hacim üçüncü iddiayı düşürür.
Bu satırların hepsi, üretimde bulunmayan bir düzeni testte kurmak içindir. Saf sürümde kurulacak bir düzen yoktur, çünkü fonksiyonun ihtiyacı olan her şey parametrelerindedir.
Saflığın Sınırı
Saflık, fonksiyonun içinde hiçbir şeyin değişmemesi demek değildir. Ölçüt gözlenebilirliktir: fonksiyonun gövdesinde tanımlanmış bir değişken kaç kez değiştirilirse değiştirilsin, dışarıdan görülmediği sürece fonksiyon saftır. Aşağıdaki döngü yerel bir birikeç kullanır ve saflığı bozmaz.
// toplam.mjs — yerel degisken degisir, disaridan gorunmez: fonksiyon saf kalir export function toplamUcret(gonderiler, ucretlendir) { let toplam = 0; for (const g of gonderiler) toplam += ucretlendir(g); return toplam; }
İki sınır ayrıca not edilmelidir. Birincisi, saf bir fonksiyon parametre olarak aldığı
nesneyi değiştirmemelidir; değiştirirse çağıranın gördüğü veri de değişir ve bu bir yan
etkidir. Bu kısıtı sağlamanın yolu bir sonraki dersin konusudur. İkincisi, zaman ve
rastgelelik saflığın doğal düşmanlarıdır: Date.now çağrısını gövdeye koymak, aynı girdiye
aynı çıktı koşulunu kırar. Çözüm bu değerleri yasaklamak değil, dışarıda üretip parametre
olarak geçirmektir. Kural, ücretlendirme kitaplığının tamamı için tek cümlede yazılabilir:
çekirdek yalnızca kendisine verileni okur.
Özet
- Saf fonksiyon iki koşulu sağlar: aynı girdiye aynı çıktı ve yan etki üretmemek. Koşullar fonksiyonun imzasından okunur; tarife parametredir, modül değişkeni değildir.
- Referans saydamlığı, bir çağrının değeriyle değiştirilmesinin davranışı değiştirmemesidir. Sınav koşturularak yapılır: saf sürümde iki koşum eşit, saf olmayan sürümde üçüncü değer 7463 yerine 6717 ve kayıt satırı sayısı üç yerine bir çıkar.
- Saf olmayan sürümde önbellekleme, çağrıları yeniden sıralama ve hatayı yalnızca girdiden yeniden üretme olanaklarının üçü de kaybolur.
- Aynı üç iddia saf sürümde 10, saf olmayan sürümde 28 satır tutar; fark iddialarda değil, sayacı sıfırlayan ve kayıt akışını susturan düzenektedir.
- Fonksiyonun gövdesinde tanımlanmış bir değişkenin değişmesi saflığı bozmaz; ölçüt değişikliğin dışarıdan gözlenip gözlenmediğidir.
Sonraki Adım
Bu dersin saf fonksiyonu kendi parametresini değiştirmedi, ama bunu bir kural olarak uygulayan hiçbir şey yoktu. Tarife nesnesi çağıranla paylaşılmış tek bir nesnedir; onu değiştiren tek bir satır, birbirinden habersiz iki çağrının sonucunu birbirine bağlar. Sonraki ders paylaşılan bu veriyi ele alıyor: aynı tarifeyi kullanan iki koşum yolunda iç içe yapının kopyalanmaması kaç sessiz değişikliğe yol açar, kopyalamanın bedeli kaç nesnedir ve bu bedel nerede ödenmeyebilir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.