Understanding the Let function

Okay not so fast, I don’t think that really covers the function, let’s have a look at its definition:

inline fun <T, R> T.let(block: (T) -> R): R

Yea that’s a bit more complicated than I thought initially too. So what more can we do with this function?

We have a T which is the type of our property we’re calling let on. However we also have some R value that we can return.

How about we take the let function inside some real world class and see how it interacts with the rest of our code.

class MyClass {

var property: Int? = 42



fun someMethod() {

val value = property?.let {

fancyPrint(it)

"success"

}

}



fun fancyPrint(int: Int) {

print(int)

} fun fancyPrint(string: String) {

print(string)

}

}

So in the above we have a class to scope some functions to. The scope of our receiving block is this@MyClass which means we can call our class functions such as fancyPrint when needed, or any other member variables of the class. The argument to the block is it , which is our unwrapped property value. Since this is just a plain closure argument, it means we can name the captured property instead of writing it .

property?.let { someValue ->

fancyPrint(someValue)

}

But lastly you can also return a value from inside the block. Note that it can be of a different type to the argument, hence the R in the definition. In the below code if the property exists, It will print it using our fancyPrint method, and then return the string success into a new local property value .

class MyClass {

var property: Int? = 42



fun someMethod() {

val value = property?.let {

fancyPrint(it)

"success"

}

fancyPrint(value) // error

}



fun fancyPrint(int: Int) {

print(int)

} fun fancyPrint(string: String) {

print(string)

}

}

Note however that there is an error if we want to fancyPrint our value property as it’s an optional. We’ll see how to prevent this later, without having to do another let dance.

Okay so what do we know so far:

let captures the value T for thread-safe reading

captures the value for thread-safe reading If the value is an optional, you probably want to unwrap it first with ?. so that your T is not an optional

so that your is not an optional The scope of let is of the enclosing class, allowing you to access class methods and properties

is of the enclosing class, allowing you to access class methods and properties You can return a value that’s of a different type R which will be optional.

Ok so let’s process this information and see what more we could do.

What about the failure case?

let is really powerful, however it’s good practice to ensure that conditional branches such as if statements cover all cases like the else branch to minimise potential for bugs.

How about we use another extension function to provide an else like clause?

fun someMethod() {

property?.let {

fancyPrint(it)

} ?: run {

showError()

}

}

Here we use the elvis operator ?: to guarantee we run one of the conditional branches. If property exists, then we can capture and use its value, if the value is null we can ensure we show an error.

We can use this same strategy to fill in a default value to solve our error from the above section:

class MyClass {

var property: Int? = 42



fun someMethod() {

val value = property?.let {

fancyPrint(it)

"success"

} ?: "No Value"

fancyPrint(value)

}



fun fancyPrint(int: Int) {

print(int)

} fun fancyPrint(string: String) {

print(string)

}

}

Pretty cool right! But what more can we do?

Let’s get Swifty

Something I really missed from Swift, was the guard statement. Effectively this is an early return if a value isn’t present. How it works, is that you bind an optional value to a new locally scoped immutable variable, then you can use it within that scope.

func someMethod() {

guard let value = property else { return }

fancyPrint(value)

}

Well in Kotlin we can bind to a value if it exists, and we can perform an action if it’s not present too.

fun someMethod() {

val value = property?.let { it } ?: return // can be simplified to just `property ?: return` too which is

// much simpler, but if you want to log anything additional the

// let syntax is super flexible. fancyPrint(value)

}

The systems type inference can determine that the return value will always be non-optional, because we can’t access the rest of the scope if the value isn’t present.

You can barely tell what language you’re writting in, how cool is that!

This is fantastic if you’re working with a lot of data that maybe coming from an API call and require processing. You can verify that you have all the required variables, then work with them in a safe manner, avoiding broken states and failing fast if data is missing.

Where to go from here

As you’ve seen, let is really quite powerful, and there was a bit more to the function than at first sight. However it also has a few friends!

apply

run

with

also

These functions are all really similar to let however you’ll find the blocks scope of this , the argument it and the result will all be slightly different. There’s a brilliant write-up on how these other functions differ below:

Thanks for making it this far, I hope you’ll leave with some cool new ideas to explore on your daily coding journey. I know delving a little bit deeper into this helped me understand it a lot more than I did previously.

Be sure to send me some claps 👏 if you enjoyed the read or learned something.

Cheers! 🍺