typelevel / cats-effect

The pure asynchronous runtime for Scala
https://typelevel.org/cats-effect/
Apache License 2.0
2.03k stars 521 forks source link

"`cedeMap`" and "`intercede`" combinators #3318

Open armanbilge opened 1 year ago

armanbilge commented 1 year ago

Explicitly cede-ing between expensive compute-bound operations is one of the strategies for addressing CPU-starvation and is suggested in the warning.

https://github.com/typelevel/cats-effect/blob/832079aecbbf956fb18f11392cfeec6cdaa52915/core/shared/src/main/scala/cats/effect/CpuStarvationCheck.scala#L43-L44

How to do this is described in the scaladocs for cede.

https://github.com/typelevel/cats-effect/blob/52b5a3ba1623ff40fc48318c9a0bfae558f0e5ed/kernel/shared/src/main/scala/cats/effect/kernel/GenSpawn.scala#L263-L269

Implementing that correctly is not completely trivial (*cough* https://github.com/typelevel/cats-effect/pull/3166 😜) and noisy enough that it probably deserves its own combinator.

In fact I think we need a couple combinators, depending on whether or not the expensiveWork() is in F[_] or not.

def cedeMap[A, B](fa: F[A])(f: A => B): F[B] =
  (fa <* cede).map(a => f(a)).guarantee(cede)

def intercede[A](fa: F[A]): F[A] =
  cede *> fa.guarantee(cede) 

(Names subject to bikeshed.) These should also be added as syntax and on IO itself.

I think both variants are important, because for example the following would not achieve the desired semantics.

fa.flatMap(data => intercede(F.pure(expensiveWork(data))))

There, expensiveWork() would be computed eagerly when the pure(...) is constructed (before the cede), not when it is interpreted (after the cede).

biuld commented 1 year ago

I want to help resolve this issue. 😃

armanbilge commented 1 year ago

@biuld go for it!