What's a good name for a function turning a `List&...
# stdlib
r
What's a good name for a function turning a
List<T>
into a
T?
as so:
Copy code
return when (size) {
  0 -> null
  1 -> this[0]
  else -> throw IllegalArgumentException("List has more than one element.")
}
And should it be in the stdlib? We have
single
(which throws if size != 1) and
singleOrNull
(which returns null if size != 1) but I find myself wanting this function more than
singleOrNull
. I'm often calling some kind of query API which returns a Collection but where I know I expect 0..1, and if it returns >1 I want to fail and find out, not hide it as null.
f
Copy code
public inline fun <E> List<E>.singleOrNullIfEmpty(): E? =
    ifEmpty { null }?.single()
e
can be made to work more generally with `Iterable`:
Copy code
fun <E> Iterable<E>.expectSingleOrNull(): E? = with(iterator()) {
    if (hasNext()) next().also { require(!hasNext()) } else null
}
👍 1
i
We have a request for such function, but we haven't found a good name for it: https://youtrack.jetbrains.com/issue/KT-28789/optional-extension-functions-for-collections IMO, it should be something different from
singleOrNull
to avoid confusion.
👌 1
f
zeroOrOne
comes to mind (from regexp).
👍 1
i
or
atMostOne
👍 3
Another question is whether we should add
OrNull
suffix for consistency with other null-returning collection operations. Or probably we could name it similar to
find
which intentionally diverges from that convention, for example,
findSingle
f
An even better thing to do would be to move the
ifNotEmpty
function that we have in Gradle with the TODO move to stdlib into, well, stdlib. This way one could write...
Copy code
collection.ifNotEmpty { single() }
...the whole naming problem would be solved.
👍 2
i
You mean one from
org.jetbrains.kotlin.utils.addToStdlib
package?
f
Yes
m
I'd like to have
singleOrNull(throwIfMultiple: Boolean = false)
. It should both quickly tell the current behavior and allow to alter it. Especially considering that `SingleOrDefault()`in .NET does throw on multiple elements.
f
The beauty of
ifNotEmpty
is that one can implement whatever behavior they like and get
null
ifEmpty
.
j
Do you folks have concrete examples for this use case? It feels like the collection should just have been a nullable type in the first place
r
Take a class
http.Headers
which defines
fun getHeader(headerName: String): List<String>
. I want to define an extension function
Headers.getLocation(): String?
on the basis that it's reasonable for a response not to have a
Location
header, but not for it to have 2 or more
Location
headers. It's a translation function from a less specific to a more specific type - half of programming is doing that. (If your argument held we should get rid of the existing
single
, because the
Collection<T>
should just have been a
T
in the first place.)
😂 2
j
I would argue that for non-repeatable HTTP headers it's reasonable to expect that the last entry overrides the previous ones instead of crashing. And in that case
lastOrNull
does the job. I get the point for the general case, I just haven't been in a situation that required this so far so I'm looking for use cases.
e
that's not how the HTTP spec says to handle duplicate headers though
https://www.rfc-editor.org/rfc/rfc9110.html#name-field-order either it is semantically equivalent to joining the repeated values with commas, or it is invalid
j
I was placing the discussion at the application level above the HTTP layer. Once you get a comma-separated list of values from a header, your application decides what to do with it. Of course, failing with a 400 is an option, but then you'll need something else than
IllegalArgumentException
or
NoSuchElementException
, so even if the stdlib provided this function, you wouldn't use it here.
r
Location
is a response header, not a request header; it's handled by the client not the server.
j
Ah my bad then
r
My current use case - working with a data storage api that returns a collection for a query. Business rules mean that for a certain query it should be impossible for the collection to contain more than one element - it would mean there is a bug.
j
In that case I'd probably want a more informative error message about why it doesn't make sense for that collection to have more than one element. When the error does happen, it makes things easier to debug. The same reason why
?: error("useful message")
is almost always better than
!!
(and I'm saying "almost" just because I don't like absolutes)
m
@Joffrey as you are the one forwarding me to this message and wanted an example: Sometimes when aggregating data you might have a "expect to group to singular fields" result. For instance, we have a complicated data class that holds versioning. When we want to get the version only applying to a certain data, we would filter this list to find only data classes that are for the "correct" version If we find nothing we will default to a standard implementation, but otherwise, we really expect to find only one current version. Right now, there is no way to force a way to expect a single element, or null otherwise in these cases. If we have our checks and requires setupped correctly, we never expect to get multiple elements. Redeclaring: "We know we checked this before, but we'll redefine the same message again in this error" shouldn't be needed. Otherwise a
expectAtMostOneOrThrow
would still be the best bet
j
I get your point. I just find it a bit better to explicitly express the invariant that you're relying on if the check happened in a different place of the code, rather than relying on generic error messages. In your case, if you have verified that only one version is present in another piece of the code, that's your invariant, and I wouldn't find it redundant to mention it in an error like
at most one current version was expected but multiple implementations found: $classNames
. You can of course extract your own domain-specific function for this. I understand the same argument could be made with
single()
, and I kinda agree, unless the invariant is literally checked in the same place, such as:
Copy code
when (myList.size) {
    0 -> EmptyThingy()
    1 -> MonoThingy(myList.single())
    2 -> MultiThingy(myList)
}
r
I think the argument is that this:
Copy code
fun getMyThing(mythings: List<String>): String? {
  val shouldBeOneOrNone = mythings.filter { TODO() }
  if (shouldBeOneOrNone.size > 1)
    throw MoreThanOneThingException(shouldBeOneOrNone)
  return shouldBeOneOrNone.singleOrNull() // guaranteed not to be > 1 now
}

class MoreThanOneThingException(
  val actualThings: List<String>
) : Exception(
  "It should be impossible for there to be more than one thing here for business reason Y, actually got ${actualThings.size}: $actualThings"
)
is better than:
Copy code
fun getMyThing(mythings: List<String>): String? = mythings.atMostOne { TODO() }
because you get a more explicit error message.
Personally I feel that: 1)
IllegalArgumentException("Collection has more than one element.")
with a stacktrace to the line of code is enough for me to work it out pretty quickly
And 2) Even if doing a custom invariant check and throw, I'd still rather use
atMostOne
after the check than
singleOrNull
, because it's explicit - you don't have to read the previous check to know that there's no chance you are turning a multi element collection into null.
e
Scala pattern matching is neat:
Copy code
list match {
    case List() => null
    case List(single) => single
    case _ => throw ...
}
I'm not sure how that could be brought into Kotlin in a natural way, though. there's discussion in https://youtrack.jetbrains.com/issue/KT-186 but nothing conclusive
j
@Rob Elliot yeah that's what I meant, although the specific exception might be unnecessary if it's not meant to be caught anyway (depending on the use case). 1. Maybe you're right and the stacktrace is sufficient most of the time. That said, I have often been in situations where stale stacktraces were not helpful when the code was moved around, and an explicit log or exception message was very valuable 2. I agree with this point
r
I'm using
List
for simplicity, but I'd expect it to be on
Iterable
and so
size
not to be trivially accessible - otherwise this isn't a million miles away from scala:
Copy code
fun getMyThing(mythings: List<String>): String? = mythings
    .filter { TODO() }
    .let { shouldBeOneOrNone ->
        when (shouldBeOneOrNone.size) {
            0 -> null
            1 -> shouldBeOneOrNone[0]
            else -> throw MoreThanOneThingException(shouldBeOneOrNone)
        }
    }
More generally this discussion seems a bit odd in the context of a Collection stdlib that is chock full of exception throwing narrowing transformations: •
single
first
last
find
etc.
m
@Joffrey Like you said yourself, the implication that
single
exists and therefore a generic exception is thrown when its predicate gets violated, an
atMostOne
wouldn't violate any new rules.
j
@Rob Elliot
find
should not be in your list I believe, it returns null. But otherwise, you're right, maybe I'm just being obtuse and such a function could be useful in some cases, especially given that so many of its friends already exist.
e
r
I saw, I was commenting on the Scala pattern matching idea - does Scala pattern match across Iterables like that? I thought not, because it relies on constructors of case classes...
e
ah, not out of the box but defining matchers which support it is doable
or
.take(2)
, which gives you all the information necessary, and is also something you could do in Kotlin to match on
size
👍 1