Mark
06/04/2022, 4:30 AMfun <T, R> Iterable<T>.singleOrNullBy(transform: (T) -> R): R? {
if (this is Collection && this.isEmpty()) return null
return asSequence()
.map(transform)
.zipWithNext()
.map { (a, b) -> if (a == b) a else null } // important we don't use mapNotNull here
.distinct()
.singleOrNull()
}
ephemient
06/04/2022, 5:38 AMinline fun <T, R> Iterable<T>.singleOrNullBy(transform: (T) -> R): R? {
return fold(null) { acc: R?, item ->
val value = transform(item)
if (acc == null || acc == value) value else return null
}
}
ephemient
06/04/2022, 5:45 AMnull
the same way; if you need that, a simple sentinel can do:
inline fun <T, R> Iterable<T>.singleOrNullBy(transform: (T) -> R): R? {
val nothing = Any()
return fold(nothing) { acc: Any?, item ->
val value = transform(item)
if (acc === nothing || acc == value) value else return null
}.takeIf { it !== nothing } as R?
}
ephemient
06/04/2022, 5:47 AMsingleOfOrNull
Mark
06/04/2022, 6:57 AMIterator
?
inline fun <T, R> Iterable<T>.singleOfOrNull(transform: (T) -> R): R? {
// let's see if we can avoid creating the iterator
if (this is Collection) {
if (this.isEmpty()) return null
if (this is List && size == 1) {
return transform(this[0])
}
}
return this.iterator().singleOfOrNull(transform)
}
inline fun <T, R> Iterator<T>.singleOfOrNull(transform: (T) -> R): R? {
if (!this.hasNext()) {
return null
}
val firstItemTransformed = transform(this.next())
if (!this.hasNext()) {
return firstItemTransformed
}
this.forEach { item ->
if (transform(item) != firstItemTransformed) {
return null
}
}
return firstItemTransformed
}
Mark
06/04/2022, 7:13 AMinline fun <T, R> Iterable<T>.singleOfOrNull(noinline transform: (T) -> R): R? {
// let's see if we can avoid creating the sequence
if (this is Collection) {
if (this.isEmpty()) return null
if (this is List && this.size == 1) {
return transform(this[0])
}
}
return this.asSequence().map(transform).distinct().singleOrNull()
}
Regarding naming, maybe singleDistinctOfOrNull
because single by itself doesn’t say anything about distinct values.ephemient
06/04/2022, 9:58 AMSequence
, just don't make the extension function inline
to begin with, and see my other comment about being safe wrt. TOCTTOU and mutable collections. but sure, that's a reasonable name and straightforward implementationephemient
06/04/2022, 10:05 AMinline fun <T, R> Iterable<T>.singleOfOrNull(transform: (T) -> R): R? {
val iterator = iterator()
if (!iterator.hasNext()) return null
val single = transform(iterator.next())
for (item in iterator) if (transform(item) != single) return null
return single
}
as Kotlin defines Iterator.iterator() to return itselfephemient
06/04/2022, 10:16 AMval list = CopyOnWriteArrayList(listOf(1))
thread { list.add(2); list.add(3); list.remove(1) }
list.singleOfOrNull { it }
could conceivably return 2
in your implementation, even though there is never a point in time where list == listOf(2)
)Mark
06/04/2022, 1:17 PMIterator
-based implementation but without the quick checks?
https://kotlinlang.slack.com/archives/C1H43FDRB/p1654337100738409?thread_ts=1654317012.586139&cid=C1H43FDRBephemient
06/04/2022, 1:18 PM.forEach
to for
2. your second quick check is vulnerable to TOCTTOUMark
06/04/2022, 1:27 PMinline fun <T, R> Iterable<T>.singleDistinctOfOrNull(transform: (T) -> R): R? {
// let's see if we can avoid creating the iterator
if (this is Collection && this.isEmpty()) {
return null
}
val iterator = this.iterator()
if (!iterator.hasNext()) {
return null
}
val firstItemTransformed = transform(iterator.next())
if (!iterator.hasNext()) {
return firstItemTransformed
}
for (item in iterator) if (transform(item) != firstItemTransformed) return null
return firstItemTransformed
}
ephemient
06/04/2022, 1:28 PMis List
check either, but you aren't saving anything: .singleOrNull()
uses iterator()
itselfMark
06/04/2022, 1:31 PMsingleOrNull()
using Iterator
- I’ve just updated it.