Advertisement
  1. Code
  2. Kotlin
Code

Kotlin From Scratch: Advanced Functions

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Kotlin From Scratch.
Kotlin From Scratch: More Fun With Functions
Kotlin From Scratch: Classes and Objects

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, 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.

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.

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. 

IntelliJ IDEA intellisense feature

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()

This means that Java callers can simply call the method by referencing its generated class, just like for any other static method. 

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.

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. 

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. 

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. 

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:

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. 

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.

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.

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: 

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. 

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).  

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. 

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. 

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. 

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.

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. 

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. 

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.  

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.

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.

IntelliJ IDEA stack trace for inline function

If the function was not an inline one, our stack trace would be like the one you might be already familiar with:

IntelliJ IDEA stack trace for normal function

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+!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.