Rob Elliot
09/12/2022, 10:51 AMList<T>
into a T?
as so:
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.Fleshgrinder
09/12/2022, 10:54 AMpublic inline fun <E> List<E>.singleOrNullIfEmpty(): E? =
ifEmpty { null }?.single()
ephemient
09/12/2022, 11:01 AMfun <E> Iterable<E>.expectSingleOrNull(): E? = with(iterator()) {
if (hasNext()) next().also { require(!hasNext()) } else null
}
ilya.gorbunov
09/12/2022, 12:17 PMsingleOrNull
to avoid confusion.Fleshgrinder
09/12/2022, 12:23 PMzeroOrOne
comes to mind (from regexp).ilya.gorbunov
09/12/2022, 12:24 PMatMostOne
ilya.gorbunov
09/12/2022, 12:27 PMOrNull
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
Fleshgrinder
09/12/2022, 12:32 PMifNotEmpty
function that we have in Gradle with the TODO move to stdlib into, well, stdlib. This way one could write...
collection.ifNotEmpty { single() }
...the whole naming problem would be solved.ilya.gorbunov
09/12/2022, 12:33 PMorg.jetbrains.kotlin.utils.addToStdlib
package?Fleshgrinder
09/12/2022, 12:38 PMmcpiroman
09/13/2022, 9:28 AMsingleOrNull(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.Fleshgrinder
09/13/2022, 9:37 AMifNotEmpty
is that one can implement whatever behavior they like and get null
ifEmpty
.Joffrey
09/16/2022, 8:29 PMRob Elliot
09/16/2022, 9:29 PMhttp.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.)Joffrey
09/17/2022, 5:51 AMlastOrNull
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.ephemient
09/17/2022, 6:22 AMephemient
09/17/2022, 6:25 AMJoffrey
09/17/2022, 7:24 AMIllegalArgumentException
or NoSuchElementException
, so even if the stdlib provided this function, you wouldn't use it here.Rob Elliot
09/17/2022, 8:12 AMLocation
is a response header, not a request header; it's handled by the client not the server.Joffrey
09/17/2022, 8:13 AMRob Elliot
09/17/2022, 8:14 AMJoffrey
09/19/2022, 8:39 AM?: error("useful message")
is almost always better than !!
(and I'm saying "almost" just because I don't like absolutes)Michael de Kaste
09/19/2022, 8:52 AMexpectAtMostOneOrThrow
would still be the best betJoffrey
09/19/2022, 9:02 AMat 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:
when (myList.size) {
0 -> EmptyThingy()
1 -> MonoThingy(myList.single())
2 -> MultiThingy(myList)
}
Rob Elliot
09/19/2022, 9:02 AMfun 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:
fun getMyThing(mythings: List<String>): String? = mythings.atMostOne { TODO() }
because you get a more explicit error message.Rob Elliot
09/19/2022, 9:03 AMIllegalArgumentException("Collection has more than one element.")
with a stacktrace to the line of code is enough for me to work it out pretty quicklyRob Elliot
09/19/2022, 9:06 AMatMostOne
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.ephemient
09/19/2022, 9:07 AMlist 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 conclusiveJoffrey
09/19/2022, 9:08 AMRob Elliot
09/19/2022, 9:17 AMList
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:
fun getMyThing(mythings: List<String>): String? = mythings
.filter { TODO() }
.let { shouldBeOneOrNone ->
when (shouldBeOneOrNone.size) {
0 -> null
1 -> shouldBeOneOrNone[0]
else -> throw MoreThanOneThingException(shouldBeOneOrNone)
}
}
Rob Elliot
09/19/2022, 9:19 AMsingle
• first
• last
• find
etc.Michael de Kaste
09/19/2022, 9:25 AMsingle
exists and therefore a generic exception is thrown when its predicate gets violated, an atMostOne
wouldn't violate any new rules.Joffrey
09/19/2022, 9:32 AMfind
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.ephemient
09/19/2022, 9:37 AMRob Elliot
09/19/2022, 9:38 AMephemient
09/19/2022, 10:01 AMephemient
09/19/2022, 10:02 AM.take(2)
, which gives you all the information necessary, and is also something you could do in Kotlin to match on size