Fonksiyonel programlama (functional programming, FP), hesaplamayı değişen durum ve komut dizileri yerine fonksiyon bileşimi ve veri dönüşümleri üzerinden modelleyen bir programlama yaklaşımıdır. Saf fonksiyonlar, değişmez veri ve yan etkilerin sınırlandırılması, özellikle test edilebilir iş kuralları kurarken işe yarar.
Aşağıda temel ilkeler, imperatif kodla farkı gösteren JavaScript örnekleri, yaygın desenler, kuyruk çağrısı optimizasyonunun dillere göre sınırları ve fonksiyonel üslubu hangi dillerin ne ölçüde zorunlu kıldığı ele alınıyor.
Fonksiyonel Programlama Nedir?
İmperatif programlamada bilgisayara bir sonuca nasıl ulaşacağını adım adım söylersiniz: değişken tanımla, döngü kur, değeri güncelle. Fonksiyonel yaklaşımda ise sonucu, girdiye uygulanan dönüşümlerin bileşimi olarak tanımlarsınız. Bu bildirimsel (declarative) üslup işin “nasıl” kısmını ortadan kaldırmaz; döngüyü map ve filter gibi tekrar kullanılabilir fonksiyonların içine taşır.
Temel İlkeler
Değişmezlik (immutability): Oluşturulan veri yerinde değiştirilmez; değişiklik gerektiğinde yeni bir değer üretilir. Matematikte x = 5 dedikten sonra aynı ifade içinde x’in değeri değişmez; FP aynı varsayımı koda taşır. Python’daki tuple gibi yapılar bu fikri dil düzeyinde, sığ biçimde uygular.
Saf fonksiyonlar (pure functions): Aynı girdi için her zaman aynı çıktıyı veren ve yan etkisi olmayan fonksiyonlardır. sin(30°) her zaman 0,5’tir. Saf bir fonksiyon veritabanına yazmaz, global değişkeni değiştirmez, konsola yazdırmaz; yalnız hesaplar ve sonucu döndürür. (Programlama dillerindeki sin fonksiyonları açıyı radyan olarak alır; JavaScript’te Math.sin(30) 30 radyanın sinüsünü hesaplar.)
Birinci sınıf fonksiyonlar (first-class functions): Fonksiyonlar değişkene atanabilir, parametre olarak geçirilebilir ve başka bir fonksiyondan döndürülebilir. JavaScript’teki callback’ler ve olay dinleyicileri bu özelliğe dayanır.
Yüksek mertebeden fonksiyonlar (higher-order functions): Fonksiyon alan ya da fonksiyon döndüren fonksiyonlardır. map, filter ve reduce en bilinen örnekleridir; tekrar eden döngü kalıplarını tek bir soyutlamada toplarlar.
Neden Fonksiyonel Programlama?
Öngörülebilirlik: Saf fonksiyonları test etmek kolaydır: girdi verilir, çıktı karşılaştırılır. Dış bağımlılık olmadığı için mock veya spy gerekmez.
Eşzamanlılık: Değişmez veri ve saf fonksiyonlar, paylaşılan değişken durumdan kaynaklanan data race’leri azaltır; birden çok iş parçacığı aynı veriyi kilit kullanmadan okuyabilir. Ancak deadlock ve mantıksal yarış koşulları (race condition) ortadan kalkmaz. Erlang’da iki süreç birbirinin mesajını beklerse kilitlenir; Haskell’de MVar ile deadlock kurmak mümkündür.
Modülerlik: Küçük ve birleştirilebilir (composable) fonksiyonlar yazılır, büyük davranış bunların bileşiminden oluşur. Aynı fonksiyon başka bir bağlamda değiştirilmeden kullanılabilir.
Hata ayıklama: Yan etki ve paylaşılan durum olmadığında, beklenmeyen bir sonucun nedeni fonksiyonun kendisinde ve girdisindedir. Kodun başka bir yerinin aynı durumu değiştirip değiştirmediğini izlemek gerekmez.
İmperatif ve Fonksiyonel Yaklaşım: Bir Örnek
Bir listedeki çift sayıların karesini almak istediğinizi düşünün. İmperatif çözüm boş bir dizi oluşturur, döngü kurar, her elemanı kontrol eder ve sonucu biriktirir. Fonksiyonel çözüm aynı işi iki dönüşümün bileşimi olarak ifade eder: önce çift sayıları süz, sonra karesini al.
const numbers = [1, 2, 3, 4, 5, 6];
// İmperatif: sonucu adım adım biriktirir
const squaresLoop = [];
for (const n of numbers) {
if (n % 2 === 0) {
squaresLoop.push(n * n);
}
}
// Fonksiyonel: dönüşümleri birleştirir, numbers dizisi değişmez
const squares = numbers.filter(n => n % 2 === 0).map(n => n * n);
console.log(squaresLoop, squares); // [4, 16, 36] [4, 16, 36]
İkinci sürüm ne istendiğini söyler; döngü ve geçici dizi filter ile map’in içine taşınmıştır. Aynı fikir C#‘ta LINQ ile Where ve Select olarak karşınıza çıkar.
Temel Örnekler
// Saf fonksiyon: aynı girdiye her zaman aynı sonucu verir
const add = (a, b) => a + b;
// Yüksek mertebeden fonksiyon: fonksiyon alır, fonksiyon döndürür
const twice = fn => x => fn(fn(x));
const addOne = x => x + 1;
const addTwo = twice(addOne);
console.log(addTwo(5)); // 7
// Bileşim: compose(f, g)(x) = f(g(x))
const compose = (f, g) => x => f(g(x));
const addThenDouble = compose(
x => x * 2, // ikinci adım
x => x + 10 // ilk adım
);
console.log(addThenDouble(5)); // 30
Gerçek Projelerde Fonksiyonel Programlama
Veri dönüşümü: API’den gelen veriyi arayüze hazırlamak için map, filter ve reduce zincirleri kullanılır.
Olay akışları: RxJS gibi kütüphaneler olay akışlarını (stream) map ve filter gibi operatörlerle birleştirilebilir dönüşümler olarak işler.
Durum yönetimi: Redux’ta reducer, (state, action) => newState biçiminde saf bir fonksiyondur. Redux belgeleri bu tasarımın Flux mimarisinden ve Elm’den esinlendiğini anlatır.
Veri işleme: Apache Spark gibi dağıtık işleme motorları veri kümelerini map, filter ve reduce benzeri dönüşümlerle işler.
Yaygın Desenler
Map, filter, reduce: Veri dönüşümlerinin büyük kısmı bu üç işlemin bileşimiyle ifade edilebilir.
Özyineleme (recursion): Döngü yerine özyineleme kullanılır. Özyinelemeli çağrı fonksiyonun son işlemiyse (kuyruk çağrısı, tail call) dil bu çağrıyı yığını büyütmeden çalıştırabilir; buna kuyruk çağrısı optimizasyonu (TCO) denir. Bu güvence yalnız kuyruk konumundaki çağrılar için geçerlidir ve dile göre değişir:
- Scheme standardı kuyruk çağrılarının yığını büyütmemesini zorunlu kılar; Erlang da kuyruk çağrılarını optimize eder.
- Clojure, JVM üzerinde TCO yapmaz; yığın tüketmeden döngü kurmak için
recurkullanılır. - Scala yalnız kendini doğrudan çağıran fonksiyonu döngüye çevirir;
@tailrecnotasyonu bunun gerçekleştiğini derleme zamanında denetler. - JavaScript’te ES2015 kuyruk çağrılarını strict mode için tanımlar. WebKit bunu 2016’da Safari’nin motoru JavaScriptCore’a ekledi; V8 ekibi aynı yıl özelliği uygulamış, ama tasarım tartışmaları sürdüğü için yayımlamamıştı. Bu yüzden taşınabilir JavaScript kodu TCO’ya güvenemez: örneğin Node.js 22’de strict mode’daki kuyruk özyinelemeli bir fonksiyon bir milyon adım derinlikte
RangeError: Maximum call stack size exceededhatası verir.
Currying: Çok parametreli bir fonksiyonu, her seferinde tek parametre alan fonksiyonlar zincirine çevirmektir. Kısmi uygulama (partial application) ile bazı parametreleri önceden sabitlenmiş yeni fonksiyonlar üretilir: const add = a => b => a + b; tanımından sonra add(10), her sayıya 10 ekleyen bir fonksiyondur.
Monad’lar: Değeri bir bağlamla birlikte taşıyıp adımları aynı kuralla zincirlemenin yoludur. Bağlam yalnız yan etki değildir: Maybe eksik değeri, Either hatayı, IO ertelenmiş girdi/çıktıyı modeller. JavaScript örnekleri monad’lar üzerine yazıda yer alıyor.
Fonksiyonel Programlamanın Zorlukları
Öğrenme eğrisi: İmperatif bir geçmişten geliyorsanız problemi dönüşümler olarak düşünmeye alışmak zaman alır.
Performans: Her değişiklikte yeni dizi veya nesne oluşturmak bellek ve çöp toplama yükünü artırabilir. Kalıcı (persistent) veri yapıları yeni sürümü eskisiyle ortak parçaları paylaşarak üretir ve kopyalama maliyetini düşürür; Clojure’un koleksiyonları bu şekilde çalışır.
Hata ayıklama: Uzun yüksek mertebeden fonksiyon zincirlerinde yığın izleri (stack trace) anonim fonksiyonlarla dolar ve okunması zorlaşır.
Ekip uyumu: Ekipteki herkesin aynı soyutlamaları bilmesi gerekir. Aşırı soyut kod, yazandan başkası için okunması güç hâle gelebilir.
Hangi Dilleri Öğrenmeli?
Saf fonksiyonel diller: Haskell ve Elm. Yan etkiler tür sisteminde ayrı tutulur: Haskell’de IO türüyle, Elm’de çalışma zamanının yürüttüğü komutlarla (Cmd).
Fonksiyonel öncelikli diller: Clojure (JVM), F# (.NET), OCaml, Erlang ve Elixir. Fonksiyonel üslubu varsayılan yaparlar, ama yan etkiye tür düzeyinde bir kısıt koymazlar.
Çok paradigmalı diller: JavaScript (birinci sınıf fonksiyonlar ve dizi metotları), Python (functools, itertools), Scala (nesne yönelimli ve fonksiyonel), Kotlin ve C# (lambda ifadeleri ve LINQ).
Önerim: Bildiğiniz dilde, örneğin JavaScript’te başlayın; kavramları öğrenmek için yeterlidir. Ardından yan etkilerin tür sistemiyle nasıl ayrıldığını görmek için Haskell ya da Elm gibi saf bir dil iyi bir sonraki adımdır.
Fonksiyonel ve Nesne Yönelimli Programlama
İkisi birbirinin alternatifi olmak zorunda değildir; farklı problemlere farklı araçlar uyar.
Fonksiyonel yaklaşımın güçlü olduğu yerler: veri dönüşümü, paralel işleme, karmaşık iş kuralları, matematiksel hesaplama.
Nesne yönelimli yaklaşımın güçlü olduğu yerler: kimliği ve yaşam döngüsü olan varlıkları modellemek, simülasyonlar, arayüz bileşen hiyerarşileri.
Birçok modern kod tabanı ikisini birleştirir. React’te bileşenler fonksiyondur ve durum güncellemeleri mevcut nesneyi değiştirmek yerine yeni değer üretir. Redux’ta reducer saf bir fonksiyondur. Domain-Driven Design projelerinde varlıklar nesnelerle modellenirken iş kuralları saf fonksiyonlara taşınabilir.
Üretimde Fonksiyonel Diller
WhatsApp: 2011’de yayımladığı bir blog yazısında arka ucunun FreeBSD ve Erlang üzerinde çalıştığını ve tek bir sunucuda bir milyonun üzerinde eşzamanlı TCP bağlantısına ulaştığını duyurdu.
Jane Street: Finans şirketi, statik tipli fonksiyonel bir dil olan OCaml’ı ana geliştirme platformu olarak kullanıyor.
Bu örnekler ölçeğin ya da doğruluğun tek başına dil seçiminden geldiğini değil, fonksiyonel dillerin büyük üretim sistemlerinde kullanıldığını gösterir.
Başlamak İçin İpuçları
-
Küçük başlayın: Her şeyi bir anda dönüştürmeye çalışmayın; önce yardımcı fonksiyonlarınızı saf hâle getirin.
-
Değişmezliği alışkanlık edinin: Nesneleri
{ ...eski, alan: yeni }ya daObject.assign({}, eski, yeni)ile kopyalayarak güncelleyin;Object.assign(hedef, ...)ilk argümandaki nesneyi değiştirir. Dizilerdesort,push,splicevereversediziyi yerinde değiştirir. Bunların yerinetoSorted,toSpliced,toReversedvewith(ES2023; Node.js 20+, Chrome 110+, Firefox 115+, Safari 16+) ya damap,filterveconcatyeni dizi döndürür:const user = { name: "Ayşe", age: 30 }; const older = { ...user, age: 31 }; // user değişmez const scores = [3, 1, 2]; const sorted = scores.toSorted(); // [1, 2, 3], scores hâlâ [3, 1, 2] const updated = scores.with(0, 10); // [10, 1, 2] -
Yan etkileri sınırda toplayın: İş mantığını saf tutun; veritabanı, ağ ve dosya erişimini uygulamanın kenarındaki ince bir katmana taşıyın.
-
Test yazın: Saf fonksiyonları test etmek kolaydır; test öncelikli geliştirme (TDD) için de uygundur.
-
Bileşimi öğrenin: Küçük fonksiyonları birleştirerek büyük davranışlar kurun.
Sonuç
Fonksiyonel programlama bütün sistemi tek bir paradigmaya zorlamak zorunda değildir. Saf hesaplamaları girdi/çıktı sınırlarından ayırmak, veriyi yerinde değiştirmemek ve küçük dönüşümleri birleştirmek, nesne yönelimli veya prosedürel bir kod tabanında da hata yüzeyini daraltır. Büyük veri üzerinde sık güncelleme yapan performans kritik döngülerde ise kopyalama maliyeti ölçülerek karar verilmelidir. Amaç belirli bir üsluba bağlılık değil, davranışı daha açık ve test edilebilir hâle getirmektir.