Kotlin Extension Function Expressions
Lambdas and higher order functions #
Chapter 5 of the Kotlin in Action book has a fantastic deep dive into lambdas.
Chapter 8 of the Kotlin in Action book has a great deep dive into higher order functions.
The following is an overly complex example of a calculator function that has 2 plugins (to add and subtract), built using higher order functions.
run {
val plusPlugin: (Int, Int) -> Int = { x, y -> x + y }
val minusPlugin: (Int, Int) -> Int = { x, y -> x - y }
val calc: (Int, Int, (Int, Int) -> Int) -> Int =
{ x, y, plugin ->
val value = plugin(x, y)
println(value)
value
}
calc(1, 2, plusPlugin)
calc(10, 5, minusPlugin)
"complex-calculator-example"
}
Extension Functions and Lambdas #
Here’s an example of passing a context object to a lambda:
// You can choose what object to bind `this` to in the `call` function.
call { println("${this} ${it}") }
// String. : context
// (String)->Unit : lambda
fun call (functor: String.(String)->Unit) {
functor("Context", "Jane")
"Context".functor("Jane")
}
One of the key concepts is passing a context object to bind this
to (in the lambda), very much
like JavaScript’s call(thisObject, lambda)
method. Watch
this video by Venkat Subramaniam on creating
internal DSLs in Kotlin.
Also,
- Watch this video by Jake Wharton explaining what this is.
- Great stackoverflow discussion on this as well.
Here are more examples of this:
import java.time.*
fun main() {
ex1()
ex2()
ex3()
ex4()
}
fun ex1(){
// String. : context
// (String)->Unit : lambda
fun call (functor: String.(String)->Unit) {
functor("Context", "Jane")
"Context".functor("Jane")
}
// Similar to JS function.call(context, args), where context is this:
// MDN docs for call: https://tinyurl.com/o8eh6te
call { println("${this} ${it}") }
}
fun ex2(){
val ago = "ago"
val from_now = "from_now"
infix fun Int.days(tense:String){
val now = LocalDateTime.now();
val delta = this.toLong();
when(tense){
ago -> println(now.minusDays(delta))
from_now -> println(now.plusDays(delta))
else -> println("?")
}
}
// Simple internal DSL syntax.
2 days ago
2 days from_now
}
fun ex3(){
class Meeting(val name: String){
val start = this
infix fun at(time:IntRange) {
println("$name meeting starts at $time")
}
}
infix fun String.meeting(block: Meeting.()->Unit){
val meeting = Meeting(this)
block(meeting)
meeting.block()
}
// Simple internal DSL syntax.
"planning" meeting {
start at 3..15
}
}
fun ex4(){
class Robot{
val left="left"
val right="right"
val fast="fast"
infix fun turns(direction:String) { println("turns $direction") }
infix fun runs(speed: String) { println("runs $speed") }
}
fun operate(block: Robot.(Robot)->Unit){
val robot = Robot()
block(robot, robot)
robot.block(robot)
}
// Simple internal DSL syntax.
operate{
it turns left
it turns right
it runs fast
}
}
Extension Function Expressions combine:
- Extension functions - Functions added to a type w/out modifying the original
- Function expressions - Undeclared function bodies used as an expression (data)
- High order functions - A function that takes a function or returns a function
Here’s an unsophisticated example of using the 3 things above. This is a simple extension function that allows a List of Strings to be filtered Run the code in the Kotlin playground.
fun main() {
val data = listOf("monkey", "donkey", "banana", "apple")
println( data.filter{ it.startsWith("b") } )
}
fun <T> List<T>.filter(allow: (T) -> Boolean): List<T>{
val newList = ArrayList<T>()
for( item in this ){
if (allow(item)) { newList.add(item) }
}
return newList
}
Example 1 #
Here’s the sophisticated version of this leveraging Extension Function Expressions! Run the code in the Kotlin playground.
fun main() {
val data = listOf("monkey", "donkey", "banana", "apple")
println( data.filter{ startsWith("m") } )
}
fun <T> List<T>.filter(allow: T.() -> Boolean): List<T>{
val newList = ArrayList<T>()
for( item in this ){
if (item.allow()) { newList.add(item) }
}
return newList
}
Notes:
-
allow: T.() -> Boolean
is used instead ofallow: (T) -> Boolean
(from the unsophisticated example). TheT.
means that the function expression is an extension function ofT
itself! -
This means that
allow()
is an extension function ofT
! -
Since
allow()
is an extension function ofT
,this
is passed to it, which in this case is aString
. -
Given the change above, the
if (item.allow()) { newList.add(item) }
statement is used instead ofif (allow(item)) { newList.add(item) }
(from the unsophisticated example).
Example 2 #
The following example shows a toast, and allows the creation of a simple DSL syntax for creating the
toast. And there’s no way to forget calling show()
once it’s created!
inline fun toast(context: Context,
text: String = "",
duration: Int = Toast.LENGTH_SHORT,
functor: Toast.() -> Unit) {
val toast: Toast = Toast.makeText(context, text, duration)
toast.functor()
toast.show()
}
Example of using the function above (with simple DSL syntax for making the toast).
toast(fragment.getParentActivity()) {
setText(R.string.message_cant_make_autocomplete_request_if_location_is_null)
duration = Toast.LENGTH_LONG
}
Note that the toast()
function has 1 required parameter (context
) and the other parameters are
optional (and have default values). This means that you can call this function w/ just the 1st
argument. In the DSL syntax, the middle 2 arguments aren’t passed, and only the 1st argument
(fragment.getParentActivity()
) and the last argument (the lambda expression) is passed.
Example 3 #
Very similar to Example 2, but this is for showing a Snackbar. Again, there’s no way to forget
calling show()
after the Snackbar has been created.
inline fun snack(view: View,
text: String = "",
duration: Int = Snackbar.LENGTH_SHORT,
functor: Snackbar.() -> Unit) {
val snackbar: Snackbar = Snackbar.make(view, text, duration)
snackbar.functor()
snackbar.show()
}
How the function above might be used.
snack(fragmentContainer) {
setText(R.string.message_making_api_call_getCurrentPlace)
duration = Snackbar.LENGTH_SHORT
}
Example 4 #
This is an example of a non local return, Kotlin in Action, Ch 8 for an extension function w/ lambdas.
public inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
try { this.close() }
catch(closeException: Throwable){exception?.addSuppressed(closeException)}
}
}
Example of using this is shown below.
fun readFirstLineFromFile(path: String): String {
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
Notes:
-
When the return executes in the lambda, it returns from the function in which the lambda was called from (not just the lambda block itself).
-
The return from the outer function is possible only if the function that takes the lambda as an argument is inlined.
-
More information on when to inline extension functions in Kotlin in Action, Ch 8. Basically its best to include a function extension that accepts lambdas (IntelliJ IDEA has hints that help with this).
You can write a local return from a lambda expression as well. A local return in a lambda is similar to a break expression in a for loop. It stops the execution of the lambda and continues execution of the code from which the lambda was invoked. To distinguish a local return from a non-local one, you use labels. You can label a lambda expression from which you want to return, and then refer to this label after the return keyword.
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List<Person>) {
people.forEach label@{
if (it.name == "Alice") return@label
}
println("Alice might be somewhere")
}
👀 Watch Rust 🦀 live coding videos on our YouTube Channel.
📦 Install our useful Rust command line apps usingcargo install r3bl-cmdr
(they are from the r3bl-open-core project):
- 🐱
giti
: run interactive git commands with confidence in your terminal- 🦜
edi
: edit Markdown with style in your terminalgiti in action
edi in action