In diesem Artikel möchte ich einen Einblick zeigen, wie higher-order-functions in der schönen Sprache Kotlin geschrieben werden. Ich habe versucht, meine Codeschnipsel simpel zu halten, dass man sie einfach kopiert und testen kann. Und wenn Sie bereits ein Experte sind, kann mein letztes Beispiel interessant sein 😉
Example 1: Higher Order Function – Starter Pack
Starten wir mit einer super simplen higher-order-function. Die basicHigherOrderFunction
Methode bekommt einen Parameter namens myFunction
mit Unit
als return value. Man erkennt, dass es sich um eine higher-order-function handelt, wenn man sich den Typ des Parameters genauer anschaut: () -> R
. Nun kann ich den übergebenen parameter myFunction
wie eine Methode mit normalen Klammern in meiner Methode basicHigherOrderFunction
aufrufen.
/** My Higher-Order Function */ fun basicHigherOrderFunction(myFunction: () -> Unit) { println("Start my Function!") myFunction() // call my received parameter println("Function finished") }
Wenn man nun die Methode basicHigherOrderFunction
aufrufen würde, ist es möglich eine ganze Funktion als Parameter mit geschweiften Klammern zu übergeben. Da die Methode basicHigherOrderFunction
allerdings nur einen Parameter hat und wir in Kotlin unterwegs sind, kann man die runden Klammern auch entfernen wie im letzten Beispiel.
fun main() { basicHigherOrderFunction({ // inside normal brackets val result = 1 + 2 }) basicHigherOrderFunction { // only curly brackets val result = 1 + 2 } }
Super schlicht oder? 😎
Example 2: Higher Order Function mit mehreren Parametern
In meinem nächsten Beispiel möchte ich etwas komplizierter werden: Ich möchte meine Funktion nur ausführen, wenn der übergebene Username einem bestimmten String entspricht.
/** My Higher-Order Function */ fun execute(username: String, myFunction: () -> Unit) { when(username) { "Arnold" -> myFunction() else -> throw NotAuthorizedException("User is not authorized") } }
Wenn man sich meine higher-order-function execute(...)
anschaut, erkennt man, dass ich jetzt zwei Parameter erwarte. Der String username
und mein Lambda myFunction
. Auf der Seite, welche die Methode aufruft – in diesem Fall die main
Methode – übergebe ich nun den Usernamen und außerhalb der runden Klammern meine Funktion, welche dann aufgerufen werden soll, wenn der username einen bestimmten Wert hat.
In diesem Beispiel wird nun eine NotAuthorizedException geworfen, da mein Username dem Wert Lou
und nicht Arnold
entspricht. Und mein Lambda wird nie ausgeführt, weswegen wir auch kein println
statement sehen.
fun main() { execute("Lou") { println("Print this if I am allowed") } }
Kann man aber noch komplexer werden? – Das kann man immer 😍
Example 3: Higher Order Function mit eigenem Parameter
Jetzt möchte ich meiner übergebenen Funktion einen Parameter mitgeben, welche ich in meinem Lambda benutzen kann.
/** My Higher-Order Function */ fun getRandomNumber(myFunction: Int.() -> Unit) { println("Some really complex calculating...") val myRandomNumber = Random.nextInt() println("Passing result to myFunction()") myFunction(myRandomNumber) }
Man erkennt, dass sich der Lambda Typ minimal verändert hat. Und zwar von () -> Unit
zu Int.() -> Unit
. Der Int
vor dem .
sagt Kotlin, dass myFunction
einen parameter vom type Int
erwartet. Also muss ich in meiner Methode getRandomNumber
meinem Lambda Parameter einen Integer übergeben. In diesem Fall einen zufälligen Int. In meiner main
Funktion werde ich diesen nur printen. Zugriff auf den Integer bekomme ich über das Keyword this
.
fun main() { getRandomNumber { println("This is my random number: $this") } }
Und ja, es geht noch komplexer und verrückter! 🤓
Example 4: Higher Order Function mit return lambda
Nun dreh ich den Spieß um. Ich will einen zufälligen Integer in meiner main
Methode meinem Lambda übergeben, um diesen dann in meiner printInt
Methode diesen Wert zu printen.
/** My Higher-Order Function */ fun printInt(myFunction: () -> Int) { println("Passing result to myFunction()") val myRandomNumber = myFunction() println("This is my random Number $myRandomNumber") }
Man erkennt jetzt wieder, dass sich die Deklaration des myFunction
Parameters von Int.() -> Unit
zu () -> Int
verändert hat. Das bedeutet, dass mein übergebener Lambda mit dem Namen myFunction
jetzt einen Integer statt eines Units zurückgibt, welchen ich dann in meiner printInt
Methode benutzen kann.
fun main() { printInt { println("Some really complex calculating...") Random.nextInt() } }
Allerdings gibt es noch ein paar Keywörter, welche man sich umbedingt anschauen sollte 😈
Inline Modifier
Wenn man eine super einfache higher-order-funktion wie in meinem ersten Beispiel hat, erstellt Kotlin jedes mal ein eigenes Lambda Object im Hintergrund für den Part in den geschweiften Klammern. Wenn man jetzt eine higher-order-function in einer while-loop aufrufen würde, führt das dazu, dass ziemlich viele Objekte im Hintergrund erstellt werden.
Natürlich hat Kotlin hier auch eine Lösung parat 🤘 Und zwar das inline
keyword, welche man an den Anfang der Funktion packt. Aber was verändert das jetzt?
Mithilfe des inline
keyword kopiert der Kotlin Compiler den Inhalt der higher-order-function auf die Seite des Aufrufers. So werden also gar keine neuen Objekte im Hintergrund erstellt.
Wie genau sieht das jetzt aber dann aus? Hier ein Beispiel. Kotlin erstellt ein eigenes Objekt nur für val result = 1 + 2
. In diesem Fall hätte ich jetzt zwei unnötige Objekte im Hintergrund 😱
fun main() { basicHigherOrderFunction({ // inside normal brackets val result = 1 + 2 }) basicHigherOrderFunction { // only curly brackets val result = 1 + 2 } }
Weil niemand unnötige Objekte benötigt schreibe ich das inline
keyword an den Anfang der basicHigherOrderFunction
. Der Kotlin Compiler macht nun so etwas:
fun main() { println("Start my Function!") val result = 1 + 2 println("Function finished") println("Start my Function!") val result1 = 1 + 2 println("Function finished") }
Keine unnötigen Objekte überhaupt! Man kann sehen, dass der Inhalt meiner higher-order-function auf die caller seite gepackt wurde.
Um jetzt den Leser dieses Artikels vollends zu verwirren möchte ich auch noch auf einen interessantes Anwendungsfall in Kotlin eingehen, welcher vor allem für die Experten interessant sein könnte.
Generics und eine where clause in der Methode Deklaration?
Wenn man Code in Kotlin schreibt hat man mit hoher Wahrscheinlichkeit schonmal die .ifEmpty { ... }
Extension-Function benutzt, welche dem Entwickler erlaubt eine Art default wert zurückzugeben, wenn eine collection leer ist. Wenn man sich diese Extension function anschaut erkennt man, dass es eine higher-order-function ist, welche auch den inline
Modifier benutzt. Aber was genau passiert da? 😈
public inline fun <C, R> C.ifEmpty(defaultValue: () -> R): R where C : Collection<*>, C : R { return if (isEmpty()) defaultValue() else this }
➡️ Mithilfe des inline
modifiers erkennen wir, dass der Inhalt der Methode ifEmpty
an die Stelle kopiert wird, wo man diese Methode aufruft, welche die Erstellung eines Object verhindert.
➡️ Danach wurden zwei Generics C
und R
definiert
➡️ Da es sich um eine Extension Function handelt erkennt man nun, dass die Methode das Generic C
erweitert.
➡️ Als Parameter wurde ein Lambda mit dem Namen defaultValue
übergeben, welche ein return value vom typ R
hat und die ganze Methode ifEmpty
selbst gibt einen wert vom Typ R
zurück. Wenn man allerdings diese Methode auf einer Collection aufruft, die nicht leer ist, wird immer noch eine Collection zurückgegeben. Aber wie kann as möglich sein?
➡️ Und hier kommt der interessante Teil. In Kotlin kann man eine where
clause nach der deklaration des return values definieren. Hier hat das Kotlin Team definiert, dass C
eine Collection sein muss und C
auch ein Typ R
ist. Das ist der Trick!
Falls du noch mehr darüber lesen willst klicke hier.
Wenn du diesen Teil meines Artikels erreicht hast, hoffe ich, dass etwas interessantes für dich dabei war.
Danke fürs Lesen 🤓