The web content discusses the run scope function in Kotlin, explaining its two versions (with and without receiver), their use cases, and best practices for cleaner code.
Abstract
The article provides an in-depth look at the run scope function in Kotlin, detailing how it can be used both with and without a receiver. It emphasizes that run with receiver is similar to let but allows for an anonymous extension function, which can lead to cleaner code by avoiding unnecessary namespace pollution. The author argues that run is particularly useful when you want to perform a computation intimately tied to an object and produce a result, without defining a named extension function. The article also contrasts run with other scope functions like with and cautions against improper use of scope functions that can lead to less readable code. Examples are provided to illustrate proper usage, including handling nullable types and avoiding the definition of single-use extension methods.
Opinions
The author suggests that run with receiver is a powerful tool for object configuration and computation, akin to an anonymous extension function.
It is implied that run should be used when the computation is closely related to the receiver object and when an extension function would be overkill for a one-time use.
The author expresses a preference for using run over let when the context requires chaining multiple operations or when the operation feels like it should be an extension method.
The article criticizes the use of scope functions like run or let merely for the sake of chaining operations, stating that such practices can result in suboptimal code readability.
The author points out that run without a receiver is useful for converting a series of statements into an expression, but also warns that this usage should be carefully considered to avoid misuse.
The opinion is conveyed that with might often be the better choice than run when dealing with extensions defined inside other classes, suggesting that the choice of scope function should be deliberate and context-dependent.
Scope Functions: run()
Introducing run() with and without receiver, how its receiver version can be viewed as an anonymous extension function, how proper usage can clean up code and an example of improper usage.
— — — — — — — — — — — — — — —
THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.
This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.
There are in fact two different versions of run defined in the standard library:
Run With Receiver
In a sense, run with receiver is to let what apply is to also - it does the same thing as let, except it accepts a function with receiver and runs it on its own receiver.
If we blindly followed the similarity with the also-apply relationship, we would go on to say that we use run when the computation is intimately tied to the receiver and produces a result. This agrees with the documentation, which lists "object configuration and computing the result" as an example of when you should use run:
While it is true that this is completely valid code which is not possible to implement with the previous scope functions, this formulation kind of misses the point of what I feel run should be used for. Also, from this point of view, it is not at all obvious when one should use with as opposed to run, which we’ll talk more about in the article about with.
For me, the key benefit in using run is this: in a sense, the computation defined by block is an anonymous extension method — a single-use extension method without a name. Notice that we are writing exactly the same code that we would write if we implemented it as an extension, but without having to actually name it and pollute the namespace.
As an example, consider a Contract object that contains a List of FinancialTargets (which is nullable because we're interfacing with Java code). The contract is only valid if the list of targets is not empty and all the targets have their investment questionnaire's filled out.
Here’s a naive way:
Well, that’s not very pretty.
Here’s a better way:
That’s much better, but if we’re only using the validation criteria once in the entire codebase, it might feel strange to extract them to a separate extension function.
We can use run to "inline" the extension method:
Notice how well this reads. What we are saying is “Take the validation criteria and run them on the targets of the contract”, which corresponds very well to what we feel is happening. From this point of view, we can say that run lends itself well to situations where you want to say "run this calculation on that object". Using run offers us the ability to use the same syntax as we would when defining an extension method, while saving us from having to actually define one.
There are two other purely practical aspects to using run. One is that, because it is defined as an extension function, we can take advantage of ?. to only run calculations when the receiver is non-null (as seen above).
The other arises when you need to run extensions defined inside other classes, i.e. functions that have multiple receivers:
However, this can also be solved by with (discussed in the next article), and we will see that it is usually the better choice.
Run Without Receiver
The version of run without a receiver comes in handy in situations where you need to convert statements to an expression:
From a functional standpoint, you could use logger.debug("Commencing with fun").let { inputValue.haveFun() } or logger.debug("Commencing with fun").run { inputValue.haveFun() } to achieve the same result. However, by now you should be able to see that this would be exactly the kind of usage of scope functions that is just wrong.