Let's Code Kotlin: Scoped Functions (let, apply, also, run)

Scoped functions -- let, apply, also, and run -- can make your code more concise while providing you benefits and simplicity. They can look intimidating but don't be afraid! Dive in with me as I go over these tools.

Let's Code Kotlin: Scoped Functions (let, apply, also, run)

Kotlin offers a lot of tools to help make coding easier and simpler. At first glance, though, they can be a bit overwhelming and the exact usages of each can be confusing. I'm going to do my best to break down let, apply, also, and run.

The primary differences between these four extension functions boil down to two things: the way the context object is referenced inside of the scope (this vs it), and the return value of the lambda. That's it.

.let { }

This is one of two scope extension functions that I use regularly. I mentioned in a previous post that I typically use let for null checks, performing some extra work on the context if the object is not null. Inside the scope, the context object is referred to as it.

val dateCompleted = book.dateCompletedTimestamp?.let {
			convertToDateString(it)
		}

It can also serve as a way of mapping one value to another, as .let returns the lambda result (whatever is the last line of the lambda). For instance, in our example, convertToDateString will turn that timestamp into a String. Since it's on the last line of the lambda, it will become the value of dateCompleted. If .dateCompletedTimestamp was null, then dateCompleted would be null as well.

.apply { }

apply is another useful extension function that performs the exact opposite of let: the context object is referred to as this and the same object is returned. I typically use apply when initially creating an object. You could similarly just build your object as a data class or with a Builder, but for those that you can't or don't have control over this works beautifully.

fun createBookUpdatePayload(dateCompleted: Long, bookId: String, rating: Int): JsonObject {
	return JsonObject().apply {
		addProperty("date_completed", message)
		addProperty("book_id", bookId)
		addProperty("rating", rating)
	}
}

.also { }

This one, according to the docs, seems to have a very limited "official" usage: it seems to be used primarily for logging within a chain. I believe I've used it similarly a few times. If you've used it another way, leave a comment so we all can get a better understanding of what .also can do. .also refers to its context object as it and it returns the object it references.

fun getLastCompletedBook(): Book {
		return allBooks
						.filter { it.dateCompletedTimestamp != null }
						.also { println("Number of books read: ${it.size}") }
						.sortedByDescending { it.dateCompletedTimestamp }
						.first()
						.also { println("Last completed book: ${it.name}") }
}

.run { }

I'm going to be honest: I don't think I've used this one. Its context object is referred to as this and it returns the last line of the lambda. I also can't think of a reason to use this over something else. But there are still recommendations for it in the official documentation. It's supposed to be a cross between .let and .apply.

If I had to shove this into my fictional book app, here's how I'd do it:

fun postReadingUpdateToTwitter(bookId: Long, currentPage: Int): Completable {
	return bookService.run {
		val result = postUpdate(bookId, currentPage)
		if (result == Result.OK) {
				Completable.complete()
		} else {
				Completeable.error("Update error")
		}
	}
}

Interestingly enough, you can also use run as just a scope lambda. Meaning, without using it as an extension function.

val book = run {
	val bookName = "How to use Run"
	val bookId = 4815162342

	Book(name = bookName, dateCompletedTimestamp = null, bookId = bookId)
}

addBookToShelf(book)

My advice: skip this one. But if you've got a good example, let me know in the comments.

Bonus: with

There's also apparently another non-extension scope function named with. You instead pass the object into it, and use it like you would .apply.

with(allBooks) {
	forEach { book ->
		println("Title: ${book.title}")
	}
}

val lastCompletedBook = with(books) {
  val filtered = filter { it.dateCompletedTimestamp != null }
  val sorted = filtered.sortedByDescending { it.dateCompletedTimestamp }
  sorted.first()
}

As with the non-extension run, I might skip this. But I'm sure I can find a use for it somewhere. Though, at that point, I'd probably just make a separate function and call that.


Recap!

.let → Context object is it; returns the lambda result
.apply → Context object is this; returns the object
.run → Context object is this; returns the lambda result
.also → Context object is it; returns the object

And two non-extension functions:

run → no context object; returns the lambda result
with → pass in the context object, use with this; returns the lambda result

Whew. That's a lot but it's worth learning about them. Don't overuse them; it can be easy to throw everything into a scoped lambda, but if it hurts readability I would revert to a more verbose solution. There's plenty of official documentation on them if you need more insight. Have some fun with them though and let me know what you create with them!