Frank Neblung
Frank Neblung

Reputation: 3175

How to design higher order functions

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

Answers (2)

s1m0nw1
s1m0nw1

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

Roland
Roland

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

Related Questions