İçeriğe geç
academia.sh

Ders 19 / 23

Dönüşüm İşlemleri

Eşleme, süzme, katlama ve gruplamanın yeni koleksiyon ürettiğinin, aynı dönüşümün iki kez uygulanınca eşit ama ayrı sonuç verdiğinin ve distinct ile toSet'in elemanın eşitliğine dayandığının ölçümü.

Yayın Güncelleme

Bu derste

  • map, filter, fold ve reduce işlemlerinin sonucunu ve boş koleksiyondaki farkı ölçer
  • groupBy, associate ve zip işlemlerinin ürettiği koleksiyonu çıktıdan okur
  • Aynı dönüşümün iki kez uygulanmasının eşit ama ayrı koleksiyon verdiğini gösterir
  • distinct ve toSet işlemlerinin elemanın kendi eşitliğine dayandığını ölçer
İçindekiler

Önceki ders koleksiyonları bir veri kabı olarak ölçtü ve içeriğin eşitliği belirlediğini gösterdi. Gerçek programlarda koleksiyon nadiren olduğu gibi kullanılır. Sipariş listesinden tutarları toplamak, bir şehre ait siparişleri ayırmak, iki listeyi eşleştirmek gerekir. Bunların hepsi dönüşüm işlemleridir: bir koleksiyonu alır, ondan yeni bir koleksiyon ya da tek bir değer üretirler. Dönüşüm işlemlerinin çoğu yüksek mertebeden fonksiyondur, yani parametre olarak bir lambda alır; sorted() ya da distinct() gibi lambda almayanlar da vardır. Lambda, her elemana uygulanacak kuralı taşır; işlemin adı bu kuralın ne için kullanılacağını söyler.

Dört temel işlemin Türkçe adları şunlardır. map eşlemedir: her elemandan bir yenisini üretir ve sonuç aynı uzunlukta bir listedir. filter süzmedir: lambdanın doğru dediği elemanları tutar. fold katlamadır: bir başlangıç değerinden yola çıkıp elemanları tek bir değere birleştirir. reduce indirgemedir: katlamanın başlangıç değeri vermeyen biçimidir, ilk elemanı başlangıç sayar. Kursun ortak ölçü sorusu bu derste de geri döner: bu iki değeri eşit yapan ne? Dört ölçüt (tekil, kimlik, içerik, kısmi içerik) önceki derste tanıtıldığı biçimde kalır. Criterion sınıfı olduğu gibi tekrar verilir.

// 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 }}")
    }
}

Bu dersin ölçümleri dört ayrı veri tipi kullanır. Order bir sipariş satırıdır: kimlik, şehir ve tutar. Ticket yalnız bir kimlik taşıyan veri sınıfıdır; Plain aynı alanı taşıyan ama veri sınıfı olmayan sıradan bir sınıftır. Session ise kimliği kurucuda, ziyaret sayısını gövdede taşıyan bir veri sınıfıdır. Veri sınıfı equals’ı kurucudaki özelliklerden üretir; gövdede tanımlanan özellik bu üretime girmez. session fonksiyonu iki alanı birlikte kuran küçük bir yardımcıdır. Bu bloğun da çıktısı yoktur.

// Models.kt — donusum olcumlerinin kullandigi tipler
data class Order(val id: Int, val city: String, val amount: Int)

data class Ticket(val id: Int)

class Plain(val id: Int)

data class Session(val id: Int) {
    var visits = 0
}

fun session(id: Int, visits: Int): Session {
    val result = Session(id)
    result.visits = visits
    return result
}

Dört Temel İşlem

İlk ölçüm bir Int listesine dört işlemi uygular. map { it * 2 } her tutarı ikiyle çarpar, filter { it > 6 } altı ve altındakileri eler. fold(0) toplamı verir; başlangıç değeri 0 olduğu için sonuç tipi başlangıç değerinin tipidir. reduce ile en büyük eleman bulunur. Son iki ölçüm iki işlemin boş koleksiyonda ayrıldığı yeri gösterir: fold başlangıç değerini geri verir, reduce’un ise başlayacak ilk elemanı olmadığı için bir istisna fırlatması beklenir. Ölçümde istisna try ile yakalanır ve yalnız sınıf adı basılır.

// Main.kt — eslem, suzme, katlama, indirgeme
fun main() {
    val amounts = listOf(5, 12, 7, 20)
    println(amounts.map { it * 2 })
    println(amounts.filter { it > 6 })
    println(amounts.fold(0) { acc, n -> acc + n })
    println(amounts.reduce { acc, n -> maxOf(acc, n) })
    println(emptyList<Int>().fold(0) { acc, n -> acc + n })
    try {
        emptyList<Int>().reduce { acc, n -> acc + n }
    } catch (e: UnsupportedOperationException) {
        println(e::class.simpleName)
    }
}
[10, 24, 14, 40]
[12, 7, 20]
44
20
0
UnsupportedOperationException

İlk satırda dört eleman dört elemana eşlendi. İkinci satırda yalnız üç eleman süzmeden geçti: 5 elendi. Üçüncü satır 5 + 12 + 7 + 20 toplamıdır; dördüncü satır maxOf ile 20’yi bulur. Beşinci satır fold’un boş listede başlangıç değeri olan 0’ı döndürdüğünü, altıncı satır reduce’un aynı durumda UnsupportedOperationException fırlattığını gösterir. Kural açıktır: eleman olup olmadığı bilinmiyorsa fold seçilir; boş koleksiyon hata sayılacaksa reduce seçilir.

KE7. Bu ölçümler Int listeleri için. Elemanları başka tipte olan bir reduce’un sonuç tipinin elemanların ortak üst tipine nasıl daraldığı ve reduceOrNull gibi kardeş işlemler ayrıca ölçülmedi.

Gruplama, Eşlem Kurma ve Eşleştirme

Üç işlem daha koleksiyondan koleksiyon üretir, ama sonuç liste olmak zorunda değildir. groupBy her elemanı bir anahtara göre toplar ve anahtardan listeye giden bir eşlem kurar. associate her elemandan bir anahtar-değer çifti üretir ve bunlardan bir eşlem kurar; associateBy anahtarı elemandan alır, değer olarak elemanın kendisini saklar. İki elemanın anahtarı aynı çıkarsa sonradan gelen öncekinin yerini alır. zip iki listeyi sırayla çiftler ve çift sayısı kısa olan listeyle sınırlıdır. Ölçümde associateBy sonucunun ankara anahtarındaki siparişin kimliği okunur; iki ankara siparişinden hangisinin kaldığı böylece görünür.

// Main.kt — gruplama, eslem kurma, zip
fun main() {
    val orders = listOf(Order(1, "ankara", 30), Order(2, "izmir", 10), Order(3, "ankara", 20))
    println(orders.groupBy { it.city }.mapValues { entry -> entry.value.map { it.id } })
    println(orders.associate { it.id to it.amount })
    println(orders.associateBy { it.city }["ankara"]?.id)
    println(orders.map { it.id }.zip(listOf("a", "b", "c")))
    println(listOf(1, 2, 3).zip(listOf("a", "b")))
}
{ankara=[1, 3], izmir=[2]}
{1=30, 2=10, 3=20}
3
[(1, a), (2, b), (3, c)]
[(1, a), (2, b)]

İlk satırda groupBy iki ankara siparişini tek listede topladı ve izmir için tek elemanlı bir liste bıraktı. İkinci satır kimlikten tutara giden bir eşlemdir. Üçüncü satırda ankara anahtarı için 3 okunur: kimliği 1 olan sipariş, kimliği 3 olanın gelmesiyle yerini kaybetti. Dördüncü satır üç çiftli bir listedir. Beşinci satırda ikinci liste kısa olduğu için 3 elemanı çiftlenmeden kaldı ve sonuç iki çifttir. zip hiçbir zaman istisna fırlatmaz; uzunlukların uyuşmaması sessizce en kısa uzunluğa indirgenir. Bu davranış çoğu zaman istenendir ama iki listenin aynı uzunlukta olması gerekiyorsa bunu zip’ten önce kontrol etmek gerekir.

KE8. Bu ölçümler üç elemanlı sipariş listesi için. groupBy ile groupingBy arasındaki farklar, unzip ve üç listeli zip ayrıca ölçülmedi.

Ölçüt: İçerik

Dönüşüm işleminin sonucu yeni bir koleksiyondur. Aynı işlemi aynı koleksiyona iki kez uyguladığımızda iki sonuç elde ederiz. Bu iki sonucun aynı nesne olması gerekmez, ama içeriklerinin eşit olması gerekir. Aşağıdaki blok map ve filter için iki kez dönüşüm uygular. first ve twin aynı tarifle ayrı kurulur; other ise bir bakımdan farklıdır: map için son eleman 41’dir, filter için bir eleman eksiktir. Son satır, her elemanı olduğu gibi döndüren map { it } sonucunun kaynak listeyle aynı nesne olup olmadığını sorar.

// Main.kt — ayni donusum iki kez
fun main() {
    val amounts = listOf(5, 12, 7, 20)
    val checks = listOf(
        Criterion("map", amounts.map { it * 2 }, amounts.map { it * 2 }, listOf(10, 24, 14, 41)),
        Criterion("filter", amounts.filter { it > 6 }, amounts.filter { it > 6 }, listOf(12, 7)),
    )
    for (check in checks) check.report()
    summarize(checks)
    println(amounts.map { it } === amounts)
}
map: ayni-nesne=false, ikiz=true, farkli=false -> icerik
filter: ayni-nesne=false, ikiz=true, farkli=false -> icerik
tekil: 0
kimlik: 0
icerik: 2
kismi-icerik: 0
false

Her iki ölçüm de içerik çıktı. İkizler ayrı nesnelerdir (ayni-nesne=false), ama eşittir; farklı olan liste ise eşit değildir. Dönüşüm işleminin sonucunu tanımlayan şey, hangi çağrıdan çıktığı değil, taşıdığı elemanlardır. Bu dersin ölçütü budur: içerik. Son satır önemli bir ayrıntıyı ayırır: elemanları değiştirmeyen map { it } bile kaynak listeyi geri vermez, yeni bir liste kurar. Dönüşüm işlemleri kaynak koleksiyona dokunmaz; bu dersin ölçümlerinde sonuç her seferinde ayrı bir nesneydi ve kaynağın sonradan değişmesi sonucu etkilemedi. Boş sonuçlarda standart kitaplık ortak bir boş liste döndürebilir; bu durum burada ölçülmedi.

KE9. Bu ölçümler Int elemanlı listeler için. Boş koleksiyona uygulanan dönüşümün, önceki derste ölçülen tekil boş listeyi döndürüp döndürmediği ayrıca ölçülmedi.

Sınırlayıcı Ölçüm

İçerik ölçütü, elemanlar Int iken kolay işler: iki 5 birbirine eşittir. Elemanlar kendi eşitliğini taşıyan nesneler olunca durum değişir. distinct() ve toSet() tekrar eden elemanı elemek için elemanın kendi eşitliğini kullanır; dönüşüm işlemi ölçütü kendisi üretmez, elemandan alır. Aşağıdaki blok aynı kuralı üç tipe uygular. Ticket veri sınıfıdır, iki Ticket(1) eşittir. Plain sıradan bir sınıftır, iki Plain(1) aynı nesne olmadıkça eşit değildir. Session veri sınıfıdır ama visits özelliği gövdede tanımlıdır ve eşitliğe girmez. distinctBy ise elemanı değil seçicinin döndürdüğü değeri karşılaştırır.

// Main.kt — distinct ve toSet ogenin olcutunu kullanir
fun main() {
    val tickets = listOf(Ticket(1), Ticket(1), Ticket(2))
    val plains = listOf(Plain(1), Plain(1), Plain(2))
    println(tickets.distinct().size)
    println(plains.distinct().size)
    println(tickets.toSet().size)
    println(plains.toSet().size)
    println(plains.distinctBy { it.id }.size)
    val sessions = listOf(session(1, 0), session(1, 5))
    println(sessions.distinct().size)
    val checks = listOf(
        Criterion("veri-sinifi", Ticket(1), Ticket(1), Ticket(2)),
        Criterion("siradan-sinif", Plain(1), Plain(1), Plain(2)),
        Criterion("govde-ozelligi", session(1, 0), session(1, 0), session(1, 5)),
    )
    for (check in checks) check.report()
    summarize(checks)
}
2
3
2
3
2
1
veri-sinifi: ayni-nesne=false, ikiz=true, farkli=false -> icerik
siradan-sinif: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
govde-ozelligi: ayni-nesne=false, ikiz=true, farkli=true -> kismi-icerik
tekil: 0
kimlik: 1
icerik: 1
kismi-icerik: 1

Sınırlayıcı ölçüm şudur: distinct ve toSet ölçütü elemandan alır. İlk dört satırda aynı kimlik dizisi (1, 1, 2) iki tipe uygulanmıştır. Veri sınıfı olan Ticket için distinct ve toSet iki elemana indirir: iki Ticket(1) eşit sayılır ve biri elenir. Sıradan sınıf olan Plain için ikisi de üç eleman bırakır: Plain(1) ile Plain(1) ayrı nesnelerdir ve eşit değildir, bu yüzden hiçbiri elenmez. Beşinci satırda distinctBy { it.id } aynı Plain listesini ikiye indirir. Burada ölçütü elemanın kendisi değil, seçicinin döndürdüğü Int belirler; elemanın sıradan sınıf olması fark etmez. Altıncı satır Session için bir şaşırtma gösterir: ziyaret sayıları 0 ve 5 olmasına rağmen iki oturum tek elemana iner, çünkü visits kurucuda olmadığı için veri sınıfının eşitliğine girmez.

Sonraki üç satır aynı üç durumu Criterion ile adlandırır. Ticket için ölçüt içeriktir. Plain için ölçüt kimliktir: ikiz bile eşit değildir. Session için ölçüt kısmi içeriktir: other yalnız visits bakımından farklıdır ve yine de eşit çıkar. Dağılım 0/1/1/1 olarak üç ölçüte bölünür. Dersin ölçütü yine içeriktir, ama sınır şuradadır: dönüşüm işlemleri bir eşitlik tanımı getirmez, elemanın eşitliğini kullanır. Eleman eşitliği yanlış kurulduysa, sonuçtaki tekrar eleme de yanlış kurulmuş olur.

KE10. Bu ölçümler birer alanlı Ticket ve Plain ile iki alanlı Session için. distinct’in hangi tekrar eden elemanı tuttuğu (ilk mi, son mu) bu ölçümlerde sorulmadı; distinctBy için de aynı.

Sıralama ve Yerinde Değişiklik

Dönüşüm işlemlerinde sık yapılan bir hata, sorted ile sort arasındaki farkı gözden kaçırmaktır. sorted() ve sortedBy yeni bir sıralı liste döndürür, kaynağa dokunmaz. sort() ise yalnız değiştirilebilir listede vardır, listeyi yerinde değiştirir ve bir şey döndürmez. Birincisini yazıp sonucu kullanmamak, sıralanmış bir liste beklerken eski listeyle kalmak demektir. Ayrıca sortedBy kararlıdır: anahtarı eşit çıkan elemanlar kaynaktaki sıralarını korur. Aşağıdaki blok bunları sırayla ölçer. Sipariş listesi şehre göre sıralanır ve yalnız kimlikler basılır; iki ankara siparişi arasındaki sıra kararlılığı buradan okunur.

// Main.kt — sorted yeni liste, sort yerinde
fun main() {
    val numbers = mutableListOf(3, 1, 2)
    val ordered = numbers.sorted()
    println(numbers)
    println(ordered)
    println(ordered === numbers)
    numbers.sort()
    println(numbers)
    val orders = listOf(Order(1, "izmir", 10), Order(2, "ankara", 30), Order(3, "ankara", 20), Order(4, "bursa", 5))
    println(orders.sortedBy { it.city }.map { it.id })
    println(orders.sortedByDescending { it.amount }.map { it.id })
}
[3, 1, 2]
[1, 2, 3]
false
[1, 2, 3]
[2, 3, 4, 1]
[2, 3, 1, 4]

Birinci satır sorted() çağrısından sonra numbers’ın dokunulmadan kaldığını, ikinci satır ordered’ın sıralı yeni liste olduğunu, üçüncü satır ikisinin ayrı nesneler olduğunu gösterir. sort() çağrısından sonra numbers’ın kendisi sıralanır. Beşinci satırda şehre göre sıralama ankara, bursa, izmir düzenini kurar ve iki ankara siparişi kaynaktaki sıralarıyla (2, sonra 3) kalır; kararlılık budur. Altıncı satır tutara göre azalan sıralamadır.

KE11. Bu ölçümler Int ve Order listeleri için. sortedWith ile bileşik karşılaştırıcı, Comparable olmayan elemanlarda sıralama ve çok büyük listelerde sıralamanın bedeli ölçülmedi.

Boş Koleksiyon ve Sınır Durumları

Dönüşüm işlemlerinin ikinci hata kaynağı sınırdır: koleksiyon boşsa ya da istenen parça koleksiyondan büyükse ne olur? first() boş listede istisna fırlatır, firstOrNull() ise null döndürür. maxOrNull() boş listede null verir. average() boş listede NaN verir, istisna fırlatmaz, bu yüzden sonuç sessizce sayı olmayan bir değer olur ve sonraki hesapları bozar. chunked(n) listeyi en çok n elemanlı parçalara böler ve son parça kısa kalabilir. windowed(n) ise her konumda n elemanlı kayan bir pencere kurar; koleksiyon n’den kısaysa hiç pencere üretmez. Ölçümde istisna try ile yakalanır ve yalnız sınıf adı basılır.

// Main.kt — bos koleksiyon ve parcalama
fun main() {
    val empty = emptyList<Int>()
    println(empty.firstOrNull())
    println(empty.maxOrNull())
    println(empty.average())
    try {
        empty.first()
    } catch (e: NoSuchElementException) {
        println(e::class.simpleName)
    }
    val five = listOf(1, 2, 3, 4, 5)
    println(five.chunked(2))
    println(five.windowed(3))
    println(listOf(1, 2).windowed(3))
    println(listOf("1", "x", "3").mapNotNull { it.toIntOrNull() })
}
null
null
NaN
NoSuchElementException
[[1, 2], [3, 4], [5]]
[[1, 2, 3], [2, 3, 4], [3, 4, 5]]
[]
[1, 3]

İlk iki satır null verir. Üçüncü satırda boş listenin ortalaması NaN çıktı: istisna yok, ama sonucu sayı sanıp kullanan kod yanlış çalışır. Bu yüzden ortalama almadan önce listenin boş olup olmadığını denetlemek ya da isEmpty() sonucuna göre dallanmak gerekir. Dördüncü satır first()’in boş listede NoSuchElementException fırlattığını gösterir. Beşinci satırda chunked(2) beş elemanı [1, 2], [3, 4], [5] olarak üç parçaya böler; son parça kısadır. Altıncı satırda windowed(3) üç pencere üretir: [1, 2, 3], [2, 3, 4], [3, 4, 5]. Yedinci satırda iki elemanlı liste için pencere kurulamaz ve sonuç boş listedir. Son satırda mapNotNull, dönüştürme sırasında null veren elemanı atar: "x" sayıya çevrilemediği için yalnız 1 ve 3 kalır. map ve filterNotNull ikilisinin tek adımda yazılmış biçimidir.

KE12. Bu ölçümler Int ve String listeleri için. windowed’un step ve partialWindows parametreleri ve Double listelerinde average dışındaki toplayıcı işlemlerin boş durumları ölçülmedi.

Özet

  • map, filter, fold ve reduce kaynak koleksiyona dokunmaz; bu dersin ölçümlerinde sonuç ayrı bir nesneydi. fold boş koleksiyonda başlangıç değerini, reduce bir istisna verir.
  • groupBy anahtardan listeye giden, associate ve associateBy anahtardan değere giden bir eşlem kurar; anahtar çakışınca sonraki girdi öncekinin yerini alır. zip kısa olan listeye göre keser.
  • Aynı dönüşümü aynı koleksiyona iki kez uygulamak eşit ama ayrı iki koleksiyon verir. Bu dersin ölçütü: içerik.
  • Sınırlayıcı ölçüm: distinct ve toSet elemanın kendi eşitliğine dayanır. Veri sınıfı öğelerde tekrarlar elenir, sıradan sınıf öğelerinde elenmez, gövde özelliği eşitliğe girmez; distinctBy ölçütü seçicinin değerinden alır.

Sonraki Adım

Bu ders her dönüşüm işleminin hemen çalıştığını ve her adımda yeni bir koleksiyon kurduğunu varsaydı. Zincirdeki ara listeler gereksizse ya da kaynak sonsuzsa bu varsayım gereksiz iş yaptırır ya da hiç bitmez. Bir sonraki ders aynı işlemleri tembel dizilerle (Sequence) çalıştırır ve eşitlik sorusunu orada yeniden sorar: iki tembel dizi, aynı elemanları üretiyor olsa da eşit midir?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat