0

How do I check if a sequence is (not) empty, without assuming it can be consumed twice?

In most cases values.any() and values.none() would work (see also here):

fun doSomething(values: Sequence<Int>) {
    if (values.any()) {
        println("We have these values: " + values.joinToString(", "))
    } else {
        println("We have no values")
    }
}

but this fails if the sequence can only be consumed once:

val iterator = (0..8).iterator()
val seqFromIterator = iterator.asSequence()
doSomething(seqFromIterator) // throws `IllegalStateException: This sequence can be consumed only once.

val seqConstrainOnce = sequence { yieldAll(0..8) }.constrainOnce()
doSomething(seqConstrainOnce) // throws `IllegalStateException: This sequence can be consumed only once.

You can get around this by creating a new sequence with the iterator of the original (on which we only call hasNext() to check if it's empty, so nothing has been consumed):

fun doSomethingBetter(values: Sequence<Int>) {
    val iterator = values.iterator()
    if (iterator.hasNext()) {
        val iterable = iterator.asSequence()
        println("We have these values: " + iterable.joinToString(", "))
    } else {
        println("We have no values")
    }
}

Is this really the best way? It distracts from the purpose of the code, making it less readable. Also, we will end up with a sequence that is always constrained to be consumed once, even if the original was not.

5
  • Maybe approach it differently: Consume the sequence, process it, and check that, meaning check the output, not the input to your sequence consuming process. In the case of joinToString you'd have an empty string, so check for that. In your application it will be something else of course. Commented Jul 19, 2023 at 11:18
  • In my case that would mean catching an exception thrown when the sequence contains no elements. I'd prefer to avoid that, but it is possible. In other cases, the result of an empty sequence may be indistinguishable from the result of some specific non-empty sequence. Commented Jul 19, 2023 at 11:36
  • An empty sequence does not throw an exception. And you would have to make changes to the process consuming the sequence to handle the case of an empty incoming sequence and to return something indicating that it was an empty list (NullObject pattern for example). And in the case of indistinguishability: again that is up to the process to return something indicating that it was an empty list (for example returning a nullable type, or an object based on the Either pattern). Commented Jul 19, 2023 at 11:51
  • The sequence is taken as a constructor argument, and the class requires at least one element be present. I cannot change the return type (it's a constructor). I could convert the sequence to a list, perform the check, and pass the list to the constructor, but I was hoping that there would be a clean method to check if a sequence was empty without triggering the "consumed" state. Commented Jul 19, 2023 at 12:16
  • Well, that's not possible without consuming it. Commented Jul 19, 2023 at 12:26

1 Answer 1

0

You can wrap the sequence in a (custom) sequence class that is able to check for emptiness without it counting as a first consumption. Consuming a second time is allowed if the original sequence allows this.

class MySequence<T>(private val sequence: Sequence<T>) : Sequence<T> {
    private val iteratorRef = java.util.concurrent.atomic.AtomicReference(sequence.iterator())
    override fun iterator() = iteratorRef.getAndSet(null) ?: sequence.iterator()
    fun isNotEmpty() = iteratorRef.get()?.hasNext() ?: sequence.iterator().hasNext()
    fun isEmpty() = !isNotEmpty()
}

So now my function would look like this:

fun doSomethingBetterYet(values: Sequence<Int>) {
    val myValues = MySequence(values)
    if (myValues.isNotEmpty()) {
        println("We have these values: " + myValues.joinToString(", "))
    } else {
        println("We have no values")
    }
}

Doesn't seem very idiomatic, but it's the best I've found so far.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.