Kotlin lambda expressions, inline, crossinline, noinline, reified functions

Muhammet Küdür
7 min readMay 23, 2023

--

Selamlar, bu yazımda ilk karşılaştığımda kafamı karıştıran lambda expressions, inline, crossinline, noinline ve reified functions nedir, neden kullanılır, elimden geldiğince anlatmaya çalışacağım. Umarım faydalı bir yazı olur.

Başta inline modifierını(modifier: derleyici bu fonksiyonu derlerken fonksiyonun işlevini değiştirir aynı private,protected vb. gibi) sadece higher-order functionlarla birlikte kullanıldığını zannediyordum ancak farklı makalelerde higher-order olmayan fonksiyonlara da inline modifierını ekleyebildiğimizi gördüm.

Peki bu nasıl oluyor ve ne anlama geliyor bu inline?

Bir fonksiyona inline modifierı eklediğimizde; kullanım esnasında(compile time’da) bu fonksiyonu çağırmak yerine, fonksiyon içeriğini(body sini) fonksiyonun çağırıldığı yere yerleştirir. Yani sanki fonksiyon içeriğini gidip kopyalayıp çağırıldığı yere yapıştırıyoruz gibi düşünebiliriz, bu şekilde çalışıyor. Hemen bunu bir örnek ile görelim:

Kotlinde bir property tanımladığımızda (evet field tanımlamıyoruz) arka planda getter ve setter fonksiyonlarının otomatik olarak yazılması ve bizim göremediğimiz gibi, burada da ne olduğunu anlamak için Android Studio’daki Show Kotlin Bytecode komutunu kullanarak çıkan bytecode ‘u Java kodu olarak decompile edelim.(Bu arada IDE kızdı, bu örnekte inline kullanımının performansa etkisinin önemsiz olduğunu, parametre olarak bir fonksiyon verseydin daha iyi olurdu diyor şimdilik burayı atlayalım)

Gördüğümüz üzere 1 numaralı kullanımda doğrudan multiplyByTen fonksiyonumuzu çağırıldığını, 2. inline fonksiyon kullanımımızda bir fonksiyon çağırılmadığını, doğrudan body’nin işleme alındığını görüyoruz.

Şimdi gelelim bir fonksiyona parametre olarak başka bir fonksiyon verdiğimiz durumda neler oluyor. Öncelikle bunu kavrayabilmemiz için kısaca lambda expressions nedir buna bakalım

Lambda Expressions

  • Lambda expressions olarak tanımladığımız ifadeler: ‘functions literals’ olarak adlandırılır. Yani bir fonksiyonun tanımlanmadığı, ama bir ifade olarak geçirildiği anlamına gelir. Fonksiyon Referanslarıdır diyebiliriz.
  • Lambda expression yazarken ” (Type, Type, ….) -> Type “ şeklinde tanımlama yapıyoruz. Parametre olarak verdiğimiz tipler ile geri dönüş(return) tipi farklı olabilir. Örneğin Int verip String döndürebiliriz. İhtiyacınıza göre bir class’da verebilirsiniz hatta hiçbir şey döndürmek zorunda değilsiniz bu durumda da return type Unit oluyor.
  • -> ‘ ifadesinden sonra gelen satır/satırlar bu fonksiyonun body’sini oluşturur
  • Lambda Experssion’lar type inference’dır; son satırdaki kod( -> işaretinden sonra işleme alınacak olan son ifade) yazılan lambda’nın return type’ı olur.

Kısa bir örnek üzerinde görelim.

2 adet Integer alan ve toplamlarını döndüren bir lambda expression yazdım. Burada return type gene bir Int oldu. Decompile edip 2 numaralı kodu incelediğimizde; arka planda bu fonksiyonun bir nesnesini oluşturup invoke metodu kullanılarak verilen parametrelerle bu isimsiz fonksiyonun çağırıldığını görüyoruz. *Burası önemli :)

Lambda expressionların higher order functionlar ile kullanımı:

Higher-order function: Parametre olarak bir veya birden fazla fonksiyon alabilen, fonksiyon/fonksiyonlar return edebilen fonksiyonlara higher-order function diyoruz.

Bir fonksiyona parametre olarak başka bir fonksiyon geçirebilmek için, fonksiyon parametrelerine bir veya birden fazla(ihtiyaca göre) lambda expression yazmamız gerekiyor. En az 1 parametreye lambda expression vererek higher-order function yazmış oluyoruz.

Bir örnek ile görelim.

fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun add(x: Int, y: Int): Int {
return x + y
}
fun subtract(x: Int, y: Int): Int {
return x - y
}
fun main() {
val result1 = calculate(5, 3, ::add)
println(result1) // Çıktı: 8

val result2 = calculate(5, 3, ::subtract)
println(result2) // Çıktı: 2
}

Burada calculate fonksiyonumuz bir higher order function oldu. Calculate içerisine 3. parametre olarak verdiğimiz operation lambdası 2 adet integer alıp 1 adet intiger döndürdüğünü belirten bir fonksiyon beklediğini gösteriyor(Bu int değerleri a ve b olarak verilen ilk 2 parametre). Operation lambdasının koşullarını sağlayan herhangi bir fonksiyonu içeri paslamamız yeterli. Bunu da ::add, ::subtract olarak vererek temiz bir syntax elde ediyoruz. Yani verilen 2 adet integer değeri nasıl bir işleme sokmak istediğimizi seçiyoruz.

Not: ::functionName’ olarak parametre paslayabileceğimiz gibi süslü parantezler içerisinde bir expression vererek kullanım yapmamız da mümkün, şu şekilde:

fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return calculate (a, b)
}
val sum = calculate (5, 3) { x, y -> x + y }
println(sum) // Çıktı: 8
val multiply = calculate (5, 3) { x, y -> x * y }
println(multiply) // Çıktı: 15

Bu örnek üzerinde syntax daha temiz gibi gözüküyor ancak işlerin karmaşıklaştığı ve fonksiyonların 1 den fazla kez kullanıldığı senaryolarda bu şekilde kullanmak yerine ayrı bir fonksiyon yazıp ‘ ::functionName’ şeklinde parametre vermek daha düzgün bir görünüm ve kod tekrarının önüne geçmeyi sağlayacaktır.

Higher-order fonksiyonumuz başına ‘inline’ aldığında neler oluyor?

Lambda expression yani anonymous bir fonksiyon yazdığımızda arka planda bu fonksiyonun nesnesi oluştuğunu görmüştük. Parametre olarak bir fonksiyon verdiğimizde kullanım esnasında aynı şekilde bu fonksiyonun nesnesi arka planda oluşuyor.

Ancak; inline bir fonksiyon yazdığımızda parametre olarak verdiğimiz fonksiyonun nesnesi oluşmuyor bunun yerine fonksiyon body’si doğrudan çağırıldığı yere(compile time’da) gömülüyor. Bu sayede örneğin elimizde 1000+ elemanı olan bir liste var ve elemanları bir higher order function ile işleme almak istiyoruz, inline kullanımıyla her bir eleman için backing field’da yer tutulmasını engellemiş ve memory israfını önlemiş oluruz.

Buna ek olarak bir fonksiyonu çağırdığımızda bir çalıştırma sürecinden geçmesi biraz zaman alır (fonksiyon çağırılıp, body’nin işleme alınıp return etmesinden bahsediyorum). Bunu üst üste çok kez yaptığımızda uygulamamız bir işlem için gereğinden fazla zaman kaybeder. Bu fonksiyonu inline yaptığımızda fonksiyonun direkt body’si işleme sokulacağından uygulamamız daha performanslı çalışır, zaman kaybını da önlemiş oluruz.

Ne zaman inline kullanmalıyız?

  • Eğer bir fonksiyonu sık sık çağırıyorsanız ve bu fonksiyon küçük bir kod bloğunu temsil ediyorsa
  • Bir fonksiyon, başka bir fonksiyona parametre olarak iletilen bir lambda expression olduğunda

#Crossinline

Peki ‘ crossinline ‘ kullanmamızın amacı nedir, kullanmamız nasıl bir fayda sağlar? Bu konuya gelmeden önce local return ve non-local return nedir bunu anlamamız gerekiyor.

return   // non-local return 
return@requireName // local return
// local return ile fonksiyonun döneceği yeri seçiyoruz/etiketliyoruz..

Bunu daha iyi anlamak için stackoverflow’da yanıtlanmış bir soru üstünden açıklamaya çalışacağım. Öncelikle non-local return nedir buna bakalım.

fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")
return // This is non-local return
}
println("After lambda")
}

inline fun doSomethingElse(lambda: () -> Unit) {
println("Do something else")
lambda()
}
OUTPUT : 
public static final void doSomething() {
System.out.println("Before lambda");
System.out.println("Doing something else");
System.out.println("Inside lambda");
}

Burada bir fonksiyon içerisinde başka bir inline fonksiyon çağırımı yapıldığını görüyoruz. Derleyici inline fonksiyonun bodysini çağırıldığı yere koyacak; buradaki farkı inceleyelim.

Lambda içerisine girdikten sonra tüm fonksiyon scope’undan çıkılma sebebi budur

Sorun doSomethingElse fonksiyonuna parametre olarak verdiğimiz fonksiyonu non-local return ettiğimizde return ifadesi inline kullanımından dolayı kendi scope’unun dışına ‘taşıyor’ ve altında kalan kod satırını örnekte gördüğünüz gibi ezip yok sayıyor.

Çalıştırdığımız fonksiyon bloğunda kesin olarak çalışması gereken satırlar varsa ve sadece kullandığımız lambda expressionı return etmek istiyorsak local return kullanmamız gerekiyor. Yani fonksiyonun nereye return etmesi gerektiğini etiketliyoruz böylece return ifadesi bulunduğu fonksiyon scope’unun dışına taşmamış oluyor.

Bu durum için inline fonksiyon içerisine yazdığımız lambdayı, crossinline olarak işaretliyoruz. Crossinline ile işaretlenen lambda ifadelerde local olmayan return’ler engelleniyor ve olası hatalardan kaçınmış oluyoruz.

Kod örneği:

fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")

// return non-local -> artık buna izin verilmeyecek
return@doSomethingElse // OK: local return
}
println("After lambda")
}

inline fun doSomethingElse(crossinline lambda: () -> Unit) {
println("Doing something else")
lambda()
}
OUTPUT : 
public static final void doSomething() {
System.out.println("Before lambda");
System.out.println("Doing something else");
System.out.println("Inside lambda");
System.out.println("After lambda");
}

kulladığımız return, mevcut scope’un dışına taşmadığı için bu çıktıyı alabildik.

Kotlin dökümanında bu durum şöyle açıklanmış:

Bazı inline fonksiyonlar direkt olarak fonksiyon body’sinden değil local bir nesneden veya fonksiyona parametre olarak verilmiş bir lambda’dan çağırılabileceğini unutmayın. Bu gibi durumlarda non-local control akışlarına izin verilmez. Bunu belirtmek için ilgili lambdanın crossinline ile işaretlenmesi gerekir.

Bir örnekle:

inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
// ...
}

Inline higher-order bir fonksiyon yazarak başka bir fonksiyonun Super Type’ını değiştiriyor olabiliriz. Bu durumda da local olmayan return’lere izin verilmiyor ve ilgili lambdayı crossinline olarak işaretlememiz gerekiyor.

Çaktırmadan araya dip not: forEach bir inline fonksiyondur

#Noinline

İnline bir higher-order function’a parametre olarak verdiğimiz lambda expressionı inline yapmak istemediğimiz durumlar olabilir, bu durumda noinline ile işaretlersek, inline fonksiyon; işaretlediğimiz bu lambda’yı etkilemez.

Noinline kullanımı için bazı durumlar:

  • Bir fonksiyonu inline yaptığımızda body’si ilgili yere yapıştırılarak nesne oluşmasını engellediğini ve performans iyileştirmesi sağladığını görmüştük. Ancak fonksiyon body’si büyük kod blokları içeriyor ve çok sık çağırılıyorsa, bu durum hafızada daha fazla yer kaplayabilir ve derleme sürecini uzatabilir. Bu durumlarda, ilgili fonksiyon parametresini noinline yaparak, derleyiciye diğer optimizasyon tekniklerini kullanmasını sağlayabilirsiniz.
  • Fonksiyon referanslarını saklamak ve o fonksiyonu başka bir yerde kullanmak istediğimiz durumlarda noinline kullanabiliriz.

#Reified

Ek olarak; inline fonksiyonlarda ‘Reified’ keywordu ile karşılaşıyoruz.

Reified; generic türdeki parametreleri run time’da gerçek türe dönüştürmek için kullanılan bir özelliktir. Generic tipli inline fonksiyonlarla kullanılır. Örneğin:

fun <T> myGenericFun(c: Class<T>) 

myGenericFun isimli fonksiyonumuzdaki <T> türüne erişemezsiniz, çünkü generic type’lar yalnızca compile time’da kullanılabilir ve; run time’da silinirler. Bu sebepten ötürü; eğer fonksiyon body’sinde normal bir class gibi generic type kullanmak istiyorsak; kullanmak istediğimiz sınıfı yukarıdaki kod örneğindeki gibi parametre olarak yazmak zorundayız.

Inline reified generic bir fonksiyon şu şekildedir:

inline fun <reified T> myGenericFun()

inline fun <reified T> kullandığımız durumda T tipine Run Time’da bile erişebiliriz. Bu sayede fonksiyon parametresi olarak Class<T> yazmamıza gerek kalmaz. T ile normal bir sınıfmış gibi çalışabiliriz.

Reified yalnızca inline functionlarla birlikte kullanılabilir.

Generic typelar run time’da silindiği için spesifik olarak hangi type kullanılıyor bilinemez. Ancak, inline reified bir fonksiyon kullandığımızda bu tür parametrelere erişebilir ve normal bir tür gibi(string, int vs.) fonksiyon body’sinde kullanabiliriz. Girilen type nedir bulabiliriz.

Bir örnek üzerinde bakalım:

inline fun <reified T> exampleFunction() {
if (T::class == String::class) {
println("Type is String")
} else {
println("Type is not String")
}
}
fun main(){
exampleFunction<String>() // Output: Type is String
exampleFunction<Int>() // Output: Type is not String
}

Bu yazım burada son buluyor. Bir hata gördüyseniz veya görüş/önerileriniz varsa lütfen linkedIn üzerinden mesaj atıp benimle iletişime geçmekten çekinmeyin :) Başka bir yazıda görüşmek üzere.

--

--