Photo by Dan Dimmock on Unsplash

Let’s talk about Kotlin’s let extension function

Kotlin has some really cool extension functions as part of the Standard library. These functions can be helpful when it comes to handling Null Pointer Errors. Let’s focus on one of these functions,let, in order to write safer code and less boilerplate.

Dealing with optionals

So let’s start with some normal java-esque looking null check code. We check if a value is not null, and when the value exists we can use it right?

var property: Int? = 42fun someMethod() {
if (property != null) {
print(property) // Error
}
}

This looks fine for the most part, but we get an error:

Smart cast to ‘Int’ is impossible, because ‘property’ is a mutable property that could have been changed by this time

Since we’re using a mutable variable here, our value could technically be changed by another thread. Easy fix isn’t it, we know the value isn’t null so lets just force unwrap it with the !! operator…

Okay no let’s not do that, because the issue is our value could be changed or even set to null which means we’d crash and get a… you guessed it NPE. So how do we handle this? We require a locally scoped copy of our value to read like so:

var property: Int? = 42fun someMethod() {
val copy = property
if (copy != null) {
print(copy)
}
}

Finally our code compiles, but I thought Kotlin was suppose to help reduce boilerplate. All we’ve done is add more lines of code. How we can reduce this using the let function?

var property: Int? = 42fun someMethod() {
property.let {
fancyPrint(it) // error
}
}
fun fancyPrint(int: Int) {
print(int)
}

So we’ve started by calling let on our property however we’ve caught ourselves yet another error, what on earth? Well there’s a clue in this one, our fancyPrint function requires an Int value, and we have an optional Int value. Lets use the safe call operator ?. to safely call the let function.

var property: Int? = 42fun someMethod() {
property?.let {
fancyPrint(it)
}
}
fun fancyPrint(int: Int) {
print(int)
}

Here we go no error! Using the safe call operator ?. to safely unwrap our value if it exists is all we’re missing. If our value is null, the code will just be skipped right over. Brilliant.

So what does let actually do here?

Remember the first code block that had no errors, it required taking a copy of the current value to be used. This is what let does. It captures an immutable property that we can read within the provided blocks scope. This is why if we want our actual value inside of the let block, we need to safely unwrap the optional type in order to see the actual type and value wrapped inside it. Of course you can force unwrap too, but you’re only causing yourself a head ache and inevitable NPE.

Fantastic now you know everything there is to know about let!

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
  • If the value is an optional, you probably want to unwrap it first with ?. so that your T is not an optional
  • The scope of let 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! 🍺

Hey! I'm Codie. I photograph, film, design, program, all the fun things in life really. Currently work as an iOS and Android developer for PocketSmith

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store