When to use Kotlin's standard functions

Kotlin comes with several high-level, generic standard functions that apply to any object: let(), run(), with(), apply(), and also().

If you're new to Kotlin you may be wondering when to use them. They're fairly inscrutable; the source reveals a smorgasbord of generics, lambdas, and receivers. There are plenty of articles (1, 2, 3, etc.) and cheat sheets (1, 2) about how to differentiate between them, despite their subtle differences.

Here's my contrarian advice for Kotlin beginners: Don't use them. Yet.

It's too easy to get confused with standard functions and create a mess. I speak from personal experience, having seen (and made) messes many times with them. On top of that, they are in no way necessary; they're all just shorthands for (slightly) longer pieces of code. If you are confused by them or creating messes, then they're not useful shorthhands!

In this article I'll go over a few of ways that people abuse the standard functions. Then I will talk about how you don't need them when you're starting anyways, and why you should learn other parts of Kotlin first.

Interchangeable Confusion

Due to their generic nature, it's possible to use multiple standard functions for the same purpose. This interchangeability is actually a detriment because it leads to inconsistent, confusing code.

One of the main examples for using standard functions is to operate on a non-null version of a nullable variable:

nullableVar?.let { /* in here, it is no longer null */ }

But it's actually possible to use this pattern with a bunch of other functions

// In all these lambdas, the nullable var is no longer null
nullableVar?.run { }
nullableVar?.apply { }
nullableVar?.also { }

What's the difference between all of these? Well, you'll figure it out eventually... but if you can't name off the top of your head the difference, then this code can quickly get confusing to read.

Nested Lambdas

It's tempting for those who are into functional programming to try write Kotlin without using a single imperative line of code. But doing so can quickly result in pyramid of doom lambda stacks:

value1?.let { value1NonNull ->
  value2?.let { value2NonNull ->
    value3?.let { value3NonNull -> 
      // ...Execute code...
    }
  }
}

I give you permission to use an if statement to simplify the code:

if (value1 != null && value2 != null && value3 != null) {
  // ...Execute code...
}

Personally, I find the latter easier to understand and it avoids deep nesting.

Receiver Confusion

Kotlin allows you to change the receiver inside of a lambda function. This pattern is great for creating type-safe builders, but it has the potential to create confusing code:

class MyClass {
  fun foo(bar: Bar) {
    println(toString()) // Prints MyClass.toString()
  
    bar.apply {
      println(toString()) // Prints Bar.toString()
    }
  }
}

Which toString() is being called here? It's easy to misread it!

Sometimes people are tempted to use the scoping function to reduce code verbosity. But that's not always the case. I've actually seen code like this:

with(someVar) {
  foo()
  bar()
}

But compare it to the non-receiver changing code:

someVar.foo()
someVar.bar()

The non-standard function version is fewer lines of code!

They're Just Shorthands

Here's a little secret: all these functions are just shorthands for slightly longer code.

As mentioned earlier, one trick is to use value?.let { } to execute some code if value is non-null. I think this is one of the few cases where early usage of standard functions makes sense, in fact, since it's sort of the other side of the coin of foo?.bar().

But really, it's just a shorthand for this:

val value = someNullableVar
if (value != null) {
  // Do something with non-null value
}

A simple val plus an if-statement! Yes, it's slightly more verbose, but it's really not that bad.

Let's look at an example with apply:

val x = StringBuilder().apply {
  append(var1)
  append("\n")
  append(var2)
}

And without apply:

val x = StringBuilder()
x.append(var1)
x.append("\n")
x.append(var2)

In this case, the alternative is actually fewer lines of code!

I could go on with the others, but the idea is the same: usually you're only saving yourself a couple lines of code and/or characters by using them.

Conclusion

I just don't think these functions worth the heartache when you're first learning Kotlin. There are plenty of other Kotlin language features, idioms, and best practices you're better off learning first.

To be crystal clear: I'm not proposing that these functions should be avoided forever. I use them myself now and they can be quite useful!

What I am saying is that they're not required for actually writing Kotlin, so don’t get hung up on them. Learn how the rest of the language works first, then circle back to these functions. It'll be a lot easier to understand their correct usages at that point and when they can actually improve your code.