Is there a standard lib function that takes an `It...
# getting-started
m
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
I don’t know about any stdliib function, but maybe something like
yourList.distinctBy { getResult(it) }.size == 1
m
Yeah, but that would require all items to be iterated.
m
Well that’s true. You have to iterate over the items in order to get the lambda’s value for each item.
m
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
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
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
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
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
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
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
if your transform never returns null, yes. need to handle the initial element specially (or use a private sentinel object) otherwise
m
just
lateinit
?
e
no, lateinit requires non-null and also you can't detect if it's initialized or not in a local var
m
If the transform return type is
Any
doesn't that mean it can't return null?
e
yes, that restricts it to nonnull returns
m
Can't seem to find a way to check if a lateinit variable (within function) is initialized
e
yes, that's what I said
m
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
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
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