Home > Back-end >  What is the advantage of using extension functions as function parameters?
What is the advantage of using extension functions as function parameters?

Time:12-12

I had a closer look at Kotlin's scoping functions lately and I saw that sometimes they expect an extension function as parameter. Just take also and apply as an example:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

From a consumer's point of view it seems like there is not much of a difference between the two, I can call them pretty much in the same way:

fun main() {
    val logger = Logger.getLogger("Test")
    logger.info("A").apply {
        logger.info("B")
    }.also {
        logger.info("C")
    }
}

So I was wondering what am I missing here? Is there a pattern or best practice in Kotlin when to use extension functions in higher-order functions and what exactly are the benefits?

CodePudding user response:

From a consumer's point of view it seems like there is not much of a difference between the two, I can call them pretty much in the same way

This is because you're not taking advantage of the receiver or parameter here. You're using your existing variable logger instead of whatever receiver apply provides.

One of the main points of scope functions is to use them on expressions when you don't have a variable yet:

Logger.getLogger("Test").apply {
    info("A") // the logger is provided as `this` here
}

Logger.getLogger("Test2").also { logger ->
    logger.info("A") // the logger is provided as parameter here
}

Logger.getLogger("Test2").also {
    it.info("A") // using the implicit `it` parameter
}

As you probably noticed in the examples above, higher-order functions that take a lambda with receiver will provide the value differently to the caller of the function. Within the block, the parameter will be available as this in case of a receiver, and as a parameter otherwise.

CodePudding user response:

In the case of Logger.info, they are indeed not much different. info returns Unit, so T is Unit, and Unit is a rather bad example.

Consider a better example:

val person = Person().apply {
    firstName = "John"
    lastName = "Smith"
    age = 30
}

// vs

val person = Person().also {
    it.firstName = "John"
    it.lastName = "Smith"
    it.age = 30
}

Both of these do the same thing, namely the following, but in one single statement:

val person = Person()
person.firstName = "John"
person.lastName = "Smith"
person.age = 30

But because the lambda of apply is an "extension function", or in other words, has Person as the receiver, you do not need any additional qualification (like it. or person. etc) to refer to members of Person. You can refer to them simply by their simple names, e.g. just "firstName".

This is why apply is often used to set up objects' properties, or to apply changes to an object, because it is convenient to not have to write it. many time.

On the other hand, the lambda parameter of also has Person as a parameter, so to refer to its members, you need it.xxx, it.yyy etc. it refers to the lambda's sole parameter.

This makes also suitable for doing an additional action on the result of an expression, because it is natural to say ", and also do this on it".

  • Related