https://kotlinlang.org logo
#getting-started
Title
# getting-started
m

Mark

04/21/2022, 4:21 AM
Is there a standard lib function that takes an
Iterable
and returns true if and only if all items return the same value for a given lambda? I don't care what the value is. I know I can just apply lambda to first item and then use
Iterable.all {}
but I was wondering if there was a nicer way.
m

Maroš Šeleng

04/21/2022, 6:09 AM
I don’t know about any stdliib function, but maybe something like
yourList.distinctBy { getResult(it) }.size == 1
m

Mark

04/21/2022, 6:11 AM
Yeah, but that would require all items to be iterated.
m

Maroš Šeleng

04/21/2022, 6:33 AM
Well that’s true. You have to iterate over the items in order to get the lambda’s value for each item.
m

Mark

04/21/2022, 6:35 AM
If you only want to find out if they are all the same, then if the first two are different, then you don't need to calculate the others.
m

Michael de Kaste

04/21/2022, 7:37 AM
I'd do it with zipWithNext(), the comparisons are lazy as long as you work with a sequence
Copy code
yourList.asSequence().map{ applyLambda(it) }.zipWithNext().any{ (a,b) -> a != b }
if the first element equals the second element, you might as well compare with the second element as well when you are at the third element, etc.
👏 2
m

Mark

04/21/2022, 7:43 AM
How about for a good name?
Copy code
fun <T> Iterable<T>.allSameBy(
    transform: (T) -> Any,
): Boolean = asSequence()
    .map(transform)
    .zipWithNext()
    .all { (a, b) -> a == b }
🙌 1
e

ephemient

04/22/2022, 11:15 AM
Something less clever is fine too, I'd just use a raw iterator as a best-effort way to get a consistent result even in case of concurrent modification
Copy code
fun <T> Iterable<T>.allSameBy(
    transform: (T) -> Any,
): Boolean {
    val i = iterator()
    if (!i.hasNext()) return true
    val expected = transform(i.next())
    for (item in iterator) if (transform(item) != expected) return false
    return true
}
👍 1
m

Mark

04/22/2022, 1:33 PM
I added a quick check for collection and checking if multiple items (since
all
) does a similar check
Copy code
fun <T> Iterable<T>.allSameBy(transform: (T) -> Any): Boolean {
    if (this is Collection && size < 2) return true
    return asSequence()
        .map(transform)
        .zipWithNext()
        .all { (a, b) -> a == b }
}
@ephemient couldn't that be simplified to:
Copy code
fun <T> Iterable<T>.allSameBy(
    transform: (T) -> Any,
): Boolean {
    val expected = transform(firstOrNull() ?: return true)
    return all { transform(it) == expected }
}
e

ephemient

04/22/2022, 1:42 PM
no, because you may end up with a non-consistent view of the collection
e.g.
CopyOnWriteArrayList
is consistent if you iterate it once, but if you're looking at first and all separately, they may not be consistent under mutation
👍 1
m

Mark

04/22/2022, 1:45 PM
Copy code
fun <T> Iterable<T>.allSameBy(
    transform: (T) -> Any,
): Boolean {
    var expected: Any? = null
    return all {
        if (expected == null) {
            expected = transform(it)
            return@all true
        }
        transform(it) == expected
    }
}
e

ephemient

04/22/2022, 1:48 PM
if your transform never returns null, yes. need to handle the initial element specially (or use a private sentinel object) otherwise
m

Mark

04/22/2022, 1:48 PM
just
lateinit
?
e

ephemient

04/22/2022, 1:49 PM
no, lateinit requires non-null and also you can't detect if it's initialized or not in a local var
m

Mark

04/22/2022, 1:49 PM
If the transform return type is
Any
doesn't that mean it can't return null?
e

ephemient

04/22/2022, 1:50 PM
yes, that restricts it to nonnull returns
m

Mark

04/22/2022, 1:53 PM
Can't seem to find a way to check if a lateinit variable (within function) is initialized
e

ephemient

04/22/2022, 1:53 PM
yes, that's what I said
m

Mark

04/22/2022, 1:55 PM
Sorry, missed that bit. Ok, let's ditch the lateinit idea.
Just revisiting this:
Copy code
fun <T> Iterable<T>.allSameBy(transform: (T) -> Any): Boolean {
    // let's see if we can avoid creating the sequence
    if (this is Collection && size < 2) return true
    // return true iff 0 or 1 distinct transformed values
    // however, don't use count() because that will force iteration over entire iterable
    return this.asSequence().map(transform).distinct().drop(1).none()
}
e

ephemient

06/04/2022, 9:53 AM
1. checking size/isEmpty is ok as an optimization, but correctness should not depend on it - e.g. if a mutable collection changes between the size check and the following code, the result should still be consistent with at least one of the observable states of the collection. looks fine here, but 2. IMO the expectation is that all collections extensions are inlineable (allowing early returns etc., unless it's not reasonable, e.g.
groupingBy
which creates an intermediate data structure). so I would not use
Sequence
as long as there's a better way. but your code, your preferences…
m

Mark

06/04/2022, 1:35 PM
Copy code
inline fun <T> Iterable<T>.allSameBy(transform: (T) -> Any): Boolean {
    // let's see if we can avoid creating the iterator
    if (this is Collection && this.size < 2) {
        return true
    }
    val iterator = this.iterator()
    if (!iterator.hasNext()) {
        return true
    }
    val firstItemTransformed = transform(iterator.next())
    if (!iterator.hasNext()) {
        return true
    }
    for (item in iterator) if (transform(item) != firstItemTransformed) return false
    return true
}
Pretty much what you wrote a month ago but with some extra checks