For fun, I was looking to port a pile of Java code I’d written with Optionals over to Kotlin and I ran into an interesting issue that I can illustrate with this function from the Kotlin stdlib:
/**
* Returns the first element, or `null` if the sequence is empty.
*
* The operation is _terminal_.
*/
public fun <T> Sequence<T>.firstOrNull(): T? {
val iterator = iterator()
if (!iterator.hasNext())
return null
return iterator.next()
}
Consider if we’re dealing with a sequence of a non-nullable type, like say Sequence<String>
. At this point, it’s pretty clear what happens. If the sequence is empty, you get null
, otherwise you get the String
you’re looking for. But what happens if the concrete type is Sequence<String?>
, so nulls can exist in the sequence? Now you’ve got a more interesting problem, because you will get null
in two conditions: if the head of the list is null
, or if the list has nothing in it.
In Java8, you might use Optional<String>
for the return type, but Optional.of(null)
throws an exception. It requires its input to be non-null. Because of this, oddly enough, the standard idiom in Kotlin is somewhat more ambiguous than the verbose way Java8 wants to make us solve the same problem.
Why does any of this matter? If you’re writing another generic function over Sequence<T>
and you just assume that calling firstOrNull()
will only return null
when the list is empty, your code will be just fine for non-null sequences, but will have a hidden logic bug because you didn’t think about nulls.
Example code to demonstrate:
private fun <T> Sequence<T>.status(): String = firstOrNull()?.toString() ?: "nothing there!"
private fun <T: Any> Sequence<T>.status2(): String = firstOrNull()?.toString() ?: "nothing there!"
@Test
fun testSequenceNullability() {
val emptyList: Sequence<String> = sequenceOf()
val nonNullList: Sequence<String> = sequenceOf("Alice", "Bob", "Charlie")
val nullsIncluded: Sequence<String?> = sequenceOf(null, "Alice", "Bob")
assertEquals("nothing there!", emptyList.status())
assertEquals("Alice", nonNullList.status())
assertEquals("nothing there!", nullsIncluded.status())
assertEquals("nothing there!", nullsIncluded.status2()) // compiler error
}
Here, status()
is the “obvious” implementation, while status2()
refuses to run on sequences with nulls in them, which ensures that a return value of null
from status2()
always implies that the sequence has no entries. No more ambiguity.
Food for thought: should Kotlin stdlib functions like firstOrNull()
that return nullable values be written more like my status2()
example? Or, does Kotlin need an Option/Maybe/Result type of some sort, such that a function like firstOrNull()
has a way of distinguishing these cases in its output?