DEV Community

Victor
Victor

Posted on

Kotlin Elvis (?:) Operator: why I don't use it

Image description

As always, TL;DR at the end.

The title of this post is a bit misleading. I do use the Elvis operator in Kotlin, it is a shame that Java doesn't have it (along with a truckload of other features). But I prefer not to use it directly in my code. I'll explain why.

Just to make sure we are on the same page, the Elvis operator is the ?: operator in Kotlin. It is used to provide a default value when a nullable value is null. For example:



val someNullableValue: String? = null
val defaultValue: String = "default"

// Not nullable anymore
val result: String = someNullableValue ?: defaultValue


Enter fullscreen mode Exit fullscreen mode

"Piping" of code

Kotlin made the decisions made on Java 8 a design principle. Java 8 brought, along with other features, the Optional and Stream classes. These classes are designed to isolate the internal context of the objects contained (nullability, quantity) from the outside world. This is called a Monad, and it is a very powerful concept, in which some languages are entirely based on, while Kotlin allows you to use it when you want to.

One of the common operations with Monads is the "piping" of code. This is when you chain a series of operations on the same object, like this:



val result = someObject
    .operation1()
    .operation2()
    .operation3()

// Or more concretely
val result: Optional<Any> = someOptional
    .map { operation1(it) }
    .flatMap { operation2ReturningOptional(it) }
    .filter { operation3ReturningBoolean(it) }


Enter fullscreen mode Exit fullscreen mode

In Kotlin, although not as direct as with Optionals, you have exactly the same concept with nullable types. You can chain operations on nullable types using ?., and thus achieving the same effect as the map, flatMap and filter functions of the Optional class.



val result = someObject
    ?.let { operation1(it) }
    ?.let { operation2(it) }
    ?.takeIf { operation3(it) }


Enter fullscreen mode Exit fullscreen mode

Each operation mantains the nullability of the object, and thus achieving the optionality that the Optional class provides. The main difference is that, even after you drop the nullability of something (with, say, the elvis operator), you can still pipe the code further, with the same let, run, apply, also functions as before. And here is where the Elvis operator fails.

Piping is even further pushed by kotlin via the expression functions. If a function is simple and has does as little logic as it can (as most functions should), you can write them as expressions:



suspend fun saveUser(userVO: UserVO) = 
    userVO
      .let(::toEntity)
      .let(userRepository::save)


Enter fullscreen mode Exit fullscreen mode

The Elvis operator

The failure of the elvis operator comes from the fact that it isn't chainable, as it like an infix function. This means that you can't use it in the middle of a chain of operations, like you can with the ?. operator. This is a problem because it breaks the flow of the code, and makes it harder to read and understand.



val result = someObject
    ?.takeIf { operation3(it) }
    ?: defaultValue
    // Can't chain further


Enter fullscreen mode Exit fullscreen mode

There's two ways to mitigate this. The first one is to use the let function, which is a bit more verbose:



val result = someObject
    ?.takeIf { operation3(it) }
    .let { it ?: defaultValue }


Enter fullscreen mode Exit fullscreen mode

And the second one is to add parentheses to the elvis operator, which is even worse, specially in long chains:



val result = (
    someObject
        ?.let { operation(it) }
        ?.takeIf { operation3(it) }
        ?: defaultValue
    )
    .let { operation4(it) }



Enter fullscreen mode Exit fullscreen mode

Depending on your linter, this could look atrocious. So how could you use it?

The solution

When I start a Kotlin project, I usually add a couple of must have extensions, some of which I don't understand why they aren't in the standard library. One of them is the orElse extension function:



inline fun <T> T?.orElse(block: () -> T): T = this ?: block()


Enter fullscreen mode Exit fullscreen mode

As it is an inline function, it doesn't add any overhead to the code, and it is very easy to use:



val result = someObject
    ?.takeIf { operation3(it) }
    .orElse { defaultValue }
    .let { operation4(it) }


Enter fullscreen mode Exit fullscreen mode

And, as it is itself a function call, you can chain it as much as you want, thus solving the problem of the Elvis operator. Again, I don't really know why this isn't in the standard library, but it is a very useful function to have.

Conclusion & TL;DR

The Elvis operator is a very useful operator in Kotlin, but it breaks the flow of the code when used in the middle of a chain of operations. To solve this, you can use the orElse extension function, which is a very simple function that provides the same functionality as the Elvis operator, but in a chainable way.



inline fun <T> T?.orElse(block: () -> T): T = this ?: block()


Enter fullscreen mode Exit fullscreen mode

Top comments (0)