Ders 07 / 23
Lambdalar ve Yüksek Mertebeden Fonksiyonlar
Lambda yazımını, fonksiyon tiplerini ve fonksiyon alan fonksiyonları tanıtır; iki lambda ifadesinin eşit olmadığını, yakalama yapmayan bir lambdanın ise tek nesne olduğunu ölçer.
Yayın Güncelleme
Bu derste
- Lambda ifadesini, `it` adını ve sondaki lambda yazımını kullanmak
- Fonksiyon tipini ve fonksiyon alan ya da döndüren yüksek mertebeden fonksiyonu yazmak
- İki lambdanın kimlik ölçütüne düştüğünü, yakalama yapmayan lambdanın ise tek nesne olduğunu ölçmek
İçindekiler
Önceki derste ::plain gibi bir fonksiyon referansının her alınışta ayrı ama eşit bir nesne verdiğini gördük. Şimdi fonksiyonu yerinde yazıyoruz. Lambda, adı olmayan bir fonksiyon değeridir; bir değişkene atanabilir, bir fonksiyona argüman olarak verilebilir ya da bir fonksiyondan döndürülebilir. Dersin sorusu önceki dersin sorusunun devamıdır: aynı gövdeye sahip iki lambda eşit midir?
Lambda ve Fonksiyon Tipi
Lambda süslü parantez içinde yazılır: önce parametreler, ardından ->, sonra gövde. Parametresi tek olan ve tipi bağlamdan bilinen bir lambdada parametre bildirilmez, onun yerine örtük it adı kullanılır. Fonksiyon tipi (function type) (Int) -> Int biçimindedir: parantez içinde parametre tipleri, okun ardından dönüş tipi bulunur.
Fonksiyon alan ya da fonksiyon döndüren bir fonksiyona yüksek mertebeden fonksiyon (higher-order function) denir. Son parametresi bir fonksiyon tipi olan fonksiyon çağrılırken lambda parantezin dışına yazılabilir: buna sondaki lambda (trailing lambda) denir. inline anahtar sözcüğü bu derste yalnız tanım olarak geçer: satır içi olarak işaretlenen bir yüksek mertebeden fonksiyonun gövdesi ve ona verilen lambda çağrı yerine derleme zamanında yerleştirilir. Bu derste inline ölçülmedi.
Bu derste de ölçümler ilk derste tanıtılan yardımcı dosyayla yapılır. Criterion sınıfı, bir değer ile ikizini ve farklı bir değeri alıp üç soruyu sorar; dosya aşağıda aynen verilmiştir.
// Criterion.kt — iki degeri esit yapan olcutu siniflandirir
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
val sameObject = first === twin
val equalTwin = first == twin
val equalOther = first == other
val name: String = when {
sameObject -> "tekil"
!equalTwin -> "kimlik"
!equalOther -> "icerik"
else -> "kismi-icerik"
}
fun report() {
println("$label: ayni-nesne=$sameObject, ikiz=$equalTwin, farkli=$equalOther -> $name")
}
}
fun summarize(checks: List<Criterion>) {
for (name in listOf("tekil", "kimlik", "icerik", "kismi-icerik")) {
println("$name: ${checks.count { it.name == name }}")
}
}
Fonksiyon tipi sıradan bir tiptir: bir değişkenin, bir parametrenin ya da bir dönüş değerinin tipi olarak yazılabilir. val increment: (Int) -> Int = { it + 1 } satırında tipi açıkça yazdığımız için lambdada parametre bildirmeye gerek kalmaz ve it kullanılabilir. Tip yazılmadığında ise parametre lambdanın içinde n: Int -> biçiminde bildirilir; derleyici iki yoldan birinden parametre tipini öğrenmek zorundadır.
Aşağıdaki program bunları birlikte gösterir. applyTwice bir işlemi iki kez uygular, makeAdder ise bir fonksiyon döndürür.
// Main.kt — lambda, it, sondaki lambda ve fonksiyon tipi
fun applyTwice(value: Int, operation: (Int) -> Int): Int = operation(operation(value))
fun makeAdder(amount: Int): (Int) -> Int = { it + amount }
fun main() {
val twice = { n: Int -> n * 2 }
val increment: (Int) -> Int = { it + 1 }
println(twice(4))
println(applyTwice(3, increment))
println(applyTwice(3) { it * 10 })
val addFive = makeAdder(5)
println(addFive(10))
println(listOf(1, 2, 3).map { it * it })
}
8 5 300 15 [1, 4, 9]
twice(4) doğrudan bir lambdayı çağırdı ve 8 verdi. applyTwice(3, increment) üçe iki kez bir ekledi ve 5 çıktı. applyTwice(3) { it * 10 } sondaki lambda yazımıdır: üç, otuz, üç yüz; sonuç 300. makeAdder(5) beş ekleyen bir fonksiyon döndürdü ve bu fonksiyon 10 üzerinde 15 verdi. Son satırda map her öğeye lambdayı uyguladı ve [1, 4, 9] çıktı.
DT45. it yalnız tek parametreli lambdalarda gösterildi; birden çok parametreli bir lambdada parametrelerin adlandırılması yalnız twice örneğindeki tek parametreyle sınırlı kaldı.
DT46. Yüksek mertebeden fonksiyonlar yalnız Int üzerinde ve map ile gösterildi; filter, fold gibi öteki koleksiyon fonksiyonları sonraki konunun işidir.
İki Lambda Eşit Midir
Ölçüm dört gözlemden kurulur. iki-lambda: iki ayrı yerde yazılmış { 1 } ifadesi. ayni-cagri-noktasi: aynı yerde yazılmış { 1 } lambdasını döndüren makeConstant fonksiyonu, iki kez çağrılır. yakalayan-lambda: dış bir değişkeni, yani amount parametresini kullanan lambda, makeAdderFor(1) iki kez çağrılır. fonksiyon-referansi: ::inc iki kez, önceki dersteki gözlemin bir daha tekrarı.
Yakalama (capture), bir lambdanın kendi gövdesinin dışındaki bir değişkeni kullanmasıdır. makeAdderFor içindeki lambda amount değerini yakalar; { 1 } ise dışarıdan hiçbir şey yakalamaz.
// Main.kt — lambdalar ve kimlik
fun makeConstant(): () -> Int = { 1 }
fun makeAdderFor(amount: Int): (Int) -> Int = { it + amount }
fun inc(n: Int): Int = n + 1
fun dec(n: Int): Int = n - 1
fun main() {
val checks = listOf(
Criterion("iki-lambda", { 1 }, { 1 }, { 2 }),
Criterion("ayni-cagri-noktasi", makeConstant(), makeConstant(), { 2 }),
Criterion("yakalayan-lambda", makeAdderFor(1), makeAdderFor(1), makeAdderFor(2)),
Criterion("fonksiyon-referansi", ::inc, ::inc, ::dec),
)
for (check in checks) {
check.report()
}
summarize(checks)
}
iki-lambda: ayni-nesne=false, ikiz=false, farkli=false -> kimlik ayni-cagri-noktasi: ayni-nesne=true, ikiz=true, farkli=false -> tekil yakalayan-lambda: ayni-nesne=false, ikiz=false, farkli=false -> kimlik fonksiyon-referansi: ayni-nesne=false, ikiz=true, farkli=false -> icerik tekil: 1 kimlik: 2 icerik: 1 kismi-icerik: 0
iki-lambda kimliktir: ayrı yerde yazılan iki aynı gövdeli lambda eşit değildir, ikiz=false. yakalayan-lambda da kimliktir: makeAdderFor(1) iki kez çağrıldığında iki ayrı lambda nesnesi doğdu, aynı değeri yakalamış olsalar bile eşit değiller.
ayni-cagri-noktasi ise tekildir: makeConstant() iki çağrıda aynı nesneyi döndürdü, ayni-nesne=true. Bu lambda dışarıdan bir şey yakalamıyor; sandbox’ta her çağrıda aynı örnek olarak gözlendi. fonksiyon-referansi içeriktir, önceki dersin sonucu burada yeniden doğrulandı. Özet satırları bir tekil, iki kimlik, bir içerik ve sıfır kısmi içerik verir.
DT47. Yakalama yapmayan lambdanın tek nesne olması Kotlin 2.4.21’in JVM hedefinde ölçüldü; bu bir dil sözleşmesi değil, o sürümde gözlenen sonuçtur.
DT48. yakalayan-lambda gözlemi tek bir Int yakalamasıyla yapıldı; yakalanan değişkenin tipi ya da sayısı değiştiğinde sonucun değişmediği iddia edilmiyor.
Yakalanan Değişken Her Yaratılışta Ayrıdır
Bir lambda kendi dışındaki bir var değişkenini okuyabilir ve değiştirebilir. Soru, bu değişkenin kaç kopyasının bulunduğudur. Aşağıdaki blok bir sayaç üreten fonksiyon yazar, iki ayrı sayaç kurar ve aynı sayaca iki ayrı ad verir.
// Main.kt — yakalanan var her yaratilista ayri
fun makeCounter(): () -> Int {
var count = 0
return { ++count }
}
fun main() {
val first = makeCounter()
val second = makeCounter()
println(first())
println(first())
println(second())
println(first())
val shared = makeCounter()
val alias = shared
println(shared())
println(alias())
}
1 2 1 3 1 2
first iki kez çağrıldı ve 1, 2 verdi; second ilk çağrıda 1 verdi. İki sayaç ayrı count değişkenlerini yakaladı, çünkü makeCounter her çağrıldığında yeni bir count yaratılır. first yeniden çağrıldığında 3 bastı ve second’ın çağrısı bunu etkilemedi. Son iki satır 1 ve 2 verdi: alias, shared ile aynı lambda nesnesine işaret ettiği için ikisi aynı sayacı artırdı.
Kural şudur: yakalanan değişken lambda nesnesi ile birlikte yaşar ve nesne başına bir kopya vardır. Sık yapılan hata, sayacın fonksiyon her çağrıldığında sıfırlandığını sanmak ya da tüm lambdaların aynı değişkeni paylaştığını varsaymaktır. Java’da lambda yalnız fiilen yeniden atanmayan (effectively final) değişkenleri yakalayabilir, Kotlin ise var yakalamaya izin verir; bunun arkasında değişkenin bir referans kutusuna sarıldığı bilinir, ama bu derste ölçülmedi.
DT49. Sayaç ölçümü tek iş parçacığında yapıldı; birden çok iş parçacığı aynı yakalanan değişkeni artırırsa ne olduğu bu derste ölçülmedi.
Lambdadan Dönmek
Bir lambdanın içinde return yazmak bazen fonksiyonu bitirir, bazen de hata verir. Aşağıdaki blok üç durumu dener: forEach içinde etiketsiz return, forEach içinde return@forEach ve map içinde kendi etiketi ile return@label.
// Main.kt — lambdadan donmek
fun findFirstEven(values: List<Int>): Int {
values.forEach {
if (it % 2 == 0) return it
}
return -1
}
fun main() {
println(findFirstEven(listOf(1, 3, 4, 6)))
println(findFirstEven(listOf(1, 3)))
val visited = mutableListOf<Int>()
listOf(1, 2, 3, 4).forEach {
if (it == 2) return@forEach
visited.add(it)
}
println(visited)
val labeled = listOf(1, 2, 3).map label@{
if (it == 2) return@label 20
it
}
println(labeled)
}
4 -1 [1, 3, 4] [1, 20, 3]
findFirstEven(listOf(1, 3, 4, 6)) 4 verdi: forEach bir satır içi (inline) fonksiyondur, bu yüzden lambdanın içindeki return dış fonksiyonu bitirir ve ilk çift sayı olan 4 döner. İkinci çağrıda çift sayı olmadığı için döngü bitti ve -1 döndü.
Üçüncü satır [1, 3, 4] verdi. return@forEach yalnız o lambda çalışmasını bitirir, yani continue gibi davranır: 2 atlandı, geri kalanlar eklendi. Dördüncü satır [1, 20, 3] verdi; return@label lambdanın değerini belirler, yani map içinde 2 değeri 20 ile değiştirildi.
Sık yapılan hata, etiketsiz return ile forEach içinde yalnız bir öğeyi atlamayı beklemektir: bu bütün fonksiyonu bitirir. Bu davranış forEach satır içi olduğu için mümkündür; satır içi olmayan bir lambdada etiketsiz return derleme hatası verir, ama bu dal ölçülmedi. Java’da lambda içindeki return yalnız lambdadan çıkar ve dış metodu bitirmez.
DT50. Dönüş davranışı forEach ve map ile ölçüldü; satır içi olmayan bir yüksek mertebeden fonksiyonda etiketsiz return yazılırsa verilen hata iletisi bu derste ölçülmedi.
Alıcılı Lambda ve SAM Dönüşümü
Bazı lambdalar bir alıcı nesnenin üyelerine doğrudan erişir. Aşağıdaki blok StringBuilder.() -> Unit tipinde bir lambda kurar, Java arayüzü Runnable ve Comparator için lambda yazar ve iki fonksiyonu birleştiren bir lambda üretir.
// Main.kt — alicili lambda ve SAM donusumu
fun main() {
val build: StringBuilder.() -> Unit = {
append("kot")
append("lin")
}
println(StringBuilder().apply(build).toString())
val task = Runnable { println("calisti") }
task.run()
val byLength = Comparator<String> { a, b -> a.length - b.length }
println(listOf("ccc", "a", "bb").sortedWith(byLength))
val compose: ((Int) -> Int, (Int) -> Int) -> (Int) -> Int = { f, g -> { x -> g(f(x)) } }
val doubleThenInc = compose({ it * 2 }, { it + 1 })
println(doubleThenInc(5))
}
kotlin calisti [a, bb, ccc] 11
İlk satır kotlin verdi. build değişkeni alıcılı bir lambdadır: gövdesinde append doğrudan çağrılır, çünkü this olarak bir StringBuilder verilir. apply(build) onu yeni bir StringBuilder üzerinde çalıştırdı. İkinci satır calisti: Runnable { ... } yazımı, tek soyut metodu olan bir Java arayüzünü lambda ile oluşturur. Bu dönüşüme SAM dönüşümü (single abstract method) denir. Üçüncü satır [a, bb, ccc] verdi: Comparator de aynı biçimde lambda ile yazıldı ve uzunluğa göre sıralama yaptı.
Dördüncü satır 11 verdi. compose iki fonksiyon alıp üçüncüsünü döndürür; doubleThenInc(5) önce 5’i ikiyle çarpar (10), sonra bir ekler (11). Sıra önemlidir: g(f(x)) yazıldığı için önce f çalışır. Sık yapılan hata, birleşimde sıranın tersini varsaymaktır.
DT51. SAM dönüşümü Java arayüzleriyle ölçüldü; Kotlin’de yazılmış bir arayüz için ayrı fun interface bildirimi gerektiği bu derste ölçülmedi.
Ölçümlerden Çıkan Alışkanlıklar
Lambdalarla çalışırken üç soru sorulmalıdır: ne yakalanıyor, nereden dönülüyor ve hangi tip bekleniyor. Yakalama konusunda ölçüm net bir kural verdi: yakalanan değişken lambda nesnesiyle birlikte yaşar ve her yaratılışta ayrı bir kopya oluşur. Bu, sayaç ya da önbellek gibi durum taşıyan küçük nesneleri sınıf yazmadan kurmayı mümkün kılar; ama durumun nerede saklandığı kodda görünmediği için, okuyan kişi için gizli bir bağımlılıktır. Durumun uzun ömürlü ya da paylaşılan olması gerekiyorsa açık bir sınıf daha okunur olur.
Dönüş konusunda en önemli ayrım, lambdanın satır içi olup olmadığıdır. forEach içinde etiketsiz return dış fonksiyonu bitirir; ölçümde findFirstEven ilk çift sayıyı bulup döndü. Bu davranış kullanışlıdır, çünkü erken çıkışı döngü yazmadan ifade eder. Ama continue beklenen yerde yanlış kullanılırsa tüm fonksiyonu sessizce bitirir. Atlamak için return@forEach, değeri belirlemek için return@label yazılır. Okunurluk açısından, birden fazla dönüş noktası olan lambdalar küçük adlandırılmış fonksiyonlara bölünmelidir.
Tip konusunda, alıcılı lambda (StringBuilder.() -> Unit) gövdede this ile çalışır ve nesnenin üyelerine doğrudan erişir. Bu, yapı kurucu kod yazmak için (builder) temeldir ve sonraki derste kapsam fonksiyonlarının mekanizmasıdır. SAM dönüşümü ise Java arayüzlerini lambda ile kurmayı sağlar; Kotlin’de yazılmış arayüzler için fun interface gerekir, bu ölçülmedi.
Fonksiyonları birleştiren compose örneği, fonksiyonların değer olduğunu gösterir. Sıra g(f(x)) olduğu için f önce çalışır ve 5 girdisi için sonuç 11 oldu. Ters sıra farklı bir sonuç verirdi, ama o hesap ölçülmedi; yalnız 11 çıktısı sandbox’tan geldi. Bu yüzden birleştirme sırasını bir örnekle sınamak yeterlidir. Fonksiyon birleştirme kütüphanelerinde bu sıra sık karıştırılır, bu yüzden küçük bir örnekle doğrulanması gerekir.
Java ile karşılaştırma: Java lambdası yalnız fiilen yeniden atanmayan değişkenleri yakalar ve return yalnız lambdadan çıkar. Kotlin ikisinde de daha geniş bir davranış sunar; esnekliğin bedeli, okurun hangi return’ün neyi bitirdiğini bilmesi gerektiğidir. Kodu gözden geçirirken her return için önce çevreleyen fonksiyonun satır içi olup olmadığına, sonra etiket bulunup bulunmadığına bakmak, bu belirsizliği çabuk giderir.
Sınırlayıcı Ölçüm
Lambdalar için kimlik ölçütü iki sınırla kırılır. Birincisi yakalamadır: yakalamayan lambda çağrı yerinde tek nesne olur ve tekil çıkar, yakalayan lambda her çağrıda yeni nesne olur ve kimlik çıkar. Yani iki lambdanın eşit olup olmadığını gövdeleri değil, hangi yerde yazıldıkları ve bir şey yakalayıp yakalamadıkları belirler. İkincisi fonksiyon referansıdır: ::inc iki kez alındığında nesneler ayrı ama eşittir.
Özet
- Lambda
{ parametreler -> gövde }biçiminde yazılır; tek parametreitadını alabilir ve son parametre bir fonksiyon tipiyse lambda parantezin dışına yazılabilir. - Fonksiyon tipi
(Int) -> Intbiçimindedir; fonksiyon alan ya da döndüren fonksiyona yüksek mertebeden fonksiyon denir. - Aynı gövdeli iki lambda ayrı yazıldıysa eşit değildir: kimlik. Yakalayan lambda her çağrıda yeni nesnedir.
- Yakalama yapmayan lambda aynı çağrı noktasından her seferinde aynı nesneyi verdi: tekil.
::inciki kez içeriktir. - Bu dersin ölçütü: kimlik; sınırlayıcı ölçüm: yakalamayan lambda tekildir,
::increferansı içeriktir.
Sonraki Adım
Lambdaları değer olarak taşımayı gördük. Son derste lambdayı bir nesnenin bağlamında çalıştıran beş hazır yüksek mertebeden fonksiyonu inceliyoruz: kapsam fonksiyonları. let, run, with, apply ve also aynı nesne üzerinde çalışır ama biri nesneyi this, biri it olarak verir; hangisinin neyi döndürdüğünü de ölçüyoruz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.