Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı Functional Programming Kavramları — #01

Functional Programming Nedir? C# ve TypeScript Geliştiricisi İçin Declarative Düşünce

Mert Özen 15 Kas 2026 12 dk 5 görüntülenme
Functional Programming Nedir? C# ve TypeScript Geliştiricisi İçin Declarative Düşünce

Functional programming, hesaplamayı değerleri adım adım değiştiren komutlar yerine değer alıp değer döndüren fonksiyonların birleşimi olarak kurmaktır. Bookstore'un sepeti sessizce değiştiren ödeme kodu üzerinden imperative ile declarative farkını, first-class function'ı, kısa tarihçeyi ve LINQ, RxJS ile Angular signal'lerinde zaten kullandığın functional fikirleri gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 (SDK 10.0.402) ile, TypeScript örnekleri TypeScript 7.0.2, Node.js 26, RxJS 7.8.2 ve Angular 22.2 (@angular/core 22.2.6) ile derlenip denenmiştir.

Önce Tahmin Et: Toplamı Hesaplayan Method Sepeti Değiştirir mi?

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) ödeme adımında sepet toplamını yıllardır aynı class hesaplıyor. Kurallar basit: adedi sıfır olan satırlar sayılmaz, AUTUMN10 kuponu kasım sonuna kadar yüzde 10 indirim yapar, üyelere 15 TL indirim var, sepette basılı kitap varsa ve indirimli tutar 300 TL'nin altındaysa 49,90 TL kargo eklenir. Kod şöyle:

public class LegacyCheckout
{
    public static decimal LastTotal;

    public decimal CalculateTotal(List<LegacyCartLine> lines, string? couponCode, bool isMember)
    {
        // Adedi sıfır olan satırları temizle
        for (int i = lines.Count - 1; i >= 0; i--)
        {
            if (lines[i].Quantity <= 0)
                lines.RemoveAt(i);
        }

        decimal total = 0;
        bool hasPrintedBooks = false;
        foreach (LegacyCartLine line in lines)
        {
            total += line.UnitPrice * line.Quantity;
            if (!line.IsEbook)
                hasPrintedBooks = true;
        }

        if (couponCode == "AUTUMN10" && DateTime.Now < new DateTime(2026, 12, 1))
            total = total * 0.90m;

        if (isMember)
            total = total - 15;

        if (hasPrintedBooks && total < 300)
            total = total + 49.90m;

        total = Math.Round(total, 2, MidpointRounding.AwayFromZero);
        Console.WriteLine($"[checkout] total={total}");
        LastTotal = total;
        return total;
    }
}

LegacyCartLine her property'si get; set; olan sıradan bir class. Sepette üç satır var: Dune (189,50 TL, basılı), Emma (59,90 TL, e-kitap) ve adedi sıfıra düşürülmüş The Hobbit. Aşağıdaki kod ekrana kaç satır yazar ve lines after ne olur?

List<LegacyCartLine> cart = SampleCarts.Legacy();
Console.WriteLine($"lines before: {cart.Count}");

decimal total = new LegacyCheckout().CalculateTotal(cart, "AUTUMN10", isMember: true);

Console.WriteLine($"total: {total}");
Console.WriteLine($"lines after: {cart.Count}");
Console.WriteLine($"LastTotal: {LegacyCheckout.LastTotal}");

Cevap: Bir Toplam Hesabının Gizli İşleri

$ dotnet run --project src/Bookstore.Fp.Demo -- 01-guess
lines before: 3
[checkout] total=259.36
total: 259.36
lines after: 2
LastTotal: 259.36

Serinin kodu Bookstore.Fp.slnx solution'ında duruyor; C# örnekleri src/Bookstore.Fp.Demo console projesinde ve her çıktı bloğunun ilk satırındaki komutla çalışıyor.

Toplam doğru: 249,40 TL'nin yüzde 90'ı 224,46, üye indirimiyle 209,46, kargoyla 259,36. Ama method'un imzası "bana bir sepet ver, sana bir sayı vereyim" diyor; gövdesi ise bundan fazlasını yapıyor. Çağıranın sepetinden bir satırı siliyor (sepet sayfası artık The Hobbit'i göstermiyor), console'a yazıyor, static bir field'a son toplamı bırakıyor ve sonucu, imzada görünmeyen bir girdiye, makinenin saatine bağlıyor. Aynı sepetle 1 Aralık'ta çağırırsan kupon sessizce düşer ve test ettiğin sayı değişir.

Bu kodda yanlış olan tek bir satır yok; her satır tek başına mantıklı. Sorun, method'un ne yaptığını anlamak için gövdenin tamamını okumak ve sırayı aklında tutmak zorunda olman. total değişkeni beş kez değişiyor; doğru değeri bilmek için hangi satırda olduğunu bilmen gerekiyor. Functional programming bu sorunun cevabı olarak ortaya çıkmış bir düşünce biçimi: hesaplamayı, değerleri değiştiren adımlar dizisi yerine, değer alıp değer döndüren fonksiyonların birleşimi olarak kurmak.

Bu seri boyunca bu class'ı adım adım değiştireceğiz. Bu yazıda yalnızca bakış açısını kuruyoruz: imperative ile declarative arasındaki fark, fonksiyonu değer gibi kullanmak, kısa bir tarihçe ve bugün zaten kullandığın functional araçlar.

Imperative ve Declarative: "Nasıl" Yerine "Ne"

Imperative kod bilgisayara adımları söyler: bir değişken aç, döngüye gir, koşula bak, değişkeni güncelle. Declarative kod sonucu tarif eder: adedi sıfırdan büyük satırların tutarlarının toplamı. Aynı kuralların declarative karşılığı şöyle:

public static class CheckoutMath
{
    public static decimal Subtotal(IEnumerable<LegacyCartLine> lines) =>
        lines.Where(line => line.Quantity > 0)
             .Sum(line => line.UnitPrice * line.Quantity);

    public static bool HasPrintedBooks(IEnumerable<LegacyCartLine> lines) =>
        lines.Any(line => line.Quantity > 0 && !line.IsEbook);

    public static decimal ShippingFee(decimal discountedTotal, bool hasPrintedBooks) =>
        (hasPrintedBooks, discountedTotal) switch
        {
            (false, _) => 0m,
            (true, >= 300m) => 0m,
            _ => 49.90m
        };
}

Subtotal satırları silmiyor; ilgilenmediği satırları filtreliyor. Where ve Sum LINQ'in (Language Integrated Query; C#'a 2007'de gelen, collection'lar üzerinde sorgu yazma özelliği) operatörleri; nasıl çalıştıklarını LINQ serisinde ayrıntılı gördük, burada önemli olan şekil. Her method tek bir ifadeden oluşuyor, hiçbir local değişkeni güncellemiyor ve dönüş değeri yalnızca parametrelerine bağlı. Okurken satır numarası takip etmen gerekmiyor; Subtotal'ın ne döndürdüğünü bilmek için yalnızca onu okuman yeterli.

Declarative kod imperative koddan "daha kısa" olduğu için değil, daha az şeyi aklında tutmanı istediği için değerli. Döngü sayacı, ara durum, "bu satıra geldiğimde total neydi" sorusu ortadan kalkıyor. Bedeli de var: Where ve Sum'ın arkasında yine bir döngü çalışıyor, sadece onu sen yazmıyorsun. Bu farkın performansa etkisini serinin son yazısında ölçeceğiz.

Fonksiyon Bir Değerdir: First-Class Function ve Expression-Oriented Kod

Functional programming'in ikinci temel fikri, fonksiyonun bir değer olması: bir değişkene atanabilir, parametre olarak geçirilebilir, başka bir fonksiyondan döndürülebilir. Buna first-class function denir. C#'ta bunun aracı delegate'ler, Func ve lambda; mekaniğini C# İleri serisinin "C#'ta Delegate, Func ve Action: Method'u Değer Gibi Taşımak" ve "C#'ta Lambda ve Closure: Yakalanan Değişken Tuzağı" yazılarında gördük. İndirim kurallarını değer olarak yazınca kodun geri kalanı şöyle oluyor:

List<LegacyCartLine> cart = SampleCarts.Legacy();

decimal subtotal = CheckoutMath.Subtotal(cart);
bool hasPrinted = CheckoutMath.HasPrintedBooks(cart);
Console.WriteLine($"subtotal: {subtotal}, printed: {hasPrinted}, lines: {cart.Count}");

Func<decimal, decimal> autumnCoupon = amount => amount * 0.90m;
Func<decimal, decimal> memberDiscount = amount => amount - 15m;

decimal discounted = memberDiscount(autumnCoupon(subtotal));
decimal shipping = CheckoutMath.ShippingFee(discounted, hasPrinted);
decimal total = Math.Round(discounted + shipping, 2, MidpointRounding.AwayFromZero);
Console.WriteLine($"discounted: {discounted:0.00}, shipping: {shipping}, total: {total}");
$ dotnet run --project src/Bookstore.Fp.Demo -- 01
subtotal: 249.40, printed: True, lines: 3
discounted: 209.46, shipping: 49.90, total: 259.36

Sonuç aynı, ama sepet üç satırıyla duruyor. İndirimler artık birer değer: bir listeye koyabilir, sırasını değiştirebilir, birini test için çıkarabilirsin. memberDiscount(autumnCoupon(subtotal)) ifadesi iki fonksiyonu art arda uyguluyor; buna composition denir ve serinin dördüncü yazısının konusu. Orada bu iki satırın sırasının neden önemli olduğunu da göreceğiz (yüzde indirimi ile sabit tutarlı indirimin sırası değişince sonuç değişiyor).

ShippingFee'deki switch expression ise üçüncü fikri gösteriyor: expression-oriented kod. Legacy method'da kargo bir if statement'ı ile bir değişkene ekleniyordu; burada switch'in kendisi bir değer üretiyor. Statement "şunu yap" der, expression "şudur" der. Expression'ların bir özelliği de her dalın bir değer döndürmek zorunda olması: bir dalı unutursan derleyici uyarır. Pattern'lerin syntax'ı C# İleri serisinin "C#'ta Pattern Matching Derinlemesine: Property, Positional ve Tuple Pattern'ler" yazısında; bu seride pattern matching'i, verinin şeklini tiplerle modellediğimiz beşinci yazıda yeniden ele alacağız.

Kısa Tarihçe: Lambda Calculus'tan LINQ'e

Bu fikirler yeni değil. 1930'larda Alonzo Church, hesaplamayı yalnızca fonksiyon tanımlama ve uygulama üzerinden ifade eden lambda calculus'u geliştirdi. C#'taki x => x * 2 ifadesine lambda denmesinin sebebi bu. 1958'de John McCarthy'nin geliştirdiği Lisp, fonksiyonları veri gibi ele alan ilk yaygın dillerden biri oldu. 1970'lerde Robin Milner ve ekibinin Edinburgh'da geliştirdiği ML (Meta Language; ilk olarak bir teorem ispatlayıcının komut dili olarak doğdu), tip çıkarımını (type inference) ve algebraic data type'ları yaygınlaştırdı; bugün TypeScript'te ve C#'ta var ile gördüğün tip çıkarımının fikri bu çizgiden geliyor. Haskell'in ilk raporu Nisan 1990'da yayımlandı; dil, side effect'leri tip sisteminde ayrı tutmasıyla bilinir. F# ise Microsoft Research'te Don Syme'ın öncülüğünde ML ailesinin .NET üzerindeki temsilcisi olarak 2005'te ortaya çıktı.

İki metin de bu tarihte önemli bir yer tutar. John Backus 1977'deki Turing Award konuşmasında programlamanın değişkenleri adım adım güncelleyen bir stile sıkışıp kaldığını savundu ve fonksiyonların birleşimiyle program yazmayı önerdi. John Hughes'un 1989'da yayımlanan "Why Functional Programming Matters" makalesi ise functional programming'in değerini "şunu yapmıyoruz" listesiyle değil, modülerlikle açıkladı: Hughes'a göre higher-order function'lar ve lazy evaluation, küçük parçaları birbirine bağlamanın iki güçlü yolu.

Bu fikirler ana akım dillere birden değil, parça parça girdi. C# 3.0 ile Kasım 2007'de gelen LINQ; lambda expression'ları, extension method'ları ve sorguları değer olarak taşıyan bir yapıyı birlikte getirdi. Sonra C# 7 ile pattern matching (2017), C# 8 ile switch expression (2019), C# 9 ile record (2020) eklendi; C# 15 ise union type'larla gerçek sum type desteğini getiriyor (serinin sekizinci yazısı). TypeScript tarafında map/filter/reduce zaten JavaScript'in içindeydi; discriminated union'lar ve readonly tip sistemine geldi. Reactive Extensions fikri 2009 civarında Microsoft'ta Rx.NET olarak doğdu, JavaScript'e RxJS olarak taşındı ve Angular'ın HTTP ve form API'lerinin temeline yerleşti.

Zaten Functional Programming Yapıyorsun

Bir .NET ve Angular geliştiricisiysen bu fikirleri her gün kullanıyorsun. Aynı sepet hesabını TypeScript'te üç farklı araçla yazalım:

import { computed, signal } from "@angular/core";
import { from, filter, map, reduce } from "rxjs";
import { sampleCart, type CartLine } from "./cart.ts";

const cart = sampleCart();

// 1) Array method'ları
const subtotal = cart
  .filter((line) => line.quantity > 0)
  .map((line) => line.unitPrice * line.quantity)
  .reduce((sum, amount) => sum + amount, 0);
console.log("array subtotal:", subtotal.toFixed(2));

// 2) RxJS pipe
from(cart)
  .pipe(
    filter((line) => line.quantity > 0),
    map((line) => line.unitPrice * line.quantity),
    reduce((sum, amount) => sum + amount, 0),
  )
  .subscribe((total) => console.log("rxjs subtotal:", total.toFixed(2)));

// 3) Angular signal'leri
const lines = signal<CartLine[]>(cart);
const signalSubtotal = computed(() =>
  lines()
    .filter((line) => line.quantity > 0)
    .reduce((sum, line) => sum + line.unitPrice * line.quantity, 0),
);
console.log("signal subtotal:", signalSubtotal().toFixed(2));
lines.update((current) => current.map((line) => (line.title === "Emma" ? { ...line, quantity: 2 } : line)));
console.log("after update:", signalSubtotal().toFixed(2));
$ npx tsc --noEmit
$ node src/01-already-fp.ts
array subtotal: 249.40
rxjs subtotal: 249.40
signal subtotal: 249.40
after update: 309.30

TypeScript örnekleri projenin ts klasöründe; komutlardan önce orada bir kez npm install çalıştır. Node.js 26 .ts dosyalarını type stripping ile doğrudan çalıştırıyor; tip kontrolünü ayrıca tsc --noEmit yapıyor (çıktı vermemesi hatasız olduğu anlamına geliyor). Signal'leri burada Angular uygulaması olmadan, yalnızca @angular/core paketinden kullandık.

Üç örnekte de aynı kalıp var: veri bir dizi fonksiyondan geçiyor, her fonksiyon bir öncekinin çıktısını alıyor, hiçbiri kaynağı değiştirmiyor. filter ve map callback olarak fonksiyon alıyor; bunlara higher-order function denir. RxJS'in pipe'ı operatörleri birleştirip yeni bir fonksiyon kuruyor; bu composition. computed() ise kendi içinde yalnızca okuduğu signal'lere bağlı bir hesap tanımlıyor ve Angular bu hesabın aynı girdiyle aynı sonucu vereceğini varsayarak sonucu saklıyor. Bu varsayımın adı purity ve üçüncü yazının konusu.

Son iki satıra dikkat: Emma'nın adedini artırırken array'i yerinde değiştirmedik, map ile yeni bir array ve spread ile yeni bir object ürettik. update yeni referansı görünce computed yeniden hesaplandı. Satırı yerinde değiştirseydin ne olacağını ikinci yazıda, immutability'yi anlatırken gerçek çıktıyla göreceğiz.

Functional Programming ve OOP: Rakip Değil

Functional programming bazen nesne yönelimli programlamanın (OOP) rakibi gibi anlatılır: "class yazma, her şey fonksiyon olsun." Pratikte ikisi farklı soruların cevabı. OOP davranışı ve sorumlulukları sınırlar içine yerleştirmekle ilgilenir: hangi kod hangi veriye dokunabilir, bağımlılıklar nasıl değiştirilir. Functional programming ise hesaplamanın kendisiyle ilgilenir: bir sonucu üreten kod ne kadar öngörülebilir, ne kadar kolay birleştirilir ve test edilir.

Modern C# ikisini birlikte kullanmaya göre tasarlanmış. ASP.NET Core'da service'ler dependency injection ile kurulan class'lardır; ama bir request'in fiyat hesabı, o class'ların çağırdığı ve kendi başına test edilebilen pure fonksiyonlar olabilir. Bu düzenin bir adı var: functional core, imperative shell. Kararları veren hesaplama bir çekirdekte toplanır; veritabanı, saat, log ve HTTP gibi dış dünyaya dokunan işler ince bir kabukta kalır. Unit Test serisinin "Test Edilebilir Tasarım: Dependency Injection, Seam'ler, Functional Core ve Humble Object" yazısı bu düzeni test açısından ele alıyor; bu seri aynı düzenin "neden" tarafını ve composition'ını işleyecek.

LegacyCheckout'a bu gözle bakınca ayrım net: toplamı hesaplayan kurallar çekirdeğe ait; satırları silmek, console'a yazmak, static field'ı güncellemek ve saati okumak kabuğa ya da hiçbir yere ait değil.

Seri Boyunca Bookstore'da Ne Yapacağız?

LegacyCheckout her yazıda bir adım daha temizlenecek:

  • İkinci yazı: LegacyCartLine ve sepet değişmeyen veri olur; satır silmek yerine yeni bir sepet üretilir. Angular'da yerinde push edilen array'in UI'ı neden güncellemediğini göreceğiz.
  • Üçüncü yazı: DateTime.Now, console ve static field kenara itilir; indirim hesabı pure function olur ve xUnit [Theory] ile test edilir.
  • Dördüncü yazı: indirim kuralları bir fonksiyon listesi olarak birleştirilir; kural sırası test edilebilir hâle gelir.
  • Beşinci yazıdan itibaren: sipariş durumu, "yok" anlamı ve hatalar tiplerle modellenir (algebraic data type, Option, Result).

Serinin hedefi Haskell öğretmek değil. C#'ta ve TypeScript'te zaten var olan araçlarla, daha az şaşırtan ve daha kolay test edilen kod yazmak; bunu yaparken nerede durmak gerektiğini de göstermek.

Sık Düşülen Tuzaklar

Functional programming'i "for döngüsü yazmamak" sanmak. foreach yerine ForEach(x => total += x.Price) yazmak kodu functional yapmaz; aynı değişkeni yine değiştiriyorsun, sadece lambda'nın içinden. Belirleyici olan döngünün yokluğu değil, state değişikliğinin ve gizli girdilerin yokluğu. Tersine, bir method'un içinde local bir değişkeni güncelleyen sade bir döngü dışarıdan bakınca tamamen functional olabilir: girdisini değiştirmiyorsa ve aynı girdiyle aynı sonucu veriyorsa, içerideki döngü bir uygulama ayrıntısıdır.

LINQ'i her yerde functional saymak. Select içinde bir veritabanı çağrısı yapan, log yazan ya da dışarıdaki bir listeye ekleme yapan lambda, declarative görünümün altına side effect saklar. LINQ'in deferred execution davranışıyla (sorgunun tanımlandığında değil, enumerate edildiğinde çalışması) birleşince bu side effect'ler beklemediğin anda ve beklemediğin sayıda çalışır; ayrıntısı LINQ serisinin "LINQ Deferred Execution: Sorgu Ne Zaman Çalışır? IEnumerable ve IQueryable" yazısında.

OOP ile rakip görüp class'lardan kaçmak. Her şeyi static fonksiyonlara çevirmek dependency injection'ı, test için seam'leri ve sorumluluk sınırlarını kaybettirir. Ayrım class ile fonksiyon arasında değil, kararları veren kod ile dış dünyaya dokunan kod arasında.

Tek seferde dönüştürmeye kalkmak. Bütün codebase'i bir hafta sonunda functional hâle getirmek yerine, en çok hata çıkan hesabı (fiyat, vergi, stok) pure bir çekirdeğe almakla başla. Bu seri de LegacyCheckout'u tek bir büyük refactoring ile değil, her yazıda bir adımla dönüştürüyor.

Küçük Bir Deneme

★☆☆ LegacyCheckout.CalculateTotal'ı aynı sepet object'iyle iki kez art arda çağır. İkinci çağrının çıktısı ve lines after değeri ilkinden farklı mı? Neden?

★☆☆ CheckoutMath'e, sepette en az bir e-kitap olup olmadığını döndüren HasEbooks adında tek ifadelik bir method ekle; döngü ve local değişken kullanma.

★★☆ TypeScript örneğinde lines.update(...) satırını, Emma'nın quantity property'sini doğrudan 2 yapan ve ardından lines.set(lines()) çağıran iki satırla değiştir. after update ne yazdırıyor? Cevabı bir sonraki yazıda göreceğiz.

★★★ LegacyCheckout'taki her satırı "hesaplama" ya da "dış dünyaya dokunan iş" diye işaretle. Hangi satırlar ikisi birden? Bu ayrım functional core, imperative shell düzeninde hangi satırın nereye gideceğini söylüyor.

Bir sonraki yazıda sepetin kendisini ele alıyoruz: immutability. LegacyCartLine'ı record'a çevirip satır silmek yerine yeni sepet üreteceğiz; ortak değiştirilebilir verinin hangi bug'lara yol açtığını ve Angular signal'lerinin neden yeni referans beklediğini göreceğiz.