Kotlin Scoping Functions apply vs. with, let, also, and run

Functional-style programming is highly advocated and supported by Kotlin’s syntax as well as a range of functions in Kotlin’s standard library. In this post we will examine five such higher-order functions: apply, with, let, also, and run.

When learning these five functions, you will need to memorize 2 things: how to use them, and when to use them. Because of their similar nature, they can seem a bit redundant at first.

What do they do?

All of these five functions basically do very similar things. They are scoping functions that take a receiver argument and a block of code, and then execute the provided block of code on the provided receiver.

Let’s first see how this works with one of those functions. The with function is basically defined as follows:

When learning these functions, it can be hard to memorize how they are defined. The following spreadsheet shows their differences in a matrix. I recommend printing it and referring to it whenever needed:

When to use apply, with, let, also, or run

We know how these five functions differ, now. But we still don’t know when to use which scoping function. They are very similar in nature, and often interchangeable.

There are several best practices and conventions for these five functions defined in the official Kotlin documentation. By learning these conventions, you will write more idiomatic code, and it will help you to faster understand the intend of other developer’s code.

Conventions for using apply

Use the apply() function if you are not accessing any functions of the receiver within your block, and also want to return the same receiver. This is most often the case when initializing a new object. The following snippet shows an example:

Conventions for using also

Use the also() function, if your block does not access its receiver parameter at all, or if it does not mutate its receiver parameter. Don’t use also() if your block needs to return a different value. For example, this is very handy when executing some side effects on an object or validating its data before assigning it to a property:

Combining Multiple Scoping Functions

The previous sections have shown how scoping functions can be used in isolation in order to improve code readability. It is often tempting to combine multiple scoping functions within the same block of code.

When scoping functions are nested, the code can get confusing fast. As a rule, try not to nest the scoping functions that bind their receiver argument to the receiver of the lambda block (apply, run, with). When nesting the other scoping functions (let, also) provide an explicit name for the lambda block’s parameter, i.e. don’t use the implicit parameter it when nesting those scoping functions.

Besides nesting, scoping functions can also be combined in a call chain. Unlike nesting there is no readability penalty when combining scoping functions in this way. Quite the contrary, the improvements in readability will be even bigger.

As a conclusion to this post, we will see some examples of combining scoping functions in call chains.

The snippet above shows a dao function for inserting a User into the database. It uses Kotlin’s expression body syntax while still separating concerns within its implementation: preparing the SQL, logging the SQL, and executing the SQL. At the end, this function returns a Boolean indicating the success of the insert.