Kotlin — lambdy, anonymné funkcie, rozširujúce funkcie a lambdy s prijímačom

2024/01/04

Toto je funkcia:

fun double(n: Int): Int {
    return n * 2
}

Funkciu môžeme priradiť do premennej:

fun double(n: Int): Int {
    return n * 2
}

fun main() {
    val f = ::double
    println(f(2))
}

Syntax :: používa references, teda odkazy na funkcie, či metódy.

Funkcie môžu byť tvorené len výrazmi

Toto je tiež funkcia!

fun double(n: Int) = n * 2

Anonymné funkcie

Funkcia môže byť anonymná.

fun main() {
    val double = fun (n: Int): Int {
        return n * 2
    }
    println(double(2))
}

Ako vidno, medzi fun a zoznamom argumentov nie je meno. Môžeme ju potom priradiť do premennej double a volať naďalej. Ak sme zvedaví, tak dátový typ premennej double je:

(Int) -> Int

Berieme jeden Intídžer a vraciame tiež Intídžer.

Pod double sa skrýva premenná, ale v skutočnosti je to funkcia!

Anonymné funkcie ako parametre

Funkcie sú v Kotline bežnými občanmi. Môžeme ich odovzdávať ako parametre:

val double = fun (n: Int): Int {
    return n * 2
}
val doubles = listOf(1, 2, 3).map(double)
println(doubles)

Funkcia map na zozname List berie ako parameter funkciu, ktorá sa spustí na každom prvku zoznamu.

Je to potom funkcia vyššieho rádu (higher order function), lebo fukcia do parametra berie funkciu!

Lambdy

Anonymné funkcie sa volajú lambda výrazy, skrátene lambdy.

Prečo lambda? Čo to má s Grékmi? Nateraz nechceme vedieť.

Keďže v Kotline sa lambda výrazy používajú kade-tade, existuje skrátený zápis:

val double = { n: Int -> n * 2 }

Premenná double obsahuje anonymnú funkciu, po novom lambda výraz, čiže lambdu, ktorá má:

Lambdu používame rovnako ako anonymné funkcie:

val doubles = listOf(1, 2, 3).map(double)

Lambdy ako parametre

Funkcia môže brať lambdu ako parameter. Obvykle predstavuje nejaký „kus kódu“, čo sa môže dynamicky vykonať.

Funkcia map zoberie po jednom prvky zoznamu a na každom z nich „vykoná kus kódu“. Tento kus kódu je reprezentovaný lambdou.

Na zozname čísiel preto berie ako parameter funkciu (Int) -> Int, čiže z intídžrov do intídžrov. Naša premenná double predstavuje „kus kódu“, ktorá zdvojnásobí ľuboľný vstup a teda ju môžeme použiť na každý prvok zoznamu.

Lambdy ako koncové parametre

Ak má funkcia posledný parameter typu lambda, máme skrátený zápis:

val doubles = listOf(1, 2, 3).map { n: Int -> n * 2 }

Oficiálne sa to volá trailing lambda (koncová lambda). Toto v skutočnosti pripomína funkcie, až na šípku:

val doubles = listOf(1, 2, 3).map { n: Int -> 
    n * 2 
}

Odvodzovanie typov je zázračné — niekedy vie uhádnuť dátový typ parametra n.

val doubles = listOf(1, 2, 3).map { n ->  n * 2 }

Keďže máme zoznam Int-egerov, Kotlin zistí, že lambda vykonaná na každom prvku musí bezpodmienečne brať ako parameter jeden Int. Ako programátori teda dátový typ Int premennej n nemusíme uvádzať, pretože Kotlin si ho „domyslí“.

A ak má lambda jediný parameter, je to ešte kratšie:

val doubles = listOf(1, 2, 3).map { it * 2 }

Zjaví sa automatická premenná it („to“) s automaticky odvodeným dátovým typom - napríklad Int.

Lambda a viacero riadkov

Lambda môže mať viacero riadkov:

val doubles = listOf(1, 2, 3).map { 
    println("Calculating double of $it")
    it * 2
}

Výsledkom lambdy je posledný výraz, teda dvojnásobok premennej it. Lambda výraz je totiž, ako by som to, „výraz“.

Zátvorky {} sa podobajú na blok kódu. Toto nám dáva nové možnosti!

Ľubovoľné bloky

Aha, kód:

repeat(5) {
    println(it)
}

To, čo vyzerá podozrivo ako while(true) {... } je bežné použitie lambdy a parametrov. Ale je to bežná funkcia! Tento repeat má dva parametre:

public fun repeat(times: Int, action: (Int) -> Unit)
  1. počet opakovaní times
  2. a lambdu z Int do „ničoho“ (Unit), lebo nepotrebujeme z nej vracať žiadnu hodnotu.

To by sme mohli celé napísať komplikovane napríklad takto:

val printToConsole = { n: Int -> println(n) }
repeat(5, printToConsole)

Ale načo, keď máme koncové lambdy?

Budovateľské nadšenie s buildermi

Predstavme si košík na veci. Presnejšie, košík na reťazce, ktoré doň môžeme pridávať.

class Basket {
    private val items = mutableListOf<String>()

    fun add(item: String) {
        items += item
    }
}

Môžeme si predstaviť nasledovný pseudojazyk (DSL, domain specific language):

basket {
    it.add("Cabbage")
    it.add("Carrot")
}

Toto je v skutočnosti skrátený zápis za:

basket { b: Basket -> 
    b.add("Cabbage")
    b.add("Carrot")
}

Aby toto fungovalo, zostrojíme funkciu basket s koncovou lambdou:

fun basket(build: (Basket) -> Unit) {
    val basket = Basket()
    build(basket)
}

Lambda berie košík Basket a nevracia nič. Konkrétny objekt košíka najprv vytvoríme a potom ho použijeme ako argument lambdy v premennej build.

Takto sa môžeme napojiť na odvodzovanie typov: keďže funkcia basket vie, že parametrom lambdy je Basket a tento parameter je len jeden, môžeme ho použiť v podobe it.

Lambdy s prijímačmi (Lambdas with Receivers)

Aha, akú krásnu syntax vieme v Kotline ešte vymyslieť:

basket {
    add("Cabbage")
    add("Milk")
}

Čo je add? Je to metóda na objekte typu Basket. A kde je ten objekt? Je skrytý pod zamlčaným this.

basket {
    this.add("Cabbage")
    this.add("Milk")
}

A čo je this? Je to prijímač (receiver), ktorý vieme uviesť v lambde.

V skratke: toto je ešte kratší zápis ako hrajkanie sa s it.

Lambda s poslucháčom má extra parameter:

contentBuilder: Basket.() -> Unit

Basket pred bodkou znamená typ, ktorý sa vo vnútri lambdy zjaví pod premennou this. Čítame to ako „lambda má prijímač typu Basket, žiadne parametre a nič nevracia“. Ako to použijeme?

fun basket(contentBuilder: Basket.() -> Unit) {
    val b = Basket()
    b.contentBuilder()
}

Lambdu tuto zavoláme na objekte typu Basket (v premennej b) akoby išlo o jeho bežnú metódu.

Tento objekt b sa potom sprístupní vo vnútri lambdy contentBuilder v premennej this!

Prijímač typu Basket (premenná b) volá lambdu bez parametrov a bez návratovej hodnoty.

A ešte: receiver je naozaj dodatočný parameter, lebo volanie lambdy môžeme urobiť aj naopak:

contentBuilder(basket)

Syntax zrazu začne fungovať!

Extension Functions — rozširujúce funkcie

Predstavme si ďalší syntaktický cukor:

5.times {
    println("Hello")
}

Toto je veľmi podobné ako:

repeat(5) {
    println("Hello")
}

Ibaže číslo je vysunuté pred bodku, čiže to vyzerá ako metóda! Dokonca na čísle 5, teda metóda na Integeri. Ale Int nemá metódu times, tak ako to funguje? Začnime jednoduchším prípadom:

5.times("Hello")

V Kotline môžeme vytvárať funkcie, ktoré budia dojem, že obohacujú existujúce triedy o nové metódy. Ale presne na to naozaj slúžia! Vytvorme funkciu:

fun Int.times(s: String) {
    TODO("Not yet implemented")
}

Pred názvom funkcie a pred bodkou je Int, čo je akýsi dodatočný parameter funkcie reprezentujúci objekt, na ktorom môžeme volať metódu times. Takáto funkcia sa volá extension function (rozširujúca funkcia). Pozor, toto nie je lambda s prijímačom (lambda with receiver)! Je to normálna, slušná, pomenovaná funkcia. Keďže hodnota 5 je typu Int, môžeme na nej volať metódu times.

Rozšírenia a lambdy

Extension Functions (rozširujúce funkcie) a lambdy môžeme kombinovať!

fun Int.times(iteration: (Int) -> Unit) {
    for (i in 1..this) {
        iteration(i)
    }
}

Funkcia times prijme lambdu, ktorá síce nevracia nič, ale zato má jeden celočíselný parameter (reprezentujúci „poradové číslo kola“, ktoré sa práve vykonáva). Teraz už môžeme volať:

5.times {
    println("$it: Hello")
}

Naspäť ku košíku!

Ak chceme mať peknú syntax:

basket {
    item("Milk")
    item("Sugar")
}

Stačí dodať rozširujúcu funkciu:

fun Basket.item(item: String) = add(item)

Toto prakticky slúži ako alias metódy add na triede Basket. Alias sa však správa úplne rovnako ako pôvodná metóda a môžeme ho použiť pri volaní na prijímači vo vnútri lambdy, ktorú volá funkcia basket.

>> Home