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 PMatMostOneilya.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, findSingleFleshgrinder
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