I have a long computation in a non-suspending func...
# coroutines
a
I have a long computation in a non-suspending function which consists of several transformations (map, reduce etc.) on a
Sequence
. How do I make this computation properly cancelable? All the Sequence functions are non-suspending, and the coroutine context is not available in a non-suspending function, so I can’t check for
isActive
. Can I capture the coroutine context in the outermost (suspend) function and pass it into all the (non-suspending) sequence functions?
j
Is it an option for you to make this non-suspending function
suspend
? That would probably be the easiest way. Making it
suspend
and adding
yield()
at the points where you want to check for cancellation (and allow interlacing with other suspend functions)
a
I can make the main function itself suspend, sure. The problem is with the sequence functions. It looks like this:
Copy code
fun compute(input: Sequence<T>){
    return input
       .map{ ... }
       .reduce{ ... }
       .groupBy{ ... }
       // etc
}
The sequence functions don’t take suspending functions, they take regular functions.
My idea is to do this:
Copy code
suspend fun compute(input: Sequence<T>){
    val context = coroutineContext
    return input
       .map{
           if (!context.isActive)
               throw CancellationException()
           ... 
       }
       // etc
}
j
Which terminal operator are you using? Usually the terminal operator is
inline
, so you can use suspend functions inside
Copy code
suspend fun compute(input: Sequence<T>){
    return input
       .map{ ... }
       .forEach {
           doStuffWith(it)
           yield()
       }
}
a
Hmm
That’s a good point, but if the intermediate operators take all the time, and the terminal operator is only run at the end, that may not help
j
Same goes with
reduce
, which is also a terminal operator. Basically non-terminal operators like
map
don't take any time because they just build a new Sequence with the given non-inlined lambda. So you can't call suspend functions there, but it's ok anyway because nothing really runs there. However, when you use terminal operators like
reduce
or
forEach
they actually iterate the sequence and take time, but they are also inline - so you can use
yield()
in them
a
map
can take time, if the mapping function itself is a long computation
j
map
itself doesn't take any time, but its lambda does when it's called, and it's only called when a terminal operator is called on the sequence. My point is that you only really need
yield()
within loops (or any terminal operator) between each element, and that can be done by inserting it within the terminal operators' lambdas.
a
I understand, but in my case the lambda I pass to
map
takes time.
The example code I wrote may be confusing. The actual code is more like
Copy code
.map{ computeItem(it, context) }
and the check for
isActive
is in a loop in
computeItem
j
If I understand correctly, you mean that allowing interlacing and cancellation between each element is not sufficient for you, and you would like to cancel between each operation even on the same element? If you do:
Copy code
input
       .map { longTransform1(it) }
       .map { longTransform2(it) }
       .forEach {
           doStuffWith(it)
           yield()
       }
What will happen is the following:
Copy code
for (e in input) {
    val intermediate1 = longTransform1(e)
    val intermediate2 = longTransform1(intermediate1)
    doStuffWith(intermediate2)
    yield()
}
Are you saying you would prefer to have cancellability even between each of those transformations? Like this:
Copy code
for (e in input) {
    val intermediate1 = longTransform1(e)
    yield()
    val intermediate2 = longTransform1(intermediate1)
    yield()
    doStuffWith(intermediate2)
    yield()
}
a
Even inside
longTransform
j
Then I think you should just use
Flow
instead of
Sequence
, which is meant for this - same properties as sequences but with suspendable transformations
1
a
Interesting idea
j
I mean, if you even need cancellability in the transform functions, probably you should make those transform functions
suspend
and use flows
a
yes, you’re right
I’ll just do
Copy code
input.asFlow()
    .map{}
    // etc.
👍 1
j
Yep exactly
a
and make the computations suspend functions with
yield
inside
awesome, thanks!