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!
-
What does the function need to return?
a) The object itself (go to 3)
b) Something else (go to 2) -
How would you like to refer to the object inside the lambda?
a) Asit
-> uselet
b) Asthis
-> userun
-
How would you like to refer to the object inside the lambda?
a) Asit
-> usealso
b) Asthis
-> useapply
None of these questions helped me
Usewith
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.