Continuing series of posts #kotlin-showoff about functional constructions in kotlin I want to
Essentially, elvis operator lvalue ?: rexpression is returning left value if it's not null
or executes rexpression otherwise. The crazy thing about kotlin is most of the constructions are expressions and that gives another way to express business logic.
Returning alternative value
In the simplest case we can return alternative value if left value is null
, for example
val userList = cache.get(userId) ?: fetch(userId)
Literally means: get value of cache.get(userId)
if it's not null or fetch(userId)
In Java it can be expressed as
List<User> userList = cache.get(userId);
if (userList == null) userList = fetch(userId);
Also, it's possible to chain
val userList = memoryCache.get(userId)
?: diskCache.get(userId)
?: fetch(userId)
Looks lovely and clean, isn't it?
Early return with elvis
fun displayUsers() {
val userList = cache.get(userId)
?: return
updateUiWithNewUserList(userList)
}
In this case return if userList
is null function will return immideatelly
Throw exception with elvis operator
In Java code we often need to manage an exceptional situation and allow callers to handle it themselves. Take a look on this example:
void displayUsers() {
List<User> userList = cache.get(userId);
if (userList == null)
throw new NoDataAvailableException()
. . .
}
Employing elvis operator in kotlin it looks more concise and logic is encapsulated within one expression:
fun displayUsers() {
val userList = cache.get(userId)
?: throw NoDataAvailableException()
. . .
}
Looks cleaner, isn't it?
Elvis operator with in loops
In the same way as we used early return it's possible to make elvis to work with break
and continue
, in case if you're still using loops
fun top10Score(users: List<User>): Int {
var totalScore = 0
for(i in 0 until Math.min(users.size, 10)) {
val score = users[i].gameProgress?.score
?: continue
totalScore += score
}
return totalScore
}
If either gameProgress
or score
are null
loop will continue to the next element.
By the way, there is a way expressive functional way to implement the same logic:
fun top10Score(users: List<User>): Int = users
.take(10)
.fold(0) { acc, it -> acc + (it.gameProgress?.score ?: 0) }
It works not only for nullable types
Kotlin has numerous extension functions allowing to express flow in the concise and functional manner. We'll dive into this topic in the following articles.
But here is a spoiler for you, say it's need to throw exception in validator when user didn't provide name or it was empty. From data point of view null
isn't same as empty string, empty string isn't same as a string full of spaces. But in our business logic it's practically the same since no name is provided. Here is an example of handling this situation:
val name = user.name?.takeIf(String::isNotBlank)
?: throw UserNameIsEmptyException()
takeIf
returns receiver object if predicate is true
. Even user.name
is not null
but consist of empty string takeIf
will return null
because it doesn't satisfy condition. Exception will be thrown in this case and it's guaranteed by compiler that name
is not null
Conclusion
Elvis operator in kotlin is very powerful and helps to handle conditional logic naturally within the data flow