Serhat Sağlık
Yazılım Geliştirme Uzmanı

Java'da Fonksiyonel Programlama

Java'da Fonksiyonel Programlama

Giriş

Fonksiyonel programlama, fonksiyonlar kullanarak program tasarlama modelidir. Obje Tabanlı Programlama modeline göre güçlü ve zayıf olduğu yönler bulunur. Sektörde oldukça popüler ve aslında obje tabanlı olan Java programlama diline; Java 8 sürümüyle birlikte, fonksiyonel programlama konseptlerini uygulamamıza imkân tanıyan yenilikler eklenmiştir. Bu sayede hem obje tabanlı programlama hem de fonksiyonel programlamanın iyi yönleri harmanlanarak daha verimli uygulamalar tasarlanabilir hale gelmiştir.

Java’da fonksiyonel programlama araçları; pratik işlevler yazmada, okumada oldukça kolaylık sağlamaktadır. Karmaşık döngüler Stream API sayesinde sade bir hale getirilebilir; birçok yerden çağırılabilecek fonksiyonel arayüzler kullanılarak bazı kontroller ve işlevler; kolay okunabilir, debug ve test edilebilir, yönetilebilir şekilde yazılabilmektedir.

Bu faydaların her birinin çalışma prensibi ayrı ayrı açıklanmış ve anlaşılması için kod örnekleri ve çıktıları belirtilmiştir. Güncel sürümü kullanma imkânı olmayan öğrenci ve çalışanlar için yarar sağlayacağı düşünülerek bu çalışma hazırlanmıştır.

Türkçe bir kaynak yazılması ve araştırma dolayısı ile artan bilgi birikiminin, sektördeki çalışanlar arasındaki etkileşimle yayılarak bilgi hazinesinin bir parçası haline gelmesi, yeni başlayan yazılımcıların entegrasyonunu kolaylaştıracak ve eski çalışanları daha fazla öğrenmeye teşvik edecektir.

Bu çalışma Java 8’de eklenen yenilikler olan fonksiyonel arayüzleri, lamda işaretini kapsamaktadır. Fonksiyonel programlama tanımlanacak, fonksiyonel programlama ve obje tabanlı programlamanın farklarından bahsedilecek, Java’ya yeni eklenmiş özellikler ve bu özellikleri kullanarak fonksiyonel programlama mantığına uygun işlemler yapmak için örnekler listelenecektir. Bu bağlamda; fonksiyonel programlama hakkında farkındalık yaratarak Java dilinde fonksiyonel programlama mantığına uygun kodlar yazmayı öğrenmek ve uygulamak isteyenler için kolay anlaşılabilir bir kaynak sunma hedeflemektedir.

Fonksiyonel Programlama Nedir?

Fonksiyonel programlama, bir uygulamanın verilerini ve durumunu değiştirmeden tıpkı bir matematik fonksiyonu gibi bir girdi alıp çıktı üreten fonksiyonlar kullanılarak yapılan programlamadır. Prensip olarak kökenini 1930’lu yıllarda Alonzo Church tarafından tanımlanmış lamda kalkülüs modelinden alır. Bu modele göre örneğin \(hipotenüs(x,y) = \sqrt{x^2 + y^2}\) fonksiyonu isimsiz bir şekilde \((x,y) = \sqrt{x² + y²}\) olarak yazılır.

Fonksiyonel Programlama - Obje Tabanlı Programlama Farkları

Fonksiyonel Programlama Obje Tabanlı Programlama
Değişmez (immutable) veri kullanır. Bir kısıtlaması bulunmaz.
"Ne yapar" odaklı bildirimsel (declarative) programlama modeli kullanır. "Nasıl yapar" odaklı zorunlu (imperative) programlama modeli kullanır.
İfadeler (statement) herhangi bir sırada çalışabilir. İfadeler belli bir sırada çalışır.
Yinelemeli (iterative) veriler için özyineleme (recursive) kullanır. Yinelemeli veriler için döngü (loop) kullanır.
Değişkenler ve fonksiyonlar temel öğeleridir. Nesneler ve modeller temel öğeleridir.
Çok farklı işlemler yapılan az şey için kullanılır. Az işlem yapılan çok farklı şeyler için kullanılır.
Paralel programlamayı destekler. Paralel programlamayı desteklemez
Veri gizlemeyi desteklemez. Veri gizlemeyi destekler.


Tablodan görülebileceği gibi iki programlama tipi de farklı bir yaklaşımı benimsemektedir. Obje tabanlı programlamada (OOP) var olan objeleri/metotları kullanarak yeni objeler ve metotlar yaratmak kolaydır. Var olan işlemleri kullanarak yeni veri tiplerine kolay adapte olur ve kalıtım (inheritance) burada büyük rol oynar. Bu sebeplerle, uzun süreli devam eden ve gittikçe karmaşıklaşan projeler için daha uygundur. Fonksiyonel programlama (FP) sabit bir veri üzerine yeni işlemler eklemek için iyidir ve eski işlemler aynı şekilde kullanılmaya devam eder. Ancak, veri tipi değişikliklerine daha zor adapte olur. Obje tabanlı bir dil olan Java, fonksiyonel programlamaya uygun olmasa da Java 8 sürümüyle birlikte lamda fonksiyonları ve fonksiyonel arayüzlerin (interface) eklenmesiyle fonksiyonel programlamanın faydalarından yararlanılmasına imkan vermektedir.

Fonksiyonel Arayüzler

Fonksiyonel arayüz (interface), yalnızca bir soyut (abstract) metoda sahip arayüzlere denir. Tekil Soyut Metot Arayüz (Single Abstract Method Interface - SAM) ismiyle de bilinirler. Dolayısıyla tek bir iş yaparlar. Java 8 ile birlikte eklenmiş olan lamda fonksiyonları, SAM arayüzleri için kullanılabilir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
    public static void main(String args[]) {
        eskiYontem();
        yeniYontem();
    }

    
    public static void eskiYontem() {
        new Thread(new Runnable() {
            @Override public void run() {
                System.out.println("Klasik yontemle thread yaratma");
            }
        }).start();
    }

    
    public static void yeniYontem() {
        new Thread(() - > {
            System.out.println("Yeni yontemle thread yaratma");
        }).start();
    }
}

Örnekteki kod Runnable interface’ini iki yöntemle de kullanarak iki tane thread yaratacak ve çıktı olarak şunu üretecektir:

1
2
Klasik yontemle thread yaratma
Yeni yontemle thread yaratma

Bir arayüzün fonksiyonel arayüz olduğunu belirtmek için \@FunctionalInterface etiketi (annotation) kullanılır. Metot tanımında abstract terimi (keyword) kullanılmak zorunda değildir, çünkü zaten ön tanımlı olarak abstract olmalıdır. Örnek olarak TestFonksiyonelArayüz arayüzü verilebilir.

1
2
3
4
@FunctionalInterface
public interface TestFonksiyonelArayüz {
    int birSeyYap();
}

@FunctionalInterface’den dolayı eğer ikinci bir metot eklenseydi, program çalıştırıldığında derleyici (compiler) TestHataliFonksiyonelArayüz arayüzünün fonksiyonel arayüz tanımına uymadığını belirten aşağıdaki hatayı atacaktı ve program çalışmayacaktı.

1
2
3
4
5
@FunctionalInterface
public interface TestHataliFonksiyonelArayüz {
    int birSeyYap();
    int baskaBirSeyYap();
}
1
2
3
4
java: Unexpected @FunctionalInterface annotation
TestHataliFonksiyonelArayüz is not a functional interface
multiple non-overriding abstract methods found in interface
TestHataliFonksiyonelArayüz

Dört farklı fonksiyonel arayüz tipinden söz edilebilir:

  • Consumer
  • Supplier
  • Predicate
  • Function

Consumer

Consumer tüketici anlamına gelir ve bu arayüzler tek bir argüman (argument) alıp hiçbir değer dönmezler. Örnek kullanım:

1
2
3
4
5
6
7
8
public class Main {
	static Consumer<Character> charConsumer = ch -> System.out.println(ch);

	public static void main(String args[]) {
		Character[] charArray = {"t", "e", "s", "t"};
		Arrays.stream(charArray).forEach(charConsumer);
	}
}

Bu program bize aşağıdaki çıktıyı verecektir:

1
2
3
4
t
e
s
t

Consumer arayüzüne benzer olarak bir yerine iki argüman alan Bi-Consumer arayüzü de bulunmaktadır.

1
2
3
4
5
public static void main(String[] args) {
    Map<String, Integer> harcliklar = Map.of("mehmet",100, "ahmet", 200);
    BiConsumer<String, Integer> biConsumer = (kisi, harclik) -> System.out.println(kisi + "'in harçlığı: " + harclik);
    harcliklar.forEach(biConsumer);
}

Örneğin, bu consumer map içindeki kişi ve sayı değerlerini alarak aşağıdaki çıktıyı verir:

1
2
ahmet'in harçlığı: 200
mehmet'in harçlığı: 100

Ön tanımlı olarak ikiden fazla argüman alan tipi bulunmaz. Fakat argümanlardan birisi BiConsumer olan farklı bir BiConsumer kullanılarak, üç argümanlı veya benzer şekilde daha fazla argüman alan çeşitleri tanımlanabilir.

Consumerların çalıştıktan sonra verileri tükettiklerinden ve hiçbir değer döndürmediklerinden bahsettik fakat birden fazla işlem yapmaya ihtiyaç varsa consumerları ardı ardına çalışacak şekilde bağlamamıza imkân tanıyan andThen kullanılabilir.

Örneğin aşağıdaki kodda her bir liste elemanı için ilk önce elemanın kendisini çıktı veren consumer, ardından da elemanın bir fazlasının çıktısını veren consumer çalışacak ve sırasıyla 1,2,3,4,5,6 çıktıları alınmış olacaktır.

1
2
3
4
5
6
Consumer<Integer> consumer1 = i -> System.out.println(i);
Consumer<Integer> consumer2 = i -> System.out.println(i+1);
List<Integer> sayiList = Arrays.asList(1, 3, 5);

sayiList.stream().forEach(i -> consumer1.andThen(consumer2).accept(i));
sayiList.stream().forEach(consumer1.andThen(consumer2));

İki farklı kullanımı gösterilmiş andThen örnekleri aynı şekilde çalışmaktadır ve ikisi de ayrı olarak aşağıdaki çıktıyı verir:

1
2
3
4
5
6
1
2
3
4
5
6

Supplier

Supplier tedarikçi anlamına gelir ve bu arayüzler mantık olarak consumer’ın tersidir denebilir. Argüman almazlar fakat çıktı üretirler. Bu arayüz içinde tanımlı olarak sadece get metodu bulunur.

1
2
3
4
5
6
7
public class Main {
    static Supplier < String > messageSupplier = () - > "Test Mesaji";

    public static void main(String args[]) {
        System.out.println(messageSupplier.get());
    }
}

Örneğin, bu örnekte çıktımız messageSupplier tarafından dönülen string’in kendisi olacaktır:

1
Test Mesaji

Supplier argüman almadığı için BiSupplier gibi versiyonları vardır, fakat belli tipleri dönen versiyonları bulunmaktadır. Örneğin: IntSupplier, LongSupplier, BooleanSupplier, DoubleSupplier.

IntSupplier için örnek kullanım şu şekilde gösterilebilir:

1
2
3
4
5
6
7
8
public class Main {
    static IntSupplier birdenOnaSayiSupplier =
        () -> (int)(Math.random() * 10 + 1);
    
    public static void main(String args[]) {
        System.out.println(birdenOnaSayiSupplier.getAsInt());
    }
}

Bu supplier bize 1-10 arasında rastgele bir sayı değeri dönecektir. Örnekten görülebileceği gibi bu arayüzler get metodu yerine getAsX şeklinde metotlar kullanır.

Predicate

Predicate dayanak gibi bir anlama gelir ve bu arayüzler bir argüman alıp üzerinde kontrol mantığı çalıştırıp boolean değer dönerler. Bu arayüz içinde test metodu bulunur ve argüman üzerinde kontrol mantığını test eder.

1
2
3
4
5
6
7
8
public class Main {
    static Predicate < Integer > isCiftSayi = (i) - > i % 2 == 0;

    public static void main(String args[]) {
        System.out.println(isCiftSayi.test(10));
        System.out.println(isCiftSayi.test(9));
    }
}

Yukarıdaki örnekte predicate sayıların çift mi tek mi olduğunu kontrol eder ve istenilen int değeri üzerinde bunu test eder. Örnekteki çıktı aşağıdaki şekilde olacaktır:

1
2
true
false

Consumer’da olduğu gibi predicate için de iki argüman alan BiPredicate versiyonu bulunur.

1
2
3
4
5
6
7
8
public class Main {
    static BiPredicate<Integer, Integer> esitMi = (x, y) -> x == y;

    public static void main(String args[]) {
        System.out.println(esitMi.test(10, 10));
        System.out.println(esitMi.test(11, 9));
    }
}

Bu predicate sayı değerlerini karşılaştırarak eşitlik durumunu döner. Çıktımız şu şekilde olacaktır:

1
2
true
false

Bu arayüzün de IntPredicate, LongPredicate gibi tipe özel çeşitleri bulunmaktadır. İlk örnek olan isCiftSayi predicate arayüzü doğrudan IntPredicate olarak aşağıdaki şekilde yazılabilir:

1
2
3
4
5
6
7
8
public class Main {
    static IntPredicate isCiftSayi = (i) -> i % 2 == 0;

    public static void main(String args[]) {
        System.out.println(isCiftSayi.test(10));
        System.out.println(isCiftSayi.test(9));
    }
}

Bu aynı çıktıyı verecektir:

1
2
true
false

Birden fazla predicate kullanarak yeni bir kontrol mantığı oluşturmak istenirse and, or, not, negate metotları kullanılabilir.

Örneğin bir değerin çift sayı olup, altıya eşit olmadığını test edelim. Bunun için çift sayı olup olmadığını kontrol eden bir predicate ve altıya eşit olup olmadığını kontrol eden ikinci bir predicate tanımlayıp aşağıdaki gibi ikinci kontrolün değili alınarak kullanılabilir:

1
2
3
4
5
6
7
Predicate<Integer> ciftSayi = sayi -> sayi % 2 == 0;
Predicate<Integer> altiyaEsit = sayi -> sayi == 6;
Predicate<Integer> ciftSayiVeAltiyaEsitDegil = ciftSayi.and(altiyaEsit.negate());
System.out.println(ciftSayiVeAltiyaEsitDegil.test(3));
System.out.println(ciftSayiVeAltiyaEsitDegil.test(6));
System.out.println(ciftSayiVeAltiyaEsitDegil.test(4));

Bu kontrolün çıktısı da aşağıdaki gibi olacaktır:

1
2
3
false
false
true

Function

Function fonksiyon demektir ve bu fonksiyonel arayüz tipi matematikteki fonksiyon gibi düşünülebilir. Argüman üzerinde gerekli işlemleri yapıp herhangi bir tipte veri döner. Arayüzde tanımlı apply metodu ile argüman fonksiyona sokulabilir. Kendi dışındaki hiçbir değişkenin durumunu etkilemeyen ve kendi dışındaki şeylerden etkilenmeyen fonksiyonlara saf (pure) fonksiyon denir. Character tipinde veri alıp işlem sonrası Integer veri dönen örnek bir fonksiyon şu şekilde tanımlanabilir:

1
2
3
4
5
6
7
8
9
public class Main {
  static Function < Character, Integer > karakterinIntDegeriniGetir =
    (ch) -> Character.getNumericValue(ch);

  public static void main(String args[]) {
    System.out.println(karakterinIntDegeriniGetir.apply("a"));
    System.out.println(karakterinIntDegeriniGetir.apply("b"));
  }
}

Bu çıktı olarak karakterlerin Integer değerleri olan aşağıdaki çıktıyı verecektir:

1
2
10
11

Bu arayüzün de iki argümanlı hali olan BiFunction öntanımlı olarak bulunmaktadır. BiConsumer için verilen örnekte konsola yazdırılan String değerini, doğrudan dönen bir BiFunction şu şekilde tanımlanabilir ve kullanılabilir:

1
2
3
4
5
6
7
public static void main(String args[]) {
    BiFunction<String, Integer, String> charConsumerFunction = (str, i) -> str + "’in harçlığı: " + i;
    Map<String, Integer> harcliklar = Map.of("mehmet",100, "ahmet", 200);
    harcliklar.entrySet().forEach(
        mapEntry -> System.out.println(
            charConsumerFunction.apply(mapEntry.getKey(), mapEntry.getValue())));
}

Bu durumda da kod aynı çıktıyı verecektir:

1
2
ahmet’in harçlığı: 200
mehmet’in harçlığı: 100

Bu arayüzün de IntFunction, LongFunction gibi tipe özel çeşitleri bulunmaktadır. Predicate için verilen isCiftSayi örneği argüman olarak Integer kullanılacağından, IntFunction olarak ve boolean değer dönecek şekilde aşağıdaki gibi tanımlanabilir:

1
2
3
4
5
6
7
8
public class Main {
  static IntFunction < Boolean > isCiftSayi = i -> i % 2 == 0;

  public static void main(String args[]) {
    System.out.println(isCiftSayi.apply(10));
    System.out.println(isCiftSayi.apply(9));
  }
}

Bu durumda da yine çıktıyı verecektir:

1
2
true
false

Fonksiyonlar için ek olarak UnaryOperator ve BinaryOperator adlı iki arayüz bulunmaktadır. Bunlardan UnaryOperator, Function arayüzünden; BinaryOperator ise BiFunction arayüzünden extend eder. Farkları girdi ve çıktı tiplerinin aynı olmasıdır. Örneğin, iki sayının birbirine bölümünden kalan sayıyı bulmak için bir BinaryOperator şu şekilde tanımlanabilir:

1
2
3
4
5
6
7
public class Main {
  static BinaryOperator < Integer > bolumdenKalan = (x, y) -> x % y;

  public static void main(String args[]) {
    System.out.println(bolumdenKalan.apply(10, 3));
  }
}

Çıktı da aşağıdaki gibi olacaktır:

1
1

Ayrıca, üç argüman alan TriFunction gibi tipleri tanımlamak da mümkündür:

1
2
3
4
5
6
7
8
9
10
11
@FunctionalInterface
public interface TriFunction {
  int apply(int x, int y, int z);
}

public class Main {
  public static void main(String args[]) {
    TriFunction ucunuCarp = (x, y, z) -> x * y * z;
    System.out.println(ucunuCarp.apply(10, 3, 5));
  }
}

Bu şekilde üç adet int değeri alıp int değeri dönen bir TriFunction tanımlanarak, yapılacak işlem lamda fonksiyonu olarak kullanılan yerde tanımlanabilir ya da implementasyonu da yapıp aşağıdaki gibi çağırılabilir:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UcSayiyiCarp implements TriFunction {
  @Override
  public int apply(int x, int y, int z) {
    return x * y * z;
  }
}

public class Main {
  public static void main(String args[]) {
    UcSayiyiCarp ucSayiyiCarp = new UcSayiyiCarp();
    System.out.println(ucSayiyiCarp.apply(10, 3, 5));
  }
}

Alternatif olarak; fonksiyon içinde fonksiyon kullanmak istenirse, şu şekilde bir kullanım da olabilir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FunctionalInterface
public interface NestedFunction {
  Integer apply(BiFunction < Integer, Integer, Integer > f,
    Integer x, Integer y, Integer z);
}

public class NestedFunctionImpl implements NestedFunction {
  @Override
  public Integer apply(BiFunction < Integer, Integer, Integer > f,
    Integer x, Integer y, Integer z) {
    return f.apply(x, y) * z;
  }
}

public class Main {
  public static void main(String args[]) {
    BiFunction < Integer, Integer, Integer > function =
      (x, y) -> x * y;

    NestedFunction nestedFunction = new NestedFunctionImpl();
    System.out.println(nestedFunction.apply(function, 10, 3, 5));
  }
}

Her değerin ayrı ayrı apply edildiği, bir üçlü çarpım metodu şu şekilde de tanımlanabilir:

1
2
3
4
5
6
7
8
public class Main {
  public static void main(String args[]) {
    Function < Integer,
      Function < Integer,
      Function < Integer, Integer >>> ucluCarpim = x -> y -> z -> x * y * z;
    System.out.println(ucluCarpim.apply(10).apply(3).apply(5));
  }
}

Bu yönteme Currying denmekle birlikte, dört yöntemin de çıktısı şu şekilde olacaktır:

1
150

Eğer functionların basit bir şekilde birbiri ardına çalışmalarını sağlamak isteniyorsa andThen burada da kullanılabilir.

Örneğin aşağıdaki kodda yedi çıkarma ve ikiyle çarpma için iki farklı function tanımlanmış. Bunları art arda çalıştıracak yeni bir fonksiyonu andThen kullanarak tanımlanabilir. Burada ilk önce andThen’in çağrıldığı function çalışır ardından andThen’e argüman olarak geçilen function çalışır.

1
2
3
4
Function<Integer, Integer> yediCikar = sayi -> sayi - 7;
Function<Integer, Integer> ikiyleCarp = sayi -> sayi * 2;
Function<Integer, Integer> yediCikarVeIkiyleCarp = yediCikar.andThen(ikiyleCarp);
System.out.println(yediCikarVeIkiyleCarp.apply(10));

Çıktı aşağıdaki gibi olacaktır:

1
6

Function için ek olarak compose metodu da bulunmaktadır. Ve andThen’in tersi sırasıyla çalışır. Yani ilk önce compose’a argüman olarak geçilen function, ardından ise compose’un çağrıldığı function çalışır. Compose ile aynı çıktıyı almak için aşağıdaki gibi tanımlanması gerekirdi.

1
2
3
4
Function<Integer, Integer> yediCikar = sayi -> sayi - 7;
Function<Integer, Integer> ikiyleCarp = sayi -> sayi * 2;
Function<Integer, Integer> yediCikarVeIkiyleCarp = ikiyleCarp.compose(yediCikar);
System.out.println(yediCikarVeIkiyleCarp.apply(10));
Bu blog yazısı TÜBİTAK BİLGEM YTE Araştırma Serisi kapsamında teknik rapor olarak yayınlanmıştır.


Referanslar
  • Sheehan, L. (n.d.). Learning functional programming in go. Retrieved, November 14, 2022, https://www.oreilly.com/library/view/learning-functional-programming/9781787281394/6931699b-4fad-43fd-b201-85d104c25222.xhtml
  • Pedamkar, P. (2022, June 07). Functional programming vs OOP: Top 8 useful differences to know. Retrieved November 14, 2022, https://www.educba.com/functional-programming-vs-oop/
  • Meriç, T., & Bi, P. (2006). JFun: Functional Programming in Java., http://tekin.mericli.com/files/Mericli2006jfun.pdf
  • https://www.vavr.io/