Intention of Kotlin's "also apply let run with"

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

Popular Posts

Kotlin and Java EE: Part One - From Java to Kotlin

Kotlin and Java EE: Part Two - Having Fun with Plugins