singleOrNull - what's your expectation?

#1

In Kotlin’s standard library, there are extension functions single and singleOrNull defined on Iterable or List:

public fun <T> List<T>.single(): T {
	// ...
}

public fun <T> List<T>.singleOrNull(): T? {
	// ...
}

While there is no argument what single should do if list is not size 1, I’m wondering about your expectations for singleOrNull?

  1. null if list is empty, throw if size > 1
  2. null if list is empty, null if size > 1

In my opinion, Kotlin’s standard library is one of the best if not the best designed library for a programming language out there :smiling_face_with_three_hearts:, however this is one of a few places where it defied my expectations (which lead to a production disaster).

As a result, we had to ban it from our codebase (similarly for use which I wrote about in another topic).

Thoughts?

#2

I would expect singleOrNull to return null if the list is empty or has more than one element. This is consistent with the other xxxOrNull functions in the standard library. They all return null in the case that their base version would throw.
Also this is clearly stated in the documentation of the function so I don’t see the big problem.


Could you link me the post about use. I can’t remember it and I would like to know why you don’t like it. As far as I understand it, it works the same way as javas try with resource statement.

#3

A stdlib function that ends on “orNull” never throws an exception. It’s easy to remember that.

1 Like
#4

RTFM

2 Likes
#5

Perhaps I’m too much into coding defensively :slight_smile: If I expect a list to have 0 or 1 elements, then if it has two elements something is wrong, and I don’t want to treat that case as if the list was empty.

Consider I want to create a new user, and username should be unique. Then I query the database to find if the username exists. If I get two rows back, something is wrong, and I don’t want to proceed as if the username is not there at all (and create yet another one).

It is much harder to come up with a practical example where you want to treat 0 and >1 elements the same.

I am not making a case it is not documented well. I am saying that it goes against expectation which can lead to, and more importantly, hide other bugs. And another point is that the current behavior has less utility.

A quick search through Kotlin’s github repo: https://github.com/JetBrains/kotlin/search?q=singleornull&unscoped_q=singleornull supports my intuition. 90% of the time singleOrNull was used there, the caller actually doesn’t expect >1 elements.

#6

It’s a hard one. It is easier to state what I don’t expect

  • Throw an exception of there are no elements
  • Return any of the elements if size>1
  • Behave fundamentally different from single()
  • Throw an exception (It is the exception free version after all)
  • Duplicate firstOrNull()

Based upon those requirements the current behaviour is the only valid behaviour, but I do see how it is also confusing. The problem I see is that if the two abnormal cases (0 or >1 elements) may need to be handled differently, in that case it is not possible to have a single function do so (unless it is higher order). In the case that more than 1 element is an error, there is only one thing to it, test that. Perhaps with an extension function that explicitly throws an exception when there are multiple elements.

5 Likes
#7

I would go for (2) to return null

my expectation whenever I see …OrNull that a null will be returned instead of exception

#8

I expect it to throw exception on list of size 2+ and return null on empty list.

#9

People who expect this probably think that “single or null” stands for “the collection is allowed to have a single or null elements”.

But that is not the meaning of “orNull”. In the stdlib “orNull” generally means, returns null instead of throwing an exception. Hence “singleOrNull” would stand for “the collection is allowed to have 1 element, otherwise null is returned.”

3 Likes
#10

Yes.

I can’t imagine the code where this treatment is useful. It’ll hide bugs. I prefer bugs to explode early.

#11

Often this is accompanied with an Elvis operator that handles the erroneous case immediately.

Examples:

staff.filter{ it is Janitor }.singleOrNull() ?: throw IllegalStateException("Dedicated Janitor not found")

Or

staff.filter{ it is Janitor }.singleOrNull() ?: fallbackJanitor
#12

Again, I can’t imagine the code where it would be useful. Why do you want to handle only the case when there is a single Janitor and multiple Janitors is not an error? Sure, it’s possible, but in my experience it’s rare case. And I’m usually using similar function to query database by unique set of fields and I’m using it a lot. I’m expecting to get exactly one row if I’ve found it and zero rows if I didn’t find it. Multiple rows indicate corrupted database and I want to catch that ASAP. Treatment of multiple rows as null means that my business logic will be wrong.

#14

I understand your reasoning and may be I just need a different function, say singleOrNullIfEmpty(). Anyway my point was that I’m using a function which is named very similary both in Java and in Kotlin and it works exactly like this: null for empty result set; exception for multiple items. So to return to the original question, my natural expectation was exactly this meaning. I can see that different people might see it differently and Kotlin already sealed its meaning, so it’s better to stick with it, whether I like it or not.

#15

I think the problem comes from the predefined meaning of xxxOrNull in the kotlin standard library. As many people have pointed out it means that every possible error case is handled by returning null.
Therefor I think that the current implementation of singleOrNull is the right one for the kotlin standard library. That being said, I think I would have chosen the other option if I were just to implement singleOrNull without the context of the library.

I also agree that depending on the domain you would want to throw exceptions more aggressively in order to ensure that you catch invalid states of persistent data early. I like the idea of singleOrNullIfEmpty(). It’s maybe a bit verbose, but it clearly states what it does and wouldn’t conflict with the orNull functions in the std-lib.

#16

Exactly!

We ended up with our own extension zeroOrOne so that it doesn’t get mixed up with Kotlin’s convention. Still I feel it’s a dangerous function to have in the Standard Library.

#17

What’s dangerous are APIs that return a collection for something than can have zero or 1 element. :wink:

1 Like
#18

I looked it up in our project. One of the real world uses of this function in our project is this:

We have a website where the user can search for products by category. He can combine an arbitrary number of categories. Those are stored in a collection.

When there is only one category in the current search, we want to optimize the page for that category according to SEO best practices. If there is no category or more than one category, we don’t want to do this optimization.

1 Like
#19

Just in this moment, I am using singleOrNull for another use case, that I am glad to share.

The products on our website can be filtered by region. For this purpose the user can enter an arbitrary region name. Many regions have similar names like for example “Frankfurt (Main)” and “Frankfurt (Oder)”.

We have a region-name-resolving service that returns a collection of region objects for an arbitrary region name input. When the input matches multiple regions (e.g. “frankfurt”) then the collection will contain more than 2 elements.

This collection of region objects will be used for filtering the current search. However, the search will be filtered only, if the entered region name was not ambiguous. This is the simplified code example:

val regions = resolveRegionName(regionName)
filters.region = regions.singleOrNull()
didYouMean.regions = regions.takeIf { size > 1 }
1 Like
#20

For complex cases, may be you should consider a specific “Single” class as “Optional” in Java.

enum class Cardinality {
    NONE, SINGLE, MORE;
}
class Single<T>(val value : T?, val card : Cardinality, val tail : List<T>)

fun <T> List<T>.single(filter : (T) -> Boolean = { true} ) : Single<T> { ... }

this allowed you to treat all cases with specific needs while keeping functionnal programming style.

#21

Not sure this should be part of the standard library, but in any way, it should probably be implemented like this

sealed class OptionalList<T> { // I can't think of a good name
    class None
    class Single<T>(val vaule: T)
    class Multiple<T>(val list: List<T>)
}

That way it’s a bit more idiomatic.

1 Like