Yıl 1930’lar. The Great Depression. Amerikan ekonomisinin yerle bir olması, paranın değerinin düşmesi, ekonomik kriz… Tüm gelişmiş ülkeler o zamanlar bu krizden etkilenir.
O zamanlar Princeton üniversitesinde Alonzo Church, Alan Turing ve Jon von Neumann çeşitli araştırmalar yapıyorlar. Bu üç adamın çalıştığı konu genel olarak formal sistemler. Hesaplamanın ve hesaplamanın otomatikleştirilmesi gibi sorunlara çözüm arıyorlar. Alonzo Church lambda calculus isminde bir formal sistem geliştiriyor. Aslında bu sisteme hayali bir makine için kullanılacak bir programlama dili diyebiliriz. Dilin temelinde fonksiyonlar var, parametre olarak fonksiyon alıyor ve fonksiyon döndürüyor.
O sıralarda Alan Turing, Alonzo’dan bağımsız bir şekilde benzer bir konu üzerinde çalışıyor. Günümüzde Turing Machine olarak bildiğimiz formal sistemi geliştirmekte. Alonzo Church ile benzer sonuçlara ulaşıyor. Daha sonra bu iki yapının hemen hemen eşit güçlere sahip oldukları görülüyor.
Şimdi diğer taraftan olaya bakalım. Amerikan savunma sanayisi matematikçileri toplamış habire diferansiyel denklem çözdürüyor, amaç hedeflerin vurulma yüzdelerini arttırmak (burada gülüyoruz). Yine aynı kişiler 2. Dünya Savaşı’na hazırlanılırken boş durmuyor, Mark I ismi verilen balistik cetvelleri çözmek için ilk makineyi geliştiriyor (bu makine IBM tarafından geliştiriliyor). Tam olarak 5 ton ve 750.000 parçaya sahip bu makine, saniyede 3 operasyon yapıyor.
1949 yıllarına geldiğimizde Electronic Discrete Variable Automatic Computer (EDVAC) adında yeni bir bilgisayar geliştiriliyor ve çok başarılı oluyor. Bugün von Neumann mimarisi diye bildiğimiz mimarinin ilk uygulanmış hali bu bilgisayarda kullanılıyor. Turing makinesinin ilk gerçek uygulamasıda burada gerçekleştiriliyor.
1950’lerin sonunda MIT’de profesör olan John McCarthy Alonzo Church’ün çalışmalarına merak sarıyor ve Lisp dilini ortaya çıkarıyor. Lisp lambda calculus’un bir uygulaması ve von Neumann bilgisayarları üzerinde çalışıyor.
Dediğim gibi fonksiyonel programlama Alonzo Church’ün fikirlerinin bir implementasyonu. Tüm lambda calculus ifadeleri çevrilmiş durumda değil çünkü lambda calculus fiziksel limitlerin olduğu bir ortam için düşünülmemiş. Bu nedenle nesne yönelimli programlamaya benzer olarak sıkı sıkıya bağlı bir rehberi yok. Bundan dolayı çokça fonksiyonel dil var.
Fonksiyonel programlamada fonksiyonlar hemen hemen her şeyde kullanılır. Hatta değişkenler yerine fonksiyonlar kullanılır. Her değişken tanımlaması (değişken tanımlaması yanlış bir tabir aslında) basitçe expression olarak adlandırılabilir. Bu tanımlı expression’lar bir kere atanır ve sonradan değiştirilemez. Java final C++ const anahtar kelimeleri ile belirttiğimiz değişkenler bununla eşdeğer sayılabilir. Fonksiyonel programlamada final olmayan bir veri yoktur.
Şimdi merak ediyorsunuz hiç bir değişken ve durum değiştirmeden nasıl olacak bu iş ? Lamda calculus hesaplama ile de ilgilenir. Imperative dillerin yaptığı her şeyi fonksiyonel diller de yapabilir, ama nasıl ? Nasıl tam olarak aynı sonuçları elde ederiz ?
Fonksiyonel diller durumları (state) tutar fakat bunları değişkenler kullanmadan yapar. Onların yerine fonksiyonları kullanır. Durum bir stack üzerinde fonksiyon parametrelerinde tutulur, Eğer bir durum tutmak ve belirli anlarda değiştirmek istiyorsanız (döngü gibi) rekürsif bir fonksiyon yazmalısınız.
Fonksiyonel Programlamanın Faydaları
Neden fonksiyonel dilleri kullanalım? Bir kaç tanesini anlatmaya çalışacağım.
Unit Testing ve Debugging
Fonksiyonel programlamada her şeyin final tanımlandığını söylemiştik (tabii ki böyle bir şey yok, anlaşılması kolay olması açısından bu şekilde anlatmayı mantıklı buldum). Bununda etkisiyle hiç bir fonksiyon yan etkiye (side effect) sebep olmaz. İlgilenmeniz gereken tek şey fonksiyon argümanlarıdır. Fonksiyonların doğru sırada çağırılıp çağırılmadığı bizi ilgilendirmez. Bu sayede programı test ve debug etmek çok daha kolay olur.
Concurrency (Eşzamanlılık)
Fonksiyonel diller hiç bir ek modifiye gerektirmeden eşzamanlı olarak çalışır! Hiç bir lock kullanmadan hemde. İstediğinz kadar thread kullanabilirsiniz, race condition yok, deadlock yok.
1 2 3 |
String s1 = uzunBirIsYap(); String s2 = uzunBirIsYap2(); String s3 = birlestir(s1, s2); |
Yukarıdaki Java bloğunu paralel olarak çalıştırırsanız ne sonuç alacağınızı kimse bilemez. Çünkü hangi thread ne zaman hangi satırda bilemeyiz. Ama fonksiyonel dillerde yan etki olmadığı için sıranın önemi yoktur.
Biraz Daha Derine İnelim
Fonksiyonel programlama hesaplama (computation) yaparken durum (state) ve değişken veriden (mutable data) kaçınmaya çalışan, değerlendirme yöntemi (evaluation) olarak matematiksel fonksiyonlara benzer fonksiyonlar kullananan bir programlama paradigmasıdır.
Programlama Paradigması
Programlama paradigması programın oluşturma sürecinde kullanılan kavramsal modeldir. En çok kullanılan programlama paradigması (varsayım) nesne yönelimli programlamadır. Diğer yaygın paradigmalardan ikisi imperative ve declarative programlamadır. Diğer paradigmaların listesine buradan ulaşabilirsiniz:
http://en.wikipedia.org/wiki/List_of_multi-paradigm_programming_languages#Paradigm_summaries
Yaygın olarak kullandığımız diller genellikle çoklu-paradigma dillerdir. Birden fazla paradigmayı barındırır. Örneğin OOP Imperative, FP Declarative olma eğilimindedir.
Örnek verecek olursak Scala hem nesne yönelimli hem de fonksiyonel bir dildir.
Yukarıda bahsettiğim kavramları biraz daha genişletmeye çalışacağım.
Hesaplama (Computation)
Programın yaptığı her hangi bir şey hesaplamadır.
Değerlendirme Yöntemi Olarak Matematiksel Fonksiyonlar
Fonksiyonel programlama bu konsept üzerine kuruludur. OOP’te biz fonksiyonları metodlar olarak biliriz ve nesnelerin durumlarını değiştirmek için kullanırız. Matematiksel fonksiyonlarda durum veya nesne kavramı yoktur. Matematiksel fonksiyonlarda bir değer verilir ve o değer hesaplanır.
1 |
f ( x ) = x * x + 3 |
Bu fonksiyona x bir değer geçiririz ve yine bir değer alırız. Değişen durum (state) yoktur.
Durumdan ve Değişken Veriden Kaçınmak
Fonksiyonel paradigmada değişkenlerle çalışmıyoruz. Bunun yerine değerlerle (value) çalışıyoruz. Bunu kavramak ne kadar zor olsada bir o kadar da zorunlu. Çünkü fonksiyonel programlamanın merkezinde değişmez veri yapıları var.
Fonksiyonel Dillerin Özellikleri
First Class Fonksiyonlar
Bu fonksiyonlar bize fonksiyonların temel tip olduğunu söylüyor.
- Her hangi bir fonksiyon başka bir fonksiyonu parametre olarak alabilir
- Her hangi bir fonksiyon dönüş değeri olarak verilebilir
- Her hangi bir fonksiyon bir değer olarak atanabilir
Aşağıdaki durum literatürde closure olarak geçer.
1 2 3 4 5 |
def arttirici(inc: Int): (Int => Int) = (x: Int) => x + inc val a = arttirici(10) // a: (Int) => Int = val b = a(25) // res4: Int = 35 |
1 2 3 4 5 6 7 8 |
Function usAldiriciFnYap(int power) { int usAlFn(int base) { return pow(base, power); } return usAlFn; } Function kare = usAldiriciFnYap(2); square(3); // 9 döner |
High Order Fonksiyonlar
Bir fonksiyon başka bir fonksiyona parametre olarak geçirilebilir veya sonuç olarak döndürülebilir.
1 2 3 4 5 6 |
class EklemeFonksiyonu { int ekle(int i, int j) { return i + j; } } EklemeFonksiyonu ekle = new EklemeFonksiyonu(); |
Java high order classes bir dil ama bunu kimse reklam etmiyor 🙂 Neden high order classes? Çünkü artık bu sınıfı başka bir fonksiyona parametre olarak geçirebiliriz. Burada da buna benzer bir mantık var.
1 2 3 4 5 6 7 8 9 10 |
class MessageHandler { void handleMessage(Message msg) { // ... msg.setClientCode("ABCD_123"); // ... sendMessage(msg); } // ... } |
Diyelim ki bir sunucuya bir mesaj geliyor gelen mesaj paketlenip başka bir sunucuya gönderiliyor.
Diyelim ki mimari değişti ve artık mesaj gönderen 2 sunucumuz var. Yani bu sunucuların her biri birer istemci aynı zamanda. Gönderen sunucuya göre mesajı paketleyeceğiz. Mesajı kimin gönderdiğini öğrenmemiz gerek şöyle bir hal aldı kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MessageHandler { void handleMessage(Message msg) { // ... if(msg.getDestination().equals("server1") { msg.setClientCode("Server1_x_y"); } else { msg.setClientCode("Server_2_x_y"); } // ... sendMessage(msg); } // ... } |
Evet bir çözüm. Ama ölçeklenebilir bir çözüm değil. Bir sunucu daha eklediğimizi farz edelim kodu tekrar değiştirmemiz gerekecek çünkü şuan ki kod 2 sunucu için doğru çalışır. Nesne yönelimli programlamanın nimetlerinden faydalanalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
abstract class MessageHandler { void handleMessage(Message msg) { // ... msg.setClientCode(getClientCode()); // ... sendMessage(msg); } abstract String getClientCode(); // ... } class MessageHandlerOne extends MessageHandler { String getClientCode() { return "Server_1_x_y"; } } class MessageHandlerTwo extends MessageHandler { String getClientCode() { return "Server_2_x_y"; } } |
Şimdi yeni bir sunucu ekleme işlemi daha yönetilebilir bir halde. İyi uğraştık. Bu işlemi high order functions kullanabildiğimiz bir dilde yapmaya çalışalım şimdi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class MessageHandler { void handleMessage(Message msg, Function getClientCode) { // ... Message msg1 = msg.setClientCode(getClientCode()); // ... sendMessage(msg1); } // ... } String getClientCodeOne() { return "Server_1_x_y"; } String getClientCodeTwo() { return "Server_2_x_y"; } MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne); |
Ne oldu ? Yeni bir tip tanımlamamıza gerek kalmadı, sınıf hiyerarşisi yok.
Bir örnek daha:
1 2 3 4 |
def apply(f: Int => String, v: Int) = f(v) def layout[A](x: A) = "[" + x.toString() + "]" // println( apply( layout, 10) ) //[10] |
Pure Fonksiyonlar
Side effect barındırmayan fonksiyonlara denir. Side effect bir fonksiyonun scope alanının dışında her hangi bir veride değişiklik yapmasıdır. Pure fonksiyonların yan etkisi yoktur. Böylece paralel programlamada farklı thread’lerin farklı değer görme durumunun önüne geçmiş oluruz.
1 2 |
def pureFonk(x : Int, y : Int) = x + y def pureOlmayanFonk(x : Int, y : Int) = println(x + y) |
Currying
Çok parametreli bir fonksiyonun parametre sayısının azaltımasına currying denir. Fonksiyonlara iç içe değerleri paslamaktır.
1 2 3 4 5 6 |
def divisibleby(factor : Int) (value : Int) = value % factor == 0 val evens = (1 to 10).filter(divisibleby(2)) def add(x:Int, y:Int) = x + y val addCurried = Function.curried(add _) add(1, 2) // 3 addCurried(1)(2) // 3 |
Recursion
Recursive fonksiyonları muhtemelen biliyorsunuz. Fonksiyonel dillerde recursion çok önemli bir yerde duruyor. Bildiğiniz gibi rekürsif fonksiyonlar kendi içinde kendisini tekrar çağıran fonksiyonlardır. Belirli bir durma koşulu vardır.
Aşağıda bir tail recursion uygulaması görüyoruz.
1 2 3 4 5 6 7 8 9 |
static int Reduce(Func<int, int, int> reducer, IEnumerable<int> values) { int accum = 0; foreach (var i in values) { accum = reducer(accum, i); } return accum; } |
Kullandığımız foreach yerine şimdi recursion kullanarak fonksiyonu yazalım.
1 2 3 4 5 6 7 8 |
static int ReduceF(Func<int, int, int> reducer, IEnumerable<int> values, int seed) { if (!values.Any()) return seed; // #5 var first = values.First(); // #1 var remainder = values.Skip(1); // #2 var next = reducer(seed, first); // #3 return ReduceF(reducer, remainder, next); //#4 } |
reducer: Yapacağımız işlemi içeren fonksiyon
values: Tutulan değerler
seed: Tohum, default değer olarak belirtilebilir
ReduceF aynı zamanda high-order ve pure fonksiyonlara örnektir.
Bir üstteki Reduce fonksiyonunda acum değişkeninde durum tutuyoruz. Her iterasyonda değeri değişiyor. Bu yüzden Reduce pure bir fonksiyon değil.
ReduceF fonksiyonunu refactor edersek:
Memoization
memoization kelimesi latince memorandum kelimesinden geliyor. Yani hatırlamak diyebiliriz. Aynı inputlarla çağırılan fonksiyonların sonuçlarının arka tarafta hatırlanarak tekrar hesaplanmadan direk sonucun döndürülmesidir. Böylece gereksiz yere tekrardan hesaplama yapılmaz.
Lazy Evaluation
1 2 3 |
String s1 = uzunBirIsYap(); String s2 = uzunBirIsYap2(); String s3 = birlestir(s1, s2); |
Yukarıda bu kod bloğundan bahsetmiştim ve eşzamanlılık konusuna değinmiştim. Imperative bir dillde evaluation açık seçiktir. Sıra ile metodları çalıştırır. Genelde her hangi bir fonksiyon yan etkisi yoktur ve global state’e bağlı değildir. Örneğin Haskell lazy evaluation’a sahip ve sıra ile çalışacağını garanti etmiyor. Fonksiyonlar ihtiyaç olan zamanlarda çalışır.
Not: Bu yazı kendi düşüncelerimi, bildiklerimi ve çeşitli blog yazılarından parçaları içermektedir. Aynı zamanda Programming in Scala kitabından da faydalanılmıştır.