minOrNull returns neither min, nor null when run on list of empty string


I’m mystified by the result of these the following snippet which sums up an issue I stumbled upon in my program :

    val s= "".split(" ")
    val x=s.minByOrNull{it.toIntOrNull()?:99}?:42

split returns a list of one (empty) string.
That string is neither processed by toIntOrNull, nor triggers the null output of minByOrNull.
Then minByOrNull seems to be returning an empty string as well.

I’d be very thankful if someone could explain to me what’s happening there !


First, just to make sure we’re on the same page, let’s see what minByOrNull does.
As stated in docs:

Returns the first element yielding the smallest value of the given function or null if there are no elements

So the returned value is an element of the original list, not the result of applying the selector function:

fun main() {
    val strings = listOf("medium string", "this is a long sentence", "short")
    val shortest = strings.minByOrNull { it.length }
    println(shortest) // "short", not 5

So it makes sense, that the returned value in your case is an empty string, because it is an element of the original list.

Now, let’s see why the lambda you passed is not executed.
We can better illustrate it with following example:

fun main() {
    val strings = listOf("") // try - if you add another element to the list, selector will be executed
    val minString = strings.minByOrNull { it.also { println("Selector executed") } }

It happens, because minByOrNull has a small optimization - in case the input list has exactly one element, it is always the smallest one, whatever the selector would be, so there is no need to actually execute the selector. Instead, just this element can be returned.

You can see it in the source code of minByOrNull:

1 Like

Oh, the lambda is optimized away ! I used different lambdas first and could not understand how they were ignored, then got dizzy trying various experiments to make sense of it until the minBy and everything was getting blurry.

Thanks for your very helpful reply, it made everything clear, and made me realize looking at the code is very informative.

1 Like

Actually function´s minByOrNull return value doesn´t have anything to do with optimization. This function returns single element of the initial list. In this case we have list of Strings, then this function returns String.
Code inside curly braces {} is just a selector based on which function decides which value to select, it doesn´t convert actual type.

If take example from second message and write it with the types it would be more clear

fun main() {
    val strings: List<String> = listOf("medium string", "this is a long sentence", "short")
    val shortest: String = strings.minByOrNull { it.length }

As you may see shortest is of type String and it can never get a value 5 of type int.

If in the initial example the idea was to convert string value into integer and then select minimal value out of integers, then list of strings should be first mapped into list of integers. Something like following:

val x = s.map{it.toIntOrNull()?:99}.minOrNull()?:42

True, that optimization does not affect the return value (that would be breaking the contract of the function !), but its suppression of side effects did mystify me when I had placed other instructions in the selector, and could not explain how they were completely ignored, as if the list was empty but minByOrNull() still returning a value.
My initial code was using
minByOrNull{ it.toInt() }
and worked as intended (displaying the minimum), but I needed to handle empty strings and I invistigated why and how those empty strings weren’t triggering an exception in the selector’s toInt() call.
At that point I was starting to lose my mind, I tried various changes in the selector including toIntOrNull but also side-effects instructions, and could not understand the behaviour I was observing.

The optimization did have everything to do with completely preventing me from understanding how the toInt() selector could not run but the list be considered not empty, and pointing me to that optimization was indeed the key I needed !