İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat