How to Write a Function When Run Again Adds to Itself
Coping with Kotlin's Scope Functions
Functions in Kotlin are very important and it's much fun()
to employ them. 1 special drove of relevant functions can be described as "scope functions" and they are part of the Kotlin standard library: permit
, run
, besides
, use
and with
.
You lot probably already heard most them and it'due south also likely that y'all even used some of them however. Most people tend to have bug distinguishing all those functions, which is non very remarkable in view of the fact that their names may be a bit disruptive. This mail service intends to demonstrate the differences between the available scope functions and also wants to discuss relevant use cases. Finally, an example volition bear witness how to use scope functions and how they assist to structure Kotlin code in a more than idiomatic way.
Disclaimer: The topic of scope functions is nether consideration in various StackOverflow posts very frequently, which I will occasionally refer to throughout this commodity.
The Importance of Functions
In Kotlin, functions are as important as integers or strings. Functions can exist on the same level equally classes, may be assigned to variables and can besides be passed to/returned from other functions. Kotlin makes functions "kickoff-class citizens" of the language, which Wikipedia describes as follows:
A starting time-grade citizen [...] is an entity which supports all the operations generally available to other entities. These operations typically include being passed every bit an argument, returned from a function, modified, and assigned to a variable.
As already said, functions are as powerful and significant every bit any other type, e.g. Int
. In addition to that, functions may announced equally "higher-order functions", which in turn is described every bit the following on Wikipedia:
In mathematics and computer science, a higher-order function (too functional, functional form or functor) is a function that does at to the lowest degree one of the post-obit:
- takes ane or more functions as arguments (i.e., procedural parameters),
- returns a role as its issue.
The boldly printed bullet indicate is the more important ane for the present commodity since scope functions also deed as higher-society functions that accept other functions every bit their argument. Before we swoop into this farther, let's observe a simple example of college-lodge functions.
Higher-Social club Role in Activeness
A simple college-order role that'south commonly known in Kotlin is called repeat
and it's defined in the standard library:
inline fun repeat(times: Int, activity: (Int) -> Unit)
Equally you can see, repeat
takes two arguments: An ordinary integer times
and as well another function of type (Int) -> Unit
. According to the previously depicted definition, repeat
is a higher-gild function since information technology "takes one or more functions every bit arguments". In its implementation, the function simply invokes activity
as frequently as times
indicates. Allow'due south come across how echo
can be chosen from a client'due south point of view:
repeat(3) { rep -> println("current repetition: $rep") }
In Kotlin, lambdas can be lifted out of the parentheses of a function call if they human action as the concluding argument to the function.
Note that if a function takes another function as the last parameter, the lambda expression statement can be passed exterior the parenthesized argument list.
The official documentation is very clear about all lambda features and I highly recommend to study it.
In the shown snippet, a regular lambda, which only prints the electric current repetition to the console, is passed to repeat
. That's how higher-club function calls wait like.
Function Literal with Receiver
Kotlin promotes yet another very of import concept that makes functions even more than powerful. If you've always seen internal domain specific languages (DSL) in action, you might take wondered how they are implemented. The most relevant concept to understand is called function literal with receiver (also lambda with receiver). Since this feature is likewise vital for scope functions, it will exist discussed next.
Function literals with receiver are ofttimes used in combination with college-guild functions. As shown earlier, functions can be made parameters of other functions, which happens by defining parameters with the function blazon syntax (In) -> Out
. Now imagine that these function types can even be boosted by adding a receiver: Receiver.(In) -> Out
. Such types are called part literal with receiver and are all-time understood if visualized as "temporary extension functions". Take the following example:
inline fun createString(block: StringBuilder.() -> Unit): String { val sb = StringBuilder() sb.block() render sb.toString() }
The part createString
can be called a higher-guild role every bit it takes another part block
as its argument. This argument is defined as a function literal with receiver type. Now, let'south think of it equally an extension part defined for StringBuilder
that will be passed to the createString
role. Clients will manus on arbitrary functions with the signature () -> Unit
, which will be callable on instances of StringBuilder
. That's also shown in the implementation: An instance of StringBuilder
is existence created and cake
gets invoked on it. Somewhen, the method transforms StringBuilder to an ordinary
Cord` and to the caller.
What does that hateful for the client of such a method? How tin you create and pass office literals with receiver to other functions? Since the receiver is defined as StringBuilder
, a client volition be able to laissez passer lambdas to createString
that brand use of that receiver. The receiver is exposed as this
inside the lambda, which means that clients can access visible members (properties, functions etc.) without additional qualifiers:
val southward = createString { //here we're in the context of a StringBuilder
append(4) suspend("hello") }
The example shows that append
, a function defined for the receiver StringBuilder
, is being invoked without whatsoever qualifiers (e.g. information technology
). The aforementioned is possible in the definition of extension functions, which is why I used it as an analogy earlier. The client defines a temporary extension office which gets invoked on the respective receiver within createString
afterward. For another description of the concept, delight consult the associated documentation. I also tried to respond a related StackOverflow question a while ago.
Scope Functions
Scope functions brand use of the concepts described above. They are defined as higher-society functions, i.due east. they take some other function as their statement. These arguments may even appear as function literals with receiver in certain cases. Scope functions accept an arbitrary object, the context object, and bring it to another scope. In that telescopic, the context object is either accessible equally it
(or custom name) or this
, depending on the type of function. In the following, the functions let
, run
, also
, apply
and with
will be introduced and explained.
[let
]
Documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html
public inline fun <T, R> T.let(block: (T) -> R): R
I of the most famous scope functions is certainly let
. It'southward inspired by functional programming languages like Haskell and is used quite often in the Kotlin language, too. Let's inspect its signature:
- Defined equally an extension on
T
, the receiver/context object - Generic type
R
defines the function's return value - Result
R
ofblock
will be the result oflet
itself, i.due east. information technology can be an arbitrary value -
block
statement with regular function type(T) -> R
- Receiver
T
is passed as argument toblock
Utilize Cases
a. Idiomatic replacement for if (object != goose egg)
blocks
As you tin read in the Kotlin Idioms section, allow
is supposed to exist used to execute blocks if a certain object is non null
.
val len = text?.let { println("get length of $information technology") it.length } ?: 0
The nullable text
variable is brought into a new scope past let
if it isn't null
. Its value then gets mapped to its length
. Otherwise, the zippo
value is mapped to a default length 0
with the assistance of the Elvis operator. Every bit you can come across, the context object text
gets exposed every bit it
within let
, which is the default implicit name for single parameters of a lambda.
b. Map nullable value if non cipher
The allow
role is also often used for transformations, especially in combination with nullable types over again, which is besides defined as an idiom.
val mapped = value?.allow { transform(information technology) } ?: defaultValue
c. Confine scope of variable/ciphering
If a sure variable or ciphering is supposed to be bachelor only in a bars scope and should not pollute the outer scope, let
again tin can be helpful:
val transform = "stringConfinedToLetScope".allow { println("variable tin be accessed in permit: $it") "${it.length}$it" } //cannot access original string from here }
The shown string "stringConfinedToLetScope"
is made the context object of let
, which uses the value for some simple transformation that is returned as the upshot of let
. The outer telescopic only uses the transformed value and does not admission the temporarily needed string. In that location's no variable polluting the outer telescopic due to confining it to the relevant scope.
[run
]
Documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run.html
inline fun <T, R> T.run(block: T.() -> R): R
Equally an culling to let
, the run
function makes employ of a part literal with receiver as used for the block
parameter. Let's inspect its signature:
- Defined as an extension on
T
, the receiver/context object - Generic type
R
defines the function's return value - Result
R
ofcake
will be the event ofrun
itself, i.e. information technology can be an arbitrary value -
block
argument divers as function literal with receiverT.() -> R
The run
function is similar permit
except how cake
is defined.
Apply Cases
run
can basically serve the same use cases as let
, whereas the receiver T
is exposed every bit this
within the lambda argument:
a. Idiomatic replacement for if (object != null)
blocks
val len = text?.run { println("become length of $this") length //this
tin can be omitted } ?: 0
b. Transformation
It'southward also skilful to use run
for transformations. The following shows an example that is even more than readable than with permit
since it accesses the context object'due south functions without qualifiers:
import java.util.Calendar val appointment: Int = Agenda.getInstance().run { set(Calendar.Year, 2030) go(Calendar.DAY_OF_YEAR) //return value of run }
[also
]
Documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/as well.html
inline fun T.as well(block: (T) -> Unit): T
The besides
role is the scope function that got lastly added to the Kotlin language, which happened in version 1.1. Allow's audit its signature:
- Defined as an extension on
T
, the receiver/context object - Returns the receiver object
T
-
cake
statement with regular function blazon(T) -> Unit of measurement
- Receiver
T
is passed as argument toblock
also
looks similar let
, except that it returns the receiver T
as its consequence.
Use Cases
a. Receiver not used inside the block
It might be desired to practice some tasks related to the context object merely without actually using it inside the lambda statement. An example can be logging. Equally described in the official Kotlin coding conventions, using too
is the recommended way to solve scenarios like the one shown side by side:
val num = 1234.also { log.debug("the function did its job!") }
In this case, the lawmaking almost reads like a normal sentence: Assign something to the variable and also log to the console.
b. Initializing an object
Another very common scenario that tin be solved with also
is the initialization of objects. As opposed to the two previously introduced telescopic functions, let
and run
, besides
returns the receiver object after the block
execution. This fact tin can exist very handy:
val bar: Bar = Bar().also { information technology.foo = "another value" }
As shown, a Bar
instance is created and also
is utilized in order to directly initialize 1 of the instance's properties. Since also
returns the receiver object itself, the expression can directly be assigned to a variable of type Bar
.
c. Assignment of calculated values to fields
The fact that also
returns the receiver object after its execution can also be useful to assign calculated values to fields, as shown here:
fun getThatBaz() = calculateBaz().as well { baz = it }
A value is being calculated, which is assigned to a field with the aid of also
. Since also
returns that calculated value, it tin can fifty-fifty be made the direct inline result of the surrounding part.
[use
]
Documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/use.html
inline fun T.apply(block: T.() -> Unit): T
The apply
function is another telescopic function that was added because the community asked for it. Its main utilise case is the initialization of objects, like to what too
does. The deviation volition be shown next. Let's inspect its signature first:
- Defined as an extension on
T
, the receiver/context object - Returns the receiver object
T
-
cake
argument defined as part literal with receiverT.() -> R
The relation between apply
and likewise
is the same as betwixt let
and run
: Regular lambda vs. Function literal with receiver parameter:
Relation (apply
,also
) == Relation (run
,let
)
Employ Cases
a. Initializing an object
The ultimate use case for utilize
is object initialization. The customs actually asked for this part in relatively late phase of the linguistic communication. You tin can find the respective feature request hither.
val bar: Bar = Bar().apply { foo1 = Colour.RED foo2 = "Foo" }
Although also
was already shown equally a tool for solving these scenarios, it's obvious that apply
has a large advantage: There's no need to use "it
" as a qualifier since the context object, the Bar
case in this example, is exposed as this
. The divergence got answered in this StackOverflow mail service.
b. Architect-style usage of methods that return Unit
Equally described in the Kotlin Idioms department, apply
can be used for wrapping methods that would normally result in Unit
responses.
data grade FooBar(var a: Int = 0, var b: Cord? = cypher) { fun first(aArg: Int): FooBar = use { a = aArg } fun 2nd(bArg: String): FooBar = utilize { b = bArg } } fun main(args: Array<String>) { val bar = FooBar().offset(10).second("foobarValue") println(bar) }
In the example, apply
is used to wrap simple property assignments that would usually simply issue in Unit
. Since the course wants to expose a builder-style API to the customer, this approach is very useful as the setter-like methods return the surrounding object itself.
[with
]
Documentation: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/with.html
inline fun <T, R> with(receiver: T, block: T.() -> R): R
The with
part is the final scope part that will exist discussed hither. It's a very mutual feature in many fifty-fifty older languages like Visual Basic and Delphi.
Information technology varies from the other four functions in that it is non defined every bit an extension function. Allow's inspect its signature:
- Defined as an contained function that takes a receiver/context object
T
as its showtime argument - Result
R
ofblock
volition be the result ofwith
itself, i.e. information technology can be an capricious value -
block
argument defined as part literal with receiverT.() -> R
This office aligns with let
and run
in regards to its return value R
. Information technology'southward oftentimes said to exist similar to employ
; the difference got described here. Another simple clarification of with
can be establish here (both StackOverflow).
Apply Cases
a. Working with an object in a confined scope
Likewise defined equally an idiom, with
is supposed to be used when an object is but needed in a certain confined telescopic.
val south: Cord = with(StringBuilder("init")) { append("some").append("thing") println("current value: $this") toString() }
The StringBuilder
passed to with
is only acting every bit an intermediate instance that helps in creating the more relevant Cord
that gets created in with
. It'due south obvious that with
is utilized for wrapping the calls to StringBuilder
without exposing the example itself to the outer telescopic.
b. Using member extensions of a grade
Extension functions are usually defined on bundle level so that they can be imported and accessed from anywhere else with ease. It'southward also possible to ascertain these on form or object level, which is and then called a "member extension function". These kinds of extension functions can easily be used inside that class only not from exterior. In order to make them accessible from anywhere outside the enclosing class, that course has to be brought "into scope". The with
office is very useful here:
object Foo { fun ClosedRange<Int>.random() = Random().nextInt(endInclusive - showtime) + first } // random() can just be used in context of Foo
with(Foo) { val rnd = (0..10).random() println(rnd) }
The shown object Foo
defines a sweet member extension part random()
, which can be used only in the scope of that object. With the help of with
, this can easily be achieved. Annotation that this strategy is especially recommendable if particular extension functions are to be grouped meaningfully.
Comparison and Overview
After the five different scope functions have been discussed, it's necessary to see them all next to each other:
//return receiver T fun T.also(block: (T) -> Unit of measurement): T //T exposed as it fun T.use(block: T.() -> Unit): T //T exposed every bit this //return arbitrary value R fun <T, R> T.let(block: (T) -> R): R //T exposed as it fun <T, R> T.run(block: T.() -> R): R //T exposed every bit this //return arbitrary value R, not an extension function fun <T, R> with(receiver: T, cake: T.() -> R): R //T exposed equally this
The telescopic functions also
and utilise
both render the receiver object afterward their execution. In apply
, the cake parameter is defined as a function literal with receiver and T
gets exposed as this
, whereas in too
, it'southward a regular function type and T
gets exposed as it
.
The scope functions let
and run
on the other hand both return an arbitrary effect R
, i.e. the issue of the block itself. Again, run
works with a function literal with receiver, whereas let
uses the simple function blazon.
Last only not to the lowest degree, with
is kind of a misfit amongst the scope functions since information technology'southward not defined every bit an extension on T
. It defines two parameters, one of which represents the receiver object of this scope function. Aforementioned as employ
and run
, with
works with function literal with receiver.
returns receiver object | returns arbitrary event | |
---|---|---|
exposed as it | besides | let |
exposed as this | utilize | run & with i |
1Non an extension.
IDE Back up
As of version i.2.thirty, the IntelliJ Thought Kotlin plugin offers intentions that can convert between let
and run
and likewise between also
and utilize
calls. Read more about it here.
Example: Requesting a REST API
In this section, I'm going to show an example that applies the previously discussed scope functions on a pretty bones utilise case: Calling an HTTP REST endpoint. The goal is to provide functionality for requesting information about contributors of the jetbrains/kotlin GitHub project. Therefore we ascertain the appropriate GitHub endpoint and a simplified representation of a Contributor
that is annotated for Jackson:
const val ENDPOINT = "https://api.github.com/repos/jetbrains/kotlin/contributors" @JsonIgnoreProperties(ignoreUnknown = true) data course Contributor( val login: String, val contributions: Int )
The following shows the code that provides the desired functionality in its initial form:
object GitHubApiCaller { private val client = OkHttpClient() private var cachedLeadResults = mutableMapOf<Cord, Correspondent>() private val mapper = jacksonObjectMapper() @Synchronized fun getKotlinContributor(proper noun: String): Contributor { val cachedLeadResult = cachedLeadResults[name] if (cachedLeadResult != null) { LOG.debug("return cached: $cachedLeadResult") return cachedLeadResult } val asking = Request.Builder().url(ENDPOINT).build() val response = customer.newCall(request).execute() val responseAsString = response.use { val responseBytes = it.trunk()?.source()?.readByteArray() if (responseBytes != null) { Cord(responseBytes) } else throw IllegalStateException("No response from server!") } LOG.debug("response from git api: $responseAsString\north") val contributors = mapper.readValue(responseAsString) val friction match = contributors.first { it.login == name } this.cachedLeadResults[name] = match LOG.debug("establish kotlin contributor: $match") return match } }
The depicted snippet shows a singleton object GitHubApiCaller
with an OkHttpClient
(OkHttp), a Jackson mapper and a simple Map
that'south used for caching results. The code of getKotlinContributor
can be decomposed into the following sub-tasks:
- When the result is already cached, render it immediately and skip the rest
- Create a request object using the
ENDPOINT
- Get the response by executing the asking on the
client
- Extract the JSON data from the response object (Error handling omitted)
- De-serialize the JSON to an
Array
- Filter for the contributor that is searched for
- Cache the consequence and return it to the client
In my stance, this code is very comprehensive and everybody is able to make use of information technology. Withal, it tin exist taken as a good footing for a little refactoring.
Reviewing the Code
Allow's at present effort to find some appropriate employ cases for telescopic functions in the previously shown function.
Refactoring No. 1
The start thing that nosotros tin can amend is the if
block in the very beginning:
val cachedLeadResult = cachedLeadResults[proper name] if (cachedLeadResult != null) { println("return cached: $cachedLeadResult") return cachedLeadResult }
As shown before, the let
function is normally used for resolving these kinds of if
blocks. Applied to the concrete example, nosotros get the following:
render cachedLeadResults[name]?.let { LOG.debug("return cached: $it") information technology }
The problem here is that permit
is defined with a generic return type R
so that the it
needs to be written at the cease in order to brand information technology the render value of the expression. Another obvious insufficiency is the missing else
statement. The first problem tin be addressed pretty easily. Nosotros just demand to use a scope function that returns its receiver, i.e. the cached upshot, directly from the block. Additionally, information technology should notwithstanding betrayal the receiver as information technology
, which makes likewise
the best suitable candidate:
@Synchronized fun getKotlinContributor(proper noun: Cord): Contributor { return cachedLeadResults[name]?.also { LOG.debug("return cached: $it") } ?: requestContributor(proper noun) }
The Elvis operator, shown before, is very often used for handling the else
case, i.e. when the receiver is null
. In order to brand the code more readable, a individual function requestContributor
now handles the cache miss.
That'south it, the if
block was replaced with an easy also
invocation. A more than idiomatic solution.
Refactoring No. 2
The next portion that is worth reconsidering contains an unnecessary local variable that represents the request
object:
val request = Request.Builder().url(ENDPOINT).build() val response = customer.newCall(asking).execute()
It'south literally merely used for getting a response object from the client
and could therefore simply be inlined. Alternatively, the shown actions can be idea of as a basic transformation, which we learned to limited with the permit
function:
val response = Request.Builder().url(ENDPOINT).build().allow { customer.newCall(it).execute() }
The request has been made the context object of permit
and directly gets executed in a straightforward transformation. It makes the local request
variable obsolete without affecting readability negatively.
Refactoring No. 3
The post-obit snippet points at another if (obj != aught)
block, which in this case tin actually be solved with permit
:
// Before Refactoring val responseAsString = response.employ { val responseBytes = it.body()?.source()?.readByteArray() if (responseBytes != null) { String(responseBytes) } else throw IllegalStateException("No response from server!") } // With Scope Function val responseAsString = response.utilise { information technology.body()?.source()?.readByteArray()?.let { String(it) } ?: throw IllegalStateException("No response from server!") }
Once more, the Elvis operator handles the zip
scenario very nicely.
Refactoring No. 4
Moving on to the next ii statements of the code, we observe that the responseAsString
variable is existence logged and finally used for the Jackson de-serialization. Let
'south group them together:
// Before Refactoring LOG.debug("response from git api: $responseAsString\north") val contributors = mapper.readValue(responseAsString) // With Telescopic Function val contributors = responseAsString.let { LOG.debug("response from git api: $it\n") mapper.readValue(it) }
Refactoring No. v
Subsequently the first few refactorings, the state of affairs looks as follows: Nosotros have a response
, a responseAsString
and a contributors
variable and still demand to filter the Correspondent
s for the desired entry. Basically, the whole requesting and response handling is not relevant for the final step of filtering and caching. We can smoothly group these calls and confine them to their ain telescopic. Since these actions happen with the help of the OkHttpClient
, it makes sense to make the client
the context object of that scope:
private fun requestContributor(proper noun: Cord): Contributor { val contributors = with(customer) { val response = Request.Builder().url(ENDPOINT).build().permit { newCall(it).execute() } val responseAsString = response.utilize { it.trunk()?.source()?.readByteArray()?.let { String(it) } ?: throw IllegalStateException("No response from server!") } responseAsString.permit { LOG.debug("response from git api: $it\northward") mapper.readValue(information technology) } } //... }
At that place isn't whatever new code here, the previous edits have simply be wrapped in a phone call of with
and are therefore not visible to the surrounding scope (function requestContributors
) anymore. It made sense to utilise with
in this case since it exposes client
as this
and the newCall
invocation can therefore omit its qualifier. As described earlier, with
can have an arbitrary result R
. In this instance, the terminal argument within the lambda, the issue of the last allow
telephone call, becomes that R
.
Refactoring No. 6
Now a single variable contributors
is available in the outer scope and we can apply the filtering:
return contributors.start { it.login == name }.also { cachedLeadResults[name] = it LOG.debug("establish kotlin contributor: $it") }
The previous version of the above code consisted of iv independent statements that are at present grouped in a elementary also
call with the filtered Correspondent
every bit its receiver. That receiver is put in the enshroud and also logged for debugging purposes. Of form, since too
returns its receiver directly, the whole argument can be made the return of the function.
The entire role looks like this:
private fun requestContributor(name: String): Contributor { val contributors = with(client) { val response = Asking.Builder().url(ENDPOINT).build().let { newCall(it).execute() } val responseAsString = response.use { it.torso()?.source()?.readByteArray()?.let { Cord(it) } ?: throw IllegalStateException("No response from server!") } responseAsString.let { LOG.debug("response from git api: $it\n") mapper.readValue(information technology) } } render contributors.get-go { information technology.login == name }.also { cachedLeadResults[proper name] = it LOG.debug("found kotlin contributor: $it") } }
In my opinion, the code looks very well structured and yet readable. Yet, I don't desire to encourage the readers to apply scope functions in every state of affairs subsequently reading this article. Information technology's very important to know that this set of functions is so powerful that they could fifty-fifty be used to chain an unlimited amount of expressions and brand them a unmarried expression. You lot don't want to do that because it messes upwardly the code very rapidly. Endeavour to find a balance hither and don't employ scope functions everywhere.
Conclusion
In this commodity, I discussed the powerful set of scope functions of the Kotlin standard library. Many situations tin exist solved in a very idiomatic way with the help of these functions and it's vital to accept a rough idea of the differences between them. Try to memorize that in that location are scope functions that can render capricious values (let
, run
, with
) and those that return the receiver itself (use
, as well
). Then there are functions, which expose their receiver every bit information technology
(let
, besides
) and others, which expose their receiver equally this
(run
, utilize
, with
). The terminal example demonstrated how hands scope functions may be used for refactoring appropriate code sections according to the earlier learned concepts. You shouldn't get the impression that every unmarried opportunity should actually exist embraced; it'southward still necessary to reason most the application of scope functions. Also, you shouldn't try to use all of the shown functions at any price since most of them can exist used interchangeably sometimes. Try to detect your own favorites 🙂
You can find the shown code examples in this GitHub Repo.
Feel free to contact me and follow on Twitter. Also, check out my Getting Started With Kotlin cheat canvass here.
If you lot want to read more than about Kotlin's beautiful features I highly recommend the volume Kotlin in Activeness and my other articles to y'all.
Simon is a software engineer with 9+ years of feel developing software on multiple platforms including the JVM and Serverless environments. He currently builds scalable distributed services for a decision automation SaaS platform. Simon is a cocky-appointed Kotlin enthusiast.
Source: https://kotlinexpertise.com/coping-with-kotlins-scope-functions/