Kotlin is a functional language, and that means functions are front and center. The language is packed with features to make coding functions easy and expressive. In this post, you’ll learn about extension functions, higher-order functions, closures, and inline functions in Kotlin.
In the previous article, you learned about top-level functions, lambda expressions, member functions, anonymous functions, local functions, infix functions, and finally member functions in Kotlin. In this tutorial, we’ll continue to learn more about functions in Kotlin by digging into:
- extension functions
- higher-order functions
- closures
- inline functions
1. Extension Functions
Wouldn’t it be nice if the String
type in Java had a method to capitalize the first letter in a String
—like ucfirst()
in PHP? We could call this method upperCaseFirstLetter()
.
To realize this, you could create a String
subclass which extends the String
type in Java. But remember that the String
class in Java is final—which means that you can’t extend it. A possible solution for Kotlin would be to create helper functions or top-level functions, but this might not be ideal because we then couldn’t make use of the IDE auto-complete feature to see the list of methods available for the String
type. What would be really nice would be to somehow add a function to a class without having to inherit from that class.
Well, Kotlin has us covered with yet another awesome feature: extension functions. These give us the ability to extend a class with new functionality without having to inherit from that class. In other words, we don’t need to create a new subtype or alter the original type.
An extension function is declared outside the class it wants to extend. In other words, it is also a top-level function (if you want a refresher on top-level functions in Kotlin, visit the More Fun With Functions tutorial in this series).
Along with extension functions, Kotlin also supports extension properties. In this post, we’ll discuss extension functions, and we’ll wait until a future post to discuss extension properties along with classes in Kotlin.
Creating an Extension Function
As you can see in the code below, we defined a top-level function as normal for us to declare an extension function. This extension function is inside a package called com.chike.kotlin.strings
.
To create an extension function, you have to prefix the name of the class that you’re extending before the function name. The class name or the type on which the extension is defined is called the receiver type, and the receiver object is the class instance or value on which the extension function is called.
package com.chike.kotlin.strings fun String.upperCaseFirstLetter(): String { return this.substring(0, 1).toUpperCase().plus(this.substring(1)) }
Note that the this
keyword inside the function body references the receiver object or instance.
Calling an Extension Function
After creating your extension function, you’ll first need to import the extension function into other packages or files to be used in that file or package. Then, calling the function is just the same as calling any other method of the receiver type class.
package com.chike.kotlin.packagex import com.chike.kotlin.strings.upperCaseFirstLetter print("chike".upperCaseFirstLetter()) // "Chike"
In the example above, the receiver type is class String
, and the receiver object is "chike"
. If you’re using an IDE such as IntelliJ IDEA that has the IntelliSense feature, you’d see your new extension function suggested among the list of other functions in a String
type.
Java Interoperability
Note that behind the scenes, Kotlin will create a static method. This static method’s first argument is the receiver object. So it is easy for Java callers to call this static method and then pass the receiver object as an argument.
For example, if our extension function was declared in a StringUtils.kt file, the Kotlin compiler would create a Java class StringUtilsKt
with a static method upperCaseFirstLetter()
.
/* Java */ package com.chike.kotlin.strings public class StringUtilsKt { public static String upperCaseFirstLetter(String str) { return str.substring(0, 1).toUpperCase() + str.substring(1); } }
This means that Java callers can simply call the method by referencing its generated class, just like for any other static method.
/* Java */ print(StringUtilsKt.upperCaseFirstLetter("chike")); // "Chike"
Remember that this Java interop mechanism is similar to how top-level functions work in Kotlin, as we discussed in the More Fun With Functions post!
Extension Functions vs. Member Functions
Note that extension functions can’t override functions already declared in a class or interface—known as member functions (if you want a refresher on member functions in Kotlin, take a look at the previous tutorial in this series). So, if you have defined an extension function with exactly the same function signature—the same function name and same number, types and order of arguments, regardless of return type—the Kotlin compiler won’t invoke it. In the process of compilation, when a function is invoked, the Kotlin compiler will first look for a match in the member functions defined in the instance type or in its superclasses. If there is a match, then that member function is the one that is invoked or bound. If there is no match, then the compiler will invoke any extension function of that type.
So in summary: member functions always win.
Let’s see a practical example.
class Student { fun printResult() { println("Printing student result") } fun expel() { println("Expelling student from school") } } fun Student.printResult() { println("Extension function printResult()") } fun Student.expel(reason: String) { println("Expelling student from School. Reason: "$reason"") }
In the code above, we defined a type called Student
with two member functions: printResult()
and expel()
. We then defined two extension functions that have the same names as the member functions.
Let’s call the printResult()
function and see the result.
val student = Student() student.printResult() // Printing student result
As you can see, the function that was invoked or bound was the member function and not the extension function with same function signature (though IntelliJ IDEA would still give you a hint about it).
However, calling the member function expel()
and the extension function expel(reason: String)
will produce different results because the function signatures are different.
student.expel() // Expelling student from school student.expel("stole money") // Expelling student from School. Reason: "stole money"
Member Extension Functions
You’ll declare an extension function as a top-level function most of the time, but note that you can also declare them as member functions.
class ClassB { } class ClassA { fun ClassB.exFunction() { print(toString()) // calls ClassB toString() } fun callExFunction(classB: ClassB) { classB.exFunction() // call the extension function } }
In the code above, we declared an extension function exFunction()
of ClassB
type inside another class ClassA
. The dispatch receiver is the instance of the class in which the extension is declared, and the instance of the receiver type of the extension method is called the extension receiver. When there is a name conflict or shadowing between the dispatch receiver and the extension receiver, note that the compiler chooses the extension receiver.
So in the code example above, the extension receiver is an instance of ClassB
—so it means the toString()
method is of type ClassB
when called inside the extension function exFunction()
. For us to invoke the toString()
method of the dispatch receiver ClassA
instead, we need to use a qualified this
:
// ... fun ClassB.extFunction() { print([email protected]()) // now calls ClassA toString() method } // ...
2. Higher-Order Functions
A higher-order function is just a function that takes another function (or lambda expression) as a parameter, returns a function, or does both. The last()
collection function is an example of a higher-order function from the standard library.
val stringList: List= listOf("in", "the", "club") print(stringList.last{ it.length == 3}) // "the"
Here we passed a lambda to the last
function to serve as a predicate to search within a subset of elements. We’ll now dive into creating our own higher-order functions in Kotlin.
Creating a Higher-Order Function
Looking at the function circleOperation()
below, it has two parameters. The first, radius
, accepts a double, and the second, op
, is a function that accepts a double as input and also returns a double as output—we can say more succinctly that the second parameter is “a function from double to double”.
Observe that the op
function parameter types for the function are wrapped in parentheses ()
, and the output type is separated by an arrow. The function circleOperation()
is a typical example of a higher-order function that accepts a function as a parameter.
fun calCircumference(radius: Double) = (2 * Math.PI) * radius fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0) fun circleOperation(radius: Double, op: (Double) -> Double): Double { val result = op(radius) return result }
Invoking a Higher-Order Function
In the invocation of this circleOperation()
function, we pass another function, calArea()
, to it. (Note that if the method signature of the passed function doesn’t match what the higher-order function declares, the function call won’t compile.)
To pass the calArea()
function as a parameter to circleOperation()
, we need to prefix it with ::
and omit the ()
brackets.
print(circleOperation(3.0, ::calArea)) // 28.274333882308138 print(circleOperation(3.0, calArea)) // won't compile print(circleOperation(3.0, calArea())) // won't compile print(circleOperation(6.7, ::calCircumference)) // 42.09734155810323
Using higher-order functions wisely can make our code easier to read and more understandable.
Lambdas and Higher-Order Functions
We can also pass a lambda (or function literal) to a higher-order function directly when invoking the function:
circleOperation(5.3, { (2 * Math.PI) * it })
Remember, for us to avoid naming the argument explicitly, we can use the it
argument name auto-generated for us only if the lambda has one argument. (If you want a refresher on lambda in Kotlin, visit the More Fun With Functions tutorial in this series).
Returning a Function
Remember that in addition to accepting a function as a parameter, higher-order functions can also return a function to callers.
fun multiplier(factor: Double): (Double) -> Double = { number -> number*factor }
Here the multiplier()
function will return a function that applies the given factor to any number passed into it. This returned function is a lambda (or function literal) from double to double (meaning the input param of the returned function is a double type, and the output result is also a double type).
val doubler = multiplier(2) print(doubler(5.6)) // 11.2
To test this out, we passed in a factor of two and assigned the returned function to the variable doubler. We can invoke this like a normal function, and whatever value we pass into it will be doubled.
3. Closures
A closure is a function that has access to variables and parameters which are defined in an outer scope.
fun printFilteredNamesByLength(length: Int) { val names = arrayListOf("Adam", "Andrew", "Chike", "Kechi") val filterResult = names.filter { it.length == length } println(filterResult) } printFilteredNamesByLength(5) // [Chike, Kechi]
In the code above, the lambda passed to the filter()
collection function uses the parameter length
of the outer function printFilteredNamesByLength()
. Note that this parameter is defined outside the scope of the lambda, but that the lambda is still able to access the length
. This mechanism is an example of closure in functional programming.
4. Inline Functions
In More Fun With Functions, I mentioned that the Kotlin compiler creates an anonymous class in earlier versions of Java behind the scenes when creating lambda expressions.
Unfortunately, this mechanism introduces overhead because an anonymous class is created under the hood every time we create a lambda. Also, a lambda that uses the outer function parameter or local variable with a closure adds its own memory allocation overhead because a new object is allocated to the heap with every invocation.
Comparing Inline Functions With Normal Functions
To prevent these overheads, the Kotlin team provided us with the inline
modifier for functions. A higher-order function with the inline
modifier will be inlined during code compilation. In other words, the compiler will copy the lambda (or function literal) and also the higher-order function body and paste them at the call site.
Let’s look at a practical example.
fun circleOperation(radius: Double, op: (Double) -> Double) { println("Radius is $radius") val result = op(radius) println("The result is $result") } fun main(args: Array) { circleOperation(5.3, { (2 * Math.PI) * it }) }
In the code above, we have a higher-order function circleOperation()
that doesn’t have the inline
modifier. Now let’s see the Kotlin bytecode generated when we compile and decompile the code, and then compare it with one that has the inline
modifier.
public final class InlineFunctionKt { public static final void circleOperation(double radius, @NotNull Function1 op) { Intrinsics.checkParameterIsNotNull(op, "op"); String var3 = "Radius is " + radius; System.out.println(var3); double result = ((Number)op.invoke(radius)).doubleValue(); String var5 = "The result is " + result; System.out.println(var5); } public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); circleOperation(5.3D, (Function1)null.INSTANCE); } }
In the generated Java bytecode above, you can see that the compiler called the function circleOperation()
inside the main()
method.
Let’s now specify the higher-order function as inline
instead, and also see the bytecode generated.
inline fun circleOperation(radius: Double, op: (Double) -> Double) { println("Radius is $radius") val result = op(radius) println("The result is $result") } fun main(args: Array) { circleOperation(5.3, { (2 * Math.PI) * it }) }
To make a higher-order function inline, we have to insert the inline
modifier before the fun
keyword, just like we did in the code above. Let’s also check the bytecode generated for this inline function.
public static final void circleOperation(double radius, @NotNull Function1 op) { Intrinsics.checkParameterIsNotNull(op, "op"); String var4 = "Radius is " + radius; System.out.println(var4); double result = ((Number)op.invoke(radius)).doubleValue(); String var6 = "The result is " + result; System.out.println(var6); } public static final void main(@NotNull String[] args) { Intrinsics.checkParameterIsNotNull(args, "args"); double radius$iv = 5.3D; String var3 = "Radius is " + radius$iv; System.out.println(var3); double result$iv = 6.283185307179586D * radius$iv; String var9 = "The result is " + result$iv; System.out.println(var9); }
Looking at the generated bytecode for the inline function inside the main()
function, you can observe that instead of calling the circleOperation()
function, it has now copied the circleOperation()
function body including the lambda body and pasted it at its call-site.
With this mechanism, our code has been significantly optimized—no more creation of anonymous classes or extra memory allocations. But be very aware that we’d have a larger bytecode behind the scenes than before. For this reason, it is highly recommended to only inline smaller higher-order functions that accept lambda as parameters.
Many of the standard library higher-order functions in Kotlin have the inline modifier. For example, if you take a peek at the collection operation functions filter()
and first()
, you’ll see that they have the inline
modifier and are also small in size.
public inline funIterable .filter(predicate: (T) -> Boolean): List { return filterTo(ArrayList (), predicate) } public inline fun Iterable .first(predicate: (T) -> Boolean): T { for (element in this) if (predicate(element)) return element throw NoSuchElementException("Collection contains no element matching the predicate.") }
Remember not to inline normal functions which don’t accept a lambda as a parameter! They will compile, but there would be no significant performance improvement (IntelliJ IDEA would even give a hint about this).
The noinline
Modifier
If you have more than two lambda parameters in a function, you have the option to decide which lambda not to inline using the noinline
modifier on the parameter. This functionality is useful especially for a lambda parameter that will take in a lot of code. In other words, the Kotlin compiler won’t copy and paste that lambda where it is called but instead will create an anonymous class behind the scene.
inline fun myFunc(op: (Double) -> Double, noinline op2: (Int) -> Int) { // perform operations }
Here, we inserted the noinline
modifier to the second lambda parameter. Note that this modifier is only valid if the function has the inline
modifier.
Stack Trace in Inline Functions
Note that when an exception is thrown inside an inline function, the method call stack in the stack trace is different from a normal function without the inline
modifier. This is because of the copy and paste mechanism employed by the compiler for inline functions. The cool thing is that IntelliJ IDEA helps us to easily navigate the method-call stack in the stack trace for an inline function. Let’s see an example.
inline fun myFunc(op: (Double) -> Double) { throw Exception("message 123") } fun main(args: Array) { myFunc({ 4.5 }) }
In the code above, an exception is thrown deliberately inside the inline function myFunc()
. Let’s now see the stack trace inside IntelliJ IDEA when the code is run. Looking at the screenshot below, you can see that we are given two navigation options to choose: the Inline function body or the inline function call site. Choosing the former will take us to the point the exception was thrown in the function body, while the latter will take us to the point the method was called.
If the function was not an inline one, our stack trace would be like the one you might be already familiar with:
Conclusion
In this tutorial, you learned even more things you can do with functions in Kotlin. We covered:
- extension functions
- higher-order functions
- closures
- inline functions
In the next tutorial in the Kotlin From Scratch series, we’ll dig into object-oriented programming and start learning how classes work in Kotlin. See you soon!
To learn more about the Kotlin language, I recommend visiting the Kotlin documentation. Or check out some of our other Android app development posts here on Envato Tuts+!
-
Android SDKJava vs. Kotlin: Should You Be Using Kotlin for Android Development?
-
Android SDKIntroduction to Android Architecture Components
-
Android SDKHow to Use the Google Cloud Vision API in Android Apps
-
Android SDKWhat Are Android Instant Apps?
Powered by WPeMatico