Loading...
Home
  • Tech Blogs
  • Videos
  • Conferences
    • Droidcon News
    • Upcoming Conferences
    • Become a Partner
    • Past Events
    • Keep Me Informed
    • Diversity Scholarships
  • Community
    • droidcon Team
    • How to Hold a Droidcon
  • Android Careers
Sign In

Global CSS

 

Kotlin’s Noinline & Crossline, once for all

 

 
Nicola Corti
Android Infra @Spotify | ex @Yelp | Manager @GDGPisa | Community craftsman 🤝
Published: August 12, 2020
Tweet
Share
Keikyū Line Haneda Station — Tokyo, Japan

 

Kotlin, as most of the programming languages, has several reserved keywords. You can find all of them listed on this page of the official documentation: keyword-reference

 

Image for post

The keyword reference page of the Kotlin official documentation

 

I invite you to spend some time going through the list. 
Do you know all the keywords?

If you wrote some Kotlin code, you probably encountered most of them. Some keywords in that list though are less commonly used, therefore they could be harder to remember. One example: the noinline and the crossinline keywords.

The keyword reference gives the following definitions:

 

noinline turns off inlining of a lambda passed to an inline function

crossinline forbids non-local returns in a lambda passed to an inline function

 

The latter can sound a bit complicated for an inexperienced developer. Given that those keywords are not frequently used in daily development, remembering the exact semantic could be harder. If you speak with other Kotlin developers, chances are that they never used the crossinlinekeyword, and they probably don't know when to use it.

The official documentation can help understand those keywords, but in this case, I believe an example is worth a thousand words.

In this blog-post, we will learn the semantic of the noinline and crossinline keyword, with a simple example using the Kotlin Playground(with snippets that you can run and edit in your browser to follow along the article).

inline

To explain those keywords, we have to first explain the inline keyword, and the Inline Functions feature of Kotlin.

Let’s start with an example: a function to do something if you’re running on Debug:

 

fun doOnDebug(block: () -> Unit) {
    // On Android you can replace with a BuildConfig.DEBUG
    if (Env.DEBUG) {
        block()
    }
}

fun main() {
    doOnDebug {
        println("I'm on debug ¯\\_(ツ)_/¯")
    }
}

object Env {
    val DEBUG = true
}

 

The function is trivial: it takes a lambda as a parameter and executes it if you execute on a Debug environment (we are simulating it using the Boolean DEBUG property).

Using the Show Kotlin Bytecode feature of IntelliJ, we can see how the equivalent bytecode code will look like¹:

 

Image for post

Decompiled bytecode of a regular function with lambda parameters¹

 

As we can see from the image, the doOnDebug function is compiled to a function that takes a Function0 as a single parameter. Function0 is an interface declared in the Kotlin Standard Library for the JVM. It's a functional interface, used to pass a function that takes zero parameters from Java to Kotlin.

Kotlin offers the Inline Functions feature exactly to mitigate this overhead. If we add the inline keyword in front of the function definition:

 

inline fun doOnDebug(block: () -> Unit)

 

The resulted bytecode will look like this:

 

Image for post

Decompiled bytecode of an inline function invocation

 

As we can see from the bytecode, the doOnDebug code is inlined in the main() function body. The lambda parameter is affected as well as its body is also copied.

 

In other words, inline will “copy-paste” the function body, and the lambda parameter at every call site.

 

This means that we can avoid creating an instance of a Function0, resulting in saved memory allocation.

To recap, the inline keywords allows you to reduce the runtime overhead of functions taking lambda as parameters by:

  • Inlining their body at every call site.
  • Inlining every lambda parameter at the call site.

 

Please note that inlining functions can increase the size of your generated code, so make sure you do it only for small functions.

 

The full example is available in this playground (you can edit the code and run it as well):

 

With noinline and crossinline you can then customize the inlinebehavior, let's see how.

noinline

Let’s now extend our example with some logging on the main() function:

 

fun main() {
    println("START main()")
    doOnDebug {
        println("I'm on debug ¯\\_(ツ)_/¯")
    }
    println("END main()")
}

 

And let’s add some logging to doOnDebug as well. For doOnDebug, since it's a util function, we also want to specify which logger to use.

For the sake of simplicity, a logger will be just a simple function:

 

logger: (String) -> Unit

 

So we can extend the doOnDebug with invocations of this logger.

Moreover, we also want to flush it at the end of the function. Let’s assume we invoke a flush function:

 

inline fun doOnDebug(
    logger: (String) -> Unit,
    block: () -> Unit
) {
    if (Env.DEBUG) {
        logger("[LOG] Running doOnDebug...")
        block()
        logger("[LOG] Flushing the log...")
        flush(logger)
    }
}

fun flush(logger: (String) -> Unit) { 
    // Flush the logger here
}

 

As it is right now, this code won’t compile. It will fail with the following error message:

 

Illegal usage of inline-parameter 'logger' in 'public inline fun doOnDebug(logger: (String) -> Unit = ..., block: () -> Unit): Unit defined in root package in file File.kt'. Add 'noinline' modifier to the parameter declaration

 

The error message suggests we add the noinline keyword to the loggerparameter. Adding this will make the code compile:

 

inline fun doOnDebug(
    noinline logger: (String) -> Unit,

 

Why is this needed?

As previously mentioned, the inline keyword affects the function body and all the lambda parameters. In our example, logger is inlined as well.

 

Inlining a lambda limits what you can do with it. You can only invoke it. If you wish to store it in a variable or pass them to another function (as we do for the flush function) you need to tell the compiler to avoid inlining it.

 

If we try to run, the console output now will be:

 

START main()
[LOG] Running doOnDebug...
I'm on debug ¯\_(ツ)_/¯
[LOG] Flushing the log...
END main()

 

To recap, the noinline keyword is a mechanism to prevent the inlining of a specific lambda parameter of an inline function. It's useful if you wish to pass the lambda around or store it in a variable.

The full example until here is available in this playground:

 

crossinline

Let’s go back to the call site, and complicate our function a bit more.

First, we add a times: Int = 1 parameters, to allow repeating the lambda multiple times:

 

inline fun doOnDebug(
    noinline logger: (String) -> Unit,
    times : Int = 1,
    block: () -> Unit
) {
    if (Env.DEBUG) {
        logger("[LOG] Running doOnDebug...")
        repeat(times) {
            logger("[LOG] Iteration #$it...")
            block()
        }
        logger("[LOG] Flushing the log...")
        flush(logger)
    }
}

 

Then, our code contains a katakana symbol: ツ. Let’s say we want to play defensive and avoid printing if the terminal doesn’t support UTF-8.

Let’s update the doOnDebug call site like this:

 

doOnDebug {
    if(!Env.UTF8_SUPPORT) {
        return
    }
    println("I'm on debug ¯\\_(ツ)_/¯")
}

 

As long as Env.UTF8_SUPPORT is true, everything goes smoothly. Here is what is printed on the console:

 

START main()
[LOG] Running doOnDebug...
[LOG] Iteration #0...
I'm on debug ¯\_(ツ)_/¯
[LOG] Flushing the log...
END main()

 

But if you try to change Env.UTF8_SUPPORT to false, this will happen:

 

START main()
[LOG] Running doOnDebug...
[LOG] Iteration #0...

 

Seems like the execution terminated at the return, but the flushing of the logger and END main() line are missing 🧐.

 

Time to (de)-bug

To fully understand what is going on, let’s introduce a bug 🐛.

Instead of Env.UTF8_SUPPORT returning always either true or false, let's call Random. We can introduce some flakiness by letting Env.UTF8_SUPPORTreturn true only 80% of the times:

 

val UTF8_SUPPORT : Boolean get() = Random.nextInt(5) != 0

 

Now let’s try to call doOnDebug with repeat = 10 and see what's the output on the console. We expect to see on the average ~9 shrug on the screen:

 

Run main()
[LOG] Running doOnDebug
[LOG] Iteration 0
¯\_(ツ)_/¯
[LOG] Iteration 1
¯\_(ツ)_/¯
[LOG] Iteration 2

 

What happens instead is that in my run, after two iterations, UTF8_SUPPORTis false and the whole execution is halted.

What is happening here?

Given that the lambda parameter of doOnReturn is inlined, also the returnis inlined at the call site. This means that in the resulting code, that returnis causing a return of the outer function, the main().

 

This is called a non-local return in Kotlin.

 

To prevent this behavior we can mark the parameter block as crossinline. This keyword prevents non-local returns in the specified lambda parameter.

If we try to apply it to our function:

 

inline fun doOnDebug(
    noinline logger: (String) -> Unit,
    times: Int = 1,
    crossinline block: () -> Unit

 

We will see that our code will not compile anymore with the following error message:

 

'return' is not allowed here

 

The IDE autocompletion is also suggesting you to use a labeled return: return@doOnDebug. That will make sure your return will apply only to the doOnDebug function and not to the main() function.

 

Image for post

IntelliJ suggesting a labelled return

 

The full example of this article is available in this playground:

 

 

I hope that with some examples, the inline, noinline, and crossinlinekeywords are clearer now.

Happy inlining 🚇


[1] Are you wondering what that (Function0)null.INSTANCE means? 🤨 You’re not alone.

There is a bug in the Kotlin Bytecode decompiler of IntelliJ (IDEABKL-7385) that causes the lambda class to be lost (and replaced with a null).

If you use JD-GUI to analyze the bytecode instead, you will see that the parameter is actually Foo$main$1.INSTANCE (screenshot) and the resulting lambda is also decompiled correctly with the corresponding .INSTANCE field (screenshot).

 

Originally published at https://ncorti.com on July 22, 2020.

 

Thanks to Roberto Orgiu and Fabio Collini. 

Some rights reserved

 

Tags: Kotlin,Kotlin Beginners,Android App Development,Android,Software Development

 

View original article at: 


 

Originally published: July 08, 2020

Android News
Our Engineering Roadmap
Our Engineering Roadmap

By Mark Ng

We just completed our engineering road map for our Android apps at Australia Post. Each year we get together and try to decide on what we are going to do from an engineering perspective for the next 12 months. Each team gets to decide on what should be done now, what they want to complete by the end of the year and whats on the horizon for next year.

By ProAndroidDev -
Android News
Android Activity Lifecycle considered harmful
Android Activity Lifecycle considered harmful

By Eric Silverberg

The Android activity lifecycle is one of the first architectural concepts learned by a new Android developer. Ask any Android dev and they’ll likely all have seen this handy diagram from Google: 

By ProAndroidDev -
Android News
Our Safe Approach to Android Jetpack Navigation in a Multi-Modular App
Our Safe Approach to Android Jetpack Navigation in a Multi-Modular App

By Alejandro Weichandt

It has been a year since we started working on the Android version of the mobile app at Sync. During that year, we faced more than once that moment when we had to choose which path to follow on an Architectural decision. This story is about Navigation.

By ProAndroidDev -
Android News
Custom KotlinX Serializers
Custom KotlinX Serializers

By Jobin Lawrance

Let’s say we have a third-party class that we are using as a type in one of our data class that we want to be serialized, then we have to write a custom serializable for @Serializable to work.

 

By ProAndroidDev -
droidcon News

Tech Showcases,

Developer Resources &

Partners

/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/home-details/EmployerBrandingHeader
EmployerBrandingHeader
https://jobs.droidcon.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/jobs-droidcon/jobs.droidcon.com
jobs.droidcon.com

Latest Android Jobs

http://www.kotlinweekly.net/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/kotlin-weekly/Kotlin Weekly
Kotlin Weekly

Your weekly dose of Kotlin

https://proandroiddev.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/pad/ProAndroidDev
ProAndroidDev

Android Tech Blogs, Case Studies and Step-by-Step Coding

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
Zalando

Meet one of Berlin's top employers

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
Academy for App Success

Google Play resources tailored for the global droidcon community

Follow us

Team droidcon

Get in touch with us

Write us an Email

 

 

Quicklinks

> Code of Conduct

> Terms and Conditions

> How to hold a conference

> FAQs

> Imprint

Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.

powered by Breakpoint One