How to return 2+ values with 0 allocation in Kotlin

How to return 2+ values with 0 allocation in Kotlin

·

5 min read

Featured on Hashnode

The problem

Most programming languages, including Kotlin, only allow returning one value, and there's a reason for that: We don't want to depend solely on the order of the parameters, because it can easily break through refactoring, or even before that.

When we want to return multiple things, we put it into a container: a class. However, that approach has 2 caveats:

  1. Requires extra allocation
  2. Requires naming the class

If the code is not called at high frequency (many times per second), and not in UI, the first caveat should not be a concern.

However, the second caveat means that in addition to naming the function and the multiple things you are returning, you need to name that extra class. This is extra burden, and in some cases, it might lead you to give quite uninspiring names like Thing1AndThing2WithDetail3, making the code slightly more complex.

What if I told you that in Kotlin, it's possible to get more than one value out of a function execution, without any allocation, and with no class to name?

The solution

Since Kotlin 1.3, the code below can compile.

val theBonus: Bonus
val theStuff = getStuff { theBonus = it }

doSomething(theStuff, theBonus)

Maybe you know the saying that Kotlin's best feature is how all of its features can work together so smoothly?

Well, here we are using the following Kotlin features together:

  • inline functions/lambdas
  • read-only properties (aka. val)
  • contracts

The getStuff inline function has a contract that promises to the compiler that given it executes without throwing, the lambda it has been passed will always have been called exactly once upon returning.

Thanks to that contract, the compiler allows the read-only property theBonus to be initialized from this lambda, and considers it as initialized after the getStuff call.

The getStuff function is defined like that:

import kotlin.contracts.*

@OptIn(ExperimentalContracts::class)
inline fun getStuff(block: (Bonus) -> Unit): Stuff {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    val stuff = grabStuffFromCargoBikeBasket()
    val bonus = inspirationElixir()
    block(bonus)
    return stuff
}

Here, there's no extra allocation, and no need to name the class. However, you need to name the lambda in the getStuff signature, but block or action is often sufficient for these trailing lambda use cases, so it shouldn't "drain" or consume your creativity.

There's one potential caveat though, and it pertains to the inline nature of the function. As you might know, a function being inlined means that the compiler will basically copy the compiled code at each call-site. That's perfectly fine if the amount of inline code is very little, or quite limited, but if that's a giant function that possibly also calls other inline functions that are potentially substantial as well… it can make the compiled binary grow significantly, which in addition to taking up extra storage for your users, and bandwidth on download, can also make loading the program from a cold-start longer, and increase memory (RAM) consumption. All of this doesn't go in the direction go towards a better UX. That should not be a concern if you are not inlining long algorithms or so long as you stay under a dozen of function calls though. If you're in a such a case and are unsure about the impact, make your own tests to measure the relative impact in context.

By the way, you might have noticed that we had to opt-in to "ExperimentalContracts". This is because the syntax to define contracts in Kotlin might/will change in the future, which mean you'll likely need to migrate the code (hopefully with full IDE-assistance to do it in one click). However, publishing functions that use contracts is perfectly fine, even in a public library, because compiled contracts are already stable since Kotlin 1.3, so for example, users on Kotlin 1.7 will still be able to use contracts from a library compiled with Kotlin 1.3.

An actual use-case

For those that like small stories, I want to share the use-case I had where I came up with this trick.

As I was testing stuff around Wi-Fi connectivity on Android, I wanted to see the signal strength on the system scale (which is often 0 to 4 bars). For historical reasons, there are 2 APIs to do that on Android, one that works on recent versions, and a deprecated one that works on older versions. As usual, I added a when or an if/else expression. However, the APIs has a tricky difference:

  • On older Android versions, you pass the scale: I'd pass 4 or 5.
  • On newer Android versions, you get the scale from the system API, which is dynamic, not much assumptions can be made.

To display the correct signal strength, I'd need two values: the scale, and the max level, which would default to whatever desired on older Android versions.

Sure, I could have used a Double or a Float, but I like my numbers whole when possible, especially when the scale is small, which is the case here.

So the calculateSignalLevel extension function for WifiInfo would take the fallbackNumLevels parameter for older Android versions (defaulting to 5, per my arbitrary choice), the getNumLevels lambda to feed the actual scale, and would return the signal strength on that scale.

Here's how the function looks like:

import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import splitties.systemservices.wifiManager
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@OptIn(ExperimentalContracts::class)
inline fun WifiInfo.calculateSignalLevel(
    fallbackNumLevels: Int = 5,
    getNumLevels: (numLevels: Int) -> Unit
): Int {
    contract { callsInPlace(getNumLevels, InvocationKind.EXACTLY_ONCE) }
    val wifiManager = checkNotNull(wifiManager) { "Wi-Fi info is not supported in instant apps" }
    val actualNumLevels = when {
        Build.VERSION.SDK_INT >= 30 -> wifiManager.maxSignalLevel
        else -> fallbackNumLevels
    }
    getNumLevels(actualNumLevels)
    return when {
        Build.VERSION.SDK_INT >= 30 -> wifiManager.calculateSignalLevel(rssi)
        else -> @Suppress("deprecation") WifiManager.calculateSignalLevel(rssi, actualNumLevels)
    }
}

The wifiManager top-level property comes from the Splitties System Services library that saves the tiny boilerplate otherwise needed. After all, why accept unneeded boilerplate when it can even be inlined to be like hand-written?


That's it! I hope you learned something. Personally, I'm happy to finish my first blog post after almost 7 years developing Android apps full-time (overtime?), and 5 years of Kotlin 😅.

Have a great day, and see you in my next blog posts (you can click the subscribe button if you wish), on Twitter, and maybe even on Kotlin Slack!

Did you find this article valuable?

Support Louis CAD by becoming a sponsor. Any amount is appreciated!