Let run with, also apply. Filter takeIf

After some time spent with Kotlin’s stdlib, I think that naming convention is a bit messy.

We have let , run and with triplet, and also and apply pair. I would argue that run, with and apply are not really needed as cost of cognitive overload is higher that cost of typing it.. All are doing the same thing as let and also, but passing input as this rather than it. run seems to be most flexible of three, but suffers from non-intuitive name.

None of single object functions will make nullable object to behave as monad as you have to type ? anyway. Try to write a bit longer processing chain and you will see how inconvenient it is.

p.valueAsString
    ?.let(String::trim)
    ?.takeIf { it.isNotEmpty() && !it.startsWith("#") }
    ?.toInt()

where it could be

p.valueAsString
    .map (String::trim)
    .filter { it.isNotEmpty() && !it.startsWith("#") }
    .map { it.toInt() }

Then there are negated opposites of functions.

I also wanted to rant about filter vs takeIf. Filtering function for collections is called filter, while for objects it is called takeIf. OK, Kotlin treats Strings as collection of characters so it can be filtered, so filter is for collections/monads and takeIf is for objects. What should filter on String? do? IMHO it should work with optional content (whole String) rather than characters.

I am having too much functions and missing better names and overview documentation.

It would be a terrible ambiguity if T?.filter { } meant something completely different from T.filter { } for certain T types:

val collection = listOf("foo", "bar", "")
var collection2: List<String>? = collection //type could be not so explicit

collection2 = collection2.filter { it.isNotEmpty() }
collection2 = collection.filter { it.isNotEmpty() }

Could you tell at glance which filter does what?

That’s why we had to find different names for the similar concept functions like filter/takeIf: to disambiguate the operation, because they can be used on the same receiver.

2 Likes

I don’t see anything inconvenient here, those safe calls makes it very clear that we’re dealing with nulls here.

A long chain of transformations on a nullable value can be eliminated from safe calls with the run function:

p.valueAsString?.run {
    trim()
    .filterNot { it in illegalCharacters }
    // more transformations
    .takeIf  { it.isNotEmpty() && !it.startsWith("#") }
}
?.toInt()

Regarding apply / also and let / with / run functions: these are part of idiomatic Kotlin, and each has its own usages. Their primary purpose is a scope transformation, i.e. introducing it or this into the scope. Which one to choose depends first on whether you do or do not want to shadow this or it from the outer scope and then on you personal preference.

2 Likes

As Kotlin is designed with “Effective Java” in mind, nullable collections should not be used:

Item 43: Return empty arrays or collections, not nulls

This is how it works in most functional languages. As both collection and Optional are monads, they share the same set of methods. Kotlin’s fake Optional is great for Java integration, but not consistent with the monadic interface.

If I had a case like the one you described, I would call the second collection optionalCollection.

Where are those usages described? For example, I found that apply should be used for C#-style construction somewhere on the forum, but not in stdlib documentation.

run example is good, but I think that run functionality in this context is unrelated to its name.