Hero Image

From Java to Kotlin Part 5

Welcome back everyone, I hope you had a nice holiday season. As we are starting a new year, we are also going to move on to some more experienced topics. Part 1 through 4 have covered most of the basic stuff, though not everything, but enough to get a good handle on what Kotlin looks like and where it differs from Java. Now that we have that understanding and unlocked some powerful tools, let's put them to use! ​

The Scope Family

We have talked about extension functions before (see part 2) and the Kotlin standard library is full of them, the most basic and also most powerful of them belong to what I call 'the scope family' and it consists of the functions let, run, also, apply and with. What do these functions do exactly? Well, they all take an object and execute a block of code on it, but they have some minor differences. Let's go over them one by one. ​

Let

You can use let to transform one object into another. Let takes a lambda and the object you called it on is available as it (or any other name you gave it inside the lambda). The return value of the let function is whatever the lambda returns. Let is like map for a single object, for example

val s = "Hello World"
val length = s.let { it.length }

Granted, this example is a bit silly since you can just call s.length directly. But the real power of let is unlocked when you use it in combination with the elvis operator ?. which we saw in part 1. Here is an example to calculate a person's age where both the person and its birth date can be null.

val age = person?.let { it.birthDate?.until(LocalDate.now())?.years }

Now consider how verbose this looks in Java:

Integer age;
if (person == null) { 
  age = null;
} else {
  var birthDate = person.getBirthDate();
  if (birthDate == null) {
    age = null;
  } else {
    age = LocalDate.now().until(birthDate).getYears();
  }
}

Something interesting happens when you use scope functions like let: you are automatically writing in a more functional way. You start with your input object on the left and as you write your expression you eventually end up with your target object. In Java, you often have to write it the other way around. It is perfectly valid to have no return value in your lambda, since in Kotlin the default 'no return value' is still a return value called Unit:

person.let {
  println(it.name)
}

Run

Run is let's sibling. It works the same way with one minor difference: you can use the object as this inside the lambda and the this keyword can often be omitted so you get some pretty concise code:

person.run {
  println(name)
}
​
val length = s?.run { length } ?: 0

It may be enticing to use run instead of let everywhere, but less code does not automatically mean more readable code. The recommendation is to use let when applying a simple lambda and use run when you have to do some additional object configuration (like calling setters or other instance methods) before computing the result. ​

Also

If run is let's sibling, then also is its cousin. It takes the object as it, but the return value is just the object itself, not the return value of the lambda, it is used (as its name implies) to also do something extra on the object without transforming it into another object:

val person = Person(name = "John", age = 40).also {
  println(it.name)
  println(it.age)
}

Apply

Apply is to also as run is to let: it makes the object available as this and is often used to do some additional configuration on an object without changing the return value, it can be useful when some properties are not part of the constructor, for example:

val person = Person(name = "John", age = 40).apply {
  email = "[email protected]"
  phone = "+31612345678"
}

With

If the previous functions let, run, also and apply form a nice family tree of siblings and cousins then with could be considered the black sheep, it is used very differently. With takes an object and makes that object available as this inside its block, essentially changing what the keyword this refers to for the scope of that block.

with(person) {
  println(name)
  println(age)
}

It has a return value, the return value of the lambda block, so you could assign it to a variable, but it is rarely used that way. Its recommended use is when you want to group some calls for a specific object together. ​

The Family

Now that we've met the entire family, we can put them all side by side to see how they are related.

Function Reference Return value Description
let it return value of the lambda map for single element
run this return value of the lambda let using this instead of it
also it the object itself also do something extra on the object
apply this the object itself also using this instead of it
with this return value of the lambda group object calls together

​ To help you choose the right one, I've made this very helpful step by step superdeluxe scope function chooser!

  1. What does the function need to return?
    a) The object itself (go to 3)
    b) Something else (go to 2)

  2. How would you like to refer to the object inside the lambda?
    a) As it -> use let
    b) As this -> use run

  3. How would you like to refer to the object inside the lambda?
    a) As it -> use also
    b) As this -> use apply

    None of these questions helped me
    Use with

    Summary

    The scope function family contains very useful functions to help you write code in a more functional, idiomatic way. Java offers a small taste of functional programming with its streams and lambdas and with Kotlin you can have even more fun with it! It is important though not to overdo it and only use these functions where they make sense.