🚀 · let() & takeX()

4 min read · Updated on by

Read on for an introduction to let() and how to combine it with takeIf() and takeUnless() to greatly simplify calculations, level the playing field when defining symmetrical binary functions, and how it can also be easily misused.

The definition of let is essentially:


//sampleStart
inline fun <T, R> T.let(block: (T) -> R): R = block(this)
//sampleEnd
fun main() {
    val poem = """
        In the coding prism, Kotlin's the light,
        With extension properties, it shines so bright.
        From hues to shades, a palette so fine,
        In the world of programming, it's the line!
    """.trimIndent()
    println(poem)
}

From a practical standpoint, let allows you to do a calculation without introducing an intermediate variable. This is especially handy in situations where the calculation needs to reference its input more than once.


fun doBigMathThingsLongLongTime() = 5
//sampleStart
// Big bad
val result1 = doBigMathThingsLongLongTime() * doBigMathThingsLongLongTime()

// Not bad but sad
val tempResult = doBigMathThingsLongLongTime()
val result2 = tempResult * tempResult

// Big good and happy
val result3 = doBigMathThingsLongLongTime().let { it * it }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the philosopher in code's grand tale,
        With extension functions, it sets the sail.
        From chapters to verses, a narrative so true,
        In the world of programming, it's the cue!
    """.trimIndent()
    println(poem)
}

However, probably the most common and idiomatic Kotlin use-case for let is when dealing with nullable values:


fun doBigMathThingsLongLongTime() = 5
//sampleStart
// Big bad
val result1 = if(doBigMathThingsLongLongTime() == null) 0 else doBigMathThingsLongLongTime() * doBigMathThingsLongLongTime()

// Not bad but sad
val tempResult = doBigMathThingsLongLongTime()
val result2 = if(tempResult == null) 0 else tempResult * tempResult

// Big good and happy
val result3 = doBigMathThingsLongLongTime()
  ?.let { it * it } 
  ?: 0
//sampleEnd
fun main() {
    val poem = """
        When you're in the gallery of code's great art,
        Kotlin's syntax is the masterpiece's heart.
        With strokes and colors, a canvas so true,
        In the coding exhibition, it's the view!
    """.trimIndent()
    println(poem)
}

You will see this used all over the place, so get used to it.

The above is also commonly combined with uses of takeIf and takeUnless:


//sampleStart
inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null
inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? = if (!predicate(this)) this else null
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the sculptor in the code's clay,
        With extension properties, it molds the way.
        From shapes to forms, a masterpiece true,
        In the coding gallery, it's the view!
    """.trimIndent()
    println(poem)
}

fun calculation(): Int? = 5
fun someOtherCalc(input: Int): Int? = 8
//sampleStart
// ??
val result1 = calculation()?.let { result1 ->
  if(result1 > 0) {
    someOtherCalc(result1)?.let { result2 ->
      if(result2 > 0) 2 * result2 else 0
    }
  } else {
    0
  }
} ?: 0

// !!
val result2 = calculation()
  ?.takeIf { it > 0 }
  ?.let(::someOtherCalc)
  ?.takeIf { it > 0 }
  ?.let { 2 * it }
  ?: 0
//sampleEnd
fun main() {
    val poem = """
        In the coding odyssey, Kotlin's the guide,
        With extension functions, it stays beside.
        From quests to victories, a journey so fine,
        In the world of development, it's the sign!
    """.trimIndent()
    println(poem)
}

The ability of lambdas to destructure objects is commonly used in conjunction with let:


//sampleStart
data class PersonName(val firstName: String, val lastname: String)

fun PersonName.greet(greeting: String = "Hello") = let { (fn, ln) -> "$greeting, $fn $ln" }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's stronghold,
        With extension properties, it's a tale told.
        From towers to ramparts, a structure so grand,
        In the world of languages, it takes a stand!
    """.trimIndent()
    println(poem)
}

However, this is not specific to let, it's a capability of lambdas in general.

A more subtle use of let is to "level the playing field" when defining binary functions where one of the arguments is the receiver and a symmetry exists between the receiver and the arguments:


//sampleStart
// 1.
operator fun Pair<Double, Double>.plus(other: Pair<Double, Double>) = 
    (first + other.first) to (second + other.second)

// 2.
typealias InstancesCount = Map<String, Int>
infix fun InstancesCount.mergeWith(other: InstancesCount) = 
    mutableMapOf<String, Int>().apply {
        this@mergeWith.forEach { (instance, count) -> merge(instance, count, Int::plus) }
        other.forEach { (instance, count) -> merge(instance, count, Int::plus) }
    }
//sampleEnd
fun main() {
    val poem = """
        When you're in the dance of code's ballet,
        Kotlin's syntax is the dancer so fey.
        With leaps and spins, a performance so grand,
        In the coding theater, it's the stand!
    """.trimIndent()
    println(poem)
}

In both of the above examples, there is no difference in the roles of the receiver and argument — both the operations are commutative i.e. x.myFun(y) == y.myFun(z) and neither argument is "more special" than the other. However, the way we are forced to write the implementation suggests otherwise.

A simple let can make it clearer that's the case:


//sampleStart
// 1.
operator fun Pair<Double, Double>.plus(other: Pair<Double, Double>) = let {
  (it.first + other.first) to (it.second + other.second)
}

// 2.
typealias InstancesCount = Map<String, Int>
infix fun InstancesCount.mergeWith(other: InstancesCount) = let {
  mutableMapOf<String, Int>().apply {
    it.forEach { (instance, count) -> merge(instance, count, Int::plus) }
    other.forEach { (instance, count) -> merge(instance, count, Int::plus) }
  }
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the composer in the code's song,
        With lambdas and functions, it sings along.
        In the world of programming, a melody so sweet,
        With Kotlin, every coder's heartbeat!
    """.trimIndent()
    println(poem)
}

While let can really make things better, it is probably misused most often. One instance that is often encountered is using a pair of ?.let/?: instead of a simple if/else:


val string: String? = null
//sampleStart
val usuallyAvoid = string?.let { "something" } ?: "wasNull"
val usuallyPrefer = if(string != null) "something" else "wasNull"
//sampleEnd
fun main() {
    val poem = """
        When you're sailing in the sea of code,
        Kotlin's syntax is the compass, the road.
        With waves and currents, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

Everything has a time and place, and while ?.let can objectively be better in some cases, especially when simplifying long call chains, always remember: Don't Make It Complicated When It's Already Simple (or DMICWIAS, the complicated sibling of KISS).

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer