Reputation: 3175
Higher order functions have a parameter of either
We are used to filter
and with
from kotlin's stdlib:
@Test
fun `filter example`() {
val filtered = listOf("foo", "bar").filter {
it.startsWith("f")
}
assertThat(filtered).containsOnly("foo")
}
@Test
fun `with example`() {
val actual = with(StringBuilder()) {
append("foo")
append("bar")
toString()
}
assertThat(actual).isEqualTo("foobar")
}
While filter
uses a function type parameter, with
uses a function type parameter with receiver. So lambdas passed to filter
use it
to access the iterable's element, while lambdas passed to with
use this
to access the StringBuilder.
My question: Is there a rule of thumb, which style to use (it vs this), when I declare my own higher order function?
In other words: Why isn't filtering designed that way?
inline fun <T> Iterable<T>.filter2(predicate: T.() -> Boolean): List<T> = filter { it.predicate() }
If it were defined that way, we'd use it like so:
@Test
fun `filter2 function type with receiver`() {
val filtered = listOf("foo", "bar").filter2 {
// note: no use of it, but this
startsWith("f")
}
assertThat(filtered).containsOnly("foo")
}
Upvotes: 3
Views: 120
Reputation: 81929
You simply don't always want to work with receivers. For example, consider your filter
worked on the elements directly, you'd have to use the this
qualifier in the comparison then:
val filtered = listOf("foo", "bar").filter2 {
this == "f"
}
That looks weird and unnatural. What does this
point to?
You changed the scope of this
to point to the receiver and if you wanted to access the "outer" this
, it would look like this:
[email protected] =="f"
Another downside is that you loose the possibility to name your parameter. Think about nested lambdas for instance. Neither it
nor this
is appropriate then. You'd have to give custom names.
You should always consider if you really want to switch into the receiver's scope. Some situations are perfect use cases, especially DSLs. For usual higher order functions, you simply don't want to have this feature.
I think it's hard to formulate a "rule" for this but as a starter you can read what JetBrains recommends on how to choose between the available scope functions (let
,run
,also
,apply
,with
):
Are you calling methods on multiple objects in the block, or passing the instance of the context object as an argument? If you are, use one of the functions that allows you to access the context object as it, not this (also or let). Use also if the receiver is not used at all in the block.
Upvotes: 3
Reputation: 23262
My rule of thumb is the following:
whenever there is the slightest chance that I may need to name my lambda parameter, I use (Type) -> Unit
.
If I am sure that I will not name it (so it is clear from the context that everything I operate on is this
) or I even want to prohibit the naming (builder?), then I use Type.() -> Unit
.
with
, apply
and run
all use the second approach... and for me that makes sense:
with(someString) {
toUpperCase() // operates on someString... already sounds like: "with some string (do) to upper case"
}
someString.run(::println) // run println with someString; actually: someString.run { println(this) } // e.g.: print this [some string]
// actually I find this a bad sample... I usually use run where the thing to be run is from the receiver...
SomeComplexObject().apply {
// apply being similar to a builder
complex1 = 3
complex2 = 4
}.run {
// fully constructed complex object
complexOperationWithoutReturnValue() // this method is part of SomeComplexObject
} // discarding everything here...
Here is a sample where I used the second approach:
fun sendMail(from : String, to : String, msgBuilder : MessageBuilder.() -> Unit)
sendMail("from", "to") {
subject("subject")
body("body")
}
With it
or a parameter (e.g. builder ->
) it just gets uglier and it doesn't really add something to the context...
Upvotes: 3