Choosing the right scope function in Kotlin
For newcomers to Kotlin, the scope functions can be hard to wrap your head around. With similar sounding names (let
, run
, apply
, also
, with
), choosing the right one can be difficult. What are the differences between them? When should we use them?
The scope functions all serve a similar purpose: to execute code on an object. They’re mostly different in two ways:
- The return value
- Lambda receiver vs. lambda argument
What is a lambda receiver? How is it different from a lambda argument?
Lambda Receiver vs. Lambda Argument
Most likely, you’re already familiar with lambda arguments. They’re simply the argument of a lambda function.
But what is a lambda receiver? It’s an object available in a lambda function, as if the code were executing in a normal class. So the code we write can have a very clean API:
But how would you write a function like buildString
?
In the above example, action
is a lambda function, with the type of an extension function.
A lambda with a receiver is a lambda function with the same type as an extension function.
The type being extended, which is available in the lambda as the context object this
, is called the lambda receiver.
The Scope Functions
As we mentioned earlier, scope functions differ in two ways—the return type and how they access the object they’re running code on.
- The return type can be either the object itself, or the result of the lambda function.
- The object they’re accessing can be available as a lambda receiver (
this
) or a lambda argument (it
).
What makes it hard is knowing which one to choose in a certain situation.
I need to…
Use a lambda on a non-null object: let
When dealing with a nullable type, we have a few options.
How would we go about that?
Smart casting and using let
are solid options that take good advantage of Kotlin’s type system.
Doing a hard null-check, however, can result in a null pointer exception. It doesn’t handle the nullable type well—it just gives it an ultimatum.
Avoid polluting the outer scope: let
Configure an object: apply
Configure an object and compute the result: run
A lot of the times we can get away with putting all of those fields in a constructor, but the run
function is still a good option.
Use statements where an expression is required: run
(as a non-extension)
This is similar to our raffle example, where the goal is keeping a minimalist outer scope, using the let
function. These two scope functions are very similar, the difference being that run
takes a lambda receiver (this
), and let
takes a lambda argument (it
).
Add an additional effect: also
also
is the best-named scope function. Think “also, please log this variable”.
Group function calls on an object: with
When should you not use scope functions?
Frankly, the scope functions require time to understand, especially for people who are tackling Kotlin for the first time.
Therefore, the main downside is making your code less approachable.
Don’t use them just for the sake of using them, only do so in cases where it actually adds value and makes your code more readable.
Wow! You read the whole thing. People who make it this far sometimes
want to receive emails when I post something new.
I also have an RSS feed.