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