Intention of Kotlin's "also apply let run with"
One of the things that puzzled me when I started with Kotlin was why are there so many similar functions which call lambda on some objects and returns a value. After many lines of code and many lines of user group discussions, I found out that they represent a small DSL for easier monadic-style coding. Explanation of this DSL and intent of each function is missing from the Kotlin documentation, so this article will hopefully shed some light on it. There is also a short style guide on GitHub.
also
With this function, you say “also do this with the object”. I often use it to add debugging to the call chains or to do some additional processing:
kittenRest
.let { KittenEntity(it.name, it.cuteness) }
.also { println(it.name) }
.also { kittenCollection += it }
.let { it.id }
also
passes object as parameter and returns the same object (not the result of the lambda!):
data class Person(var name: String, var age: Int)
val person = Person("Edmund", 42)
val result = person.also { person -> person.age = 50 }
println(person)
println(result)
Outputs:
Person(name=Edmund, age=50)
Person(name=Edmund, age=50)
Or shorter, with the default it
parameter:
val result = person.also { it.age = 50 }
Original object was mutated and returned as result.
let
let
is a non-monadic version of map
: it accepts object as parameter and returns result of the lambda. Super-useful for conversions:
val person = Person("Edmund", 42)
val result = person.let { it.age * 2 }
println(person)
println(result)
Outputs:
Person(name=Edmund, age=42)
84
apply
apply
is used for post-construction configuration. It is a function literal with receiver: object is not passed as a parameter, but rather as this. An object passed in such way is called receiver.
val parser = ParserFactory.getInstance().apply{
setIndent(true)
setLocale(Locale("hr", "HR"))
}
Here is simple text example:
val person = Person("Edmund", 42)
val result = person.apply { age = 50 }
println(person)
println(result)
Outputs:
Person(name=Edmund, age=50)
Person(name=Edmund, age=50)
Structure of this example is aligned with others, but in practice you will do something like this:
val person = Person("Edmund").apply {
age = 50
}
run
run
is another function literal with receiver. It is used with lambdas that do not return value, but rather just create some side-effects:
text?.run{
println(text)
}
It returns value of the expression, but it shouldn’t be used.
val person = Person("Edmund", 42)
val result = person.run { age * 2 }
println(person)
println(result)
Outputs:
Person(name=Edmund, age=42)
84
with
According to Kotlin idioms, with
should be used to call multiple methods on an object.
with(table) {
clear()
load("file.txt")
}
with
returns the result of the last expression, which is confusing; you should ignore it. Note that it does not work with nullable variables.
val person = Person("Edmund", 42)
val result = with(person) {
age * 2
}
println(person)
println(result)
Outputs:
Person(name=Edmund, age=42)
84
What to Use When
Here is a short overview of what each function accepts and returns:
Parameter | Same | Different |
---|---|---|
it | also | let |
this | apply | run, with |
I was not particularly happy with the decision of standard library designers to put so many similar functions in, as they represent cognitive overload when analyzing the code. However, if you strictly use them for their intended purpose, they will state your intent and make the code more readable:
- also: additional processing on an object in a call chain
- apply: post-construction configuration
- let: conversion of value
- run: execute lambda with side-effects and no result
- with: configure object created somewhere else
Be careful when using these functions to avoid potential problems. Do not use with
on nullable variables. Avoid nesting apply
, run
and with
as you will not know what is current this
. For nested also
and let
, use named parameter instead of it
for the same reason. Avoid it
in long call chains as it is not clear what it represents.
All these functions can be replaced with let
, but then information about the intent is lost. As intent is the most valuable part of this set of functions, be careful when to use which one.
Comments
Post a Comment