Nested functions vs. Local extension function as lambda

Hello everyone,

I’m beginner in Kotlin development and I have 2 ways for solving a problem.
As you can see in the snippets below, the first one uses “nested function” and the second uses a “local extension function as lambda”.

According to the functions and your experience, I’d like to ask:

  • Which are the pros and cons related to each of the approaches (regarding performance, maintainability, readability, quality and good practices)?
  • Do you suggest any other approach better than the below ones?
  • Which one would you use?

Thanks in advance for your answers.

Option 1:

fun aggregateOption1(
    param1: Param,
    param2: Param,
): Int {
    fun getSomethingFromParam(param: Param): Int {
        return param.getCount()?.data
            ?: (param.data?.times(KG_TO_GRAMS)?.toInt())
            ?: 0
    }

    return getSomethingFromParam(param1) + getSomethingFromParam(param2)
}

.
.
Option 2:

fun aggregateOption2(
    param1: Param,
    param2: Param,
): Int {
    val getSomething: Param.() -> Int = {
        getCount()?.data
            ?: (data?.times(KG_TO_GRAMS)?.toInt())
            ?: 0
    }

    return param1.getSomething() + param2.getSomething()
}

Just to confuse things further, here’s a third variation, using a nested extension function (here declared with an expression body):

fun aggregateOption3(
    param1: Param,
    param2: Param,
): Int {
    fun Param.getSomething(): Int
        = getCount()?.data
            ?: (data?.times(KG_TO_GRAMS)?.toInt())
            ?: 0

    return param1.getSomething() + param2.getSomething()
}

(That should compile down to approx. the same code as the first option, but I find it easier to read. I think it makes more sense, too: the function behaves like a member of Param, so it’s logical that it should look like one.)

However, this case doesn’t need to be nested at all, as it’s not using the outer function’s parameters param1 or param2. (That’s the main reason for using nested functions; so it’s a little confusing to see one that doesn’t.)

So for simplicity, I’d consider making it a top-level function. (You can make it private if you don’t want anything else to see it.)

You could even move it into the same file where Param is defined (if you have access to that), or make it a full method of Param if that makes sense.

And a yet further possibility would be making it a property. It could be an extension property:

val Param.something: Int = …

Or it could be an actual property getter within Param, if you had access to it:

val something: Int
    get() = …

Either of those would give even cleaner syntax — but would make sense only if it behaves like reading a property. (I.e. it’s consistent, always giving the same result if Param’s state doesn’t change; fast; and reliable, not accessing disk or an external service or anything else that could fail.)

(For reference, all of these different options are likely to perform about the same, so performance shouldn’t be a consideration. There might be very slight variations — in particular, the lambda version may create a temporary object, though that’s far from certain — but the difference is almost certainly insignificant here. So pick whichever version seems simplest and clearest and best matches the program you’re writing.)

3 Likes