cats-effect icon indicating copy to clipboard operation
cats-effect copied to clipboard

Introduce an `unyielding { ... }` construct

Open armanbilge opened this issue 3 years ago • 16 comments

Spinning out of https://github.com/typelevel/cats-effect/pull/2478#issuecomment-982030581. tl;dr, a way to prevent (auto?) cedes/yields for a region of the program.

armanbilge avatar Nov 29 '21 22:11 armanbilge

I think it would probably also suppress regular cedes. So something like:

unyielding(cede) <-> unyielding(unit)

djspiewak avatar Nov 29 '21 22:11 djspiewak

Also maybe unceding would be a better name. Or… something

djspiewak avatar Nov 29 '21 22:11 djspiewak

uncedable :P

armanbilge avatar Nov 29 '21 22:11 armanbilge

undecedable

djspiewak avatar Nov 29 '21 22:11 djspiewak

It this the same idea as tokio::task::unconstrained? https://docs.rs/tokio/latest/tokio/task/fn.unconstrained.html

armanbilge avatar Mar 03 '22 00:03 armanbilge

Nice catch! Yes it is.

djspiewak avatar Mar 03 '22 15:03 djspiewak

Another thought about this ... in JS land, can't we very nearly cheat with:

def undecedable[A](fa: F[A]) = evalOn(fa, JSExecutionContext.queue)

armanbilge avatar Mar 13 '22 04:03 armanbilge

Is that the promises-based one? If so then yes, that would basically work. :-P

djspiewak avatar Mar 13 '22 04:03 djspiewak

Yeah 😆 for this to work it assumes that you are currently on some macrotask-based executor.

But the thing is, all the instances that I've wanted undecedable is specifically to do stuff like registering callbacks before I/O events start triggering. I.e. it's less about preventing ceding altogether, and more about preventing ceding with respect to I/O.

So this would exactly achieve that semantic without requiring any changes from implementers. I think this should do just fine for JS.

armanbilge avatar Mar 13 '22 05:03 armanbilge

Forgot to add some notes from another recent discussion on this. Specifically, what to do about:

IO.undecedable(IO.sleep(...))

or evalOn(...) or async(...).

I think an important distinction to make here is that when you are reaching for undecedable it's not because you are looking to suppress IO.cede specifically, it's because you are looking to suppress rescheduling for some region of code.

In code you 100% control, you can avoid those problem combinators the only rescheduling to worry about is auto-cedes from the runtime. But if you are applying it to arbitrary code (and the type system won't stop you!) then it could easy contain operations that unavoidably require rescheduling. It's a leaky abstraction.

The undecedable operation is really only safe in the context of an effect that is at most Concurrent+Sync, and not Temporal or Async. But we don't have a great way to express that right now.

See also the definition of Cont which relies on higher-rank polymorphism to apply this sort of constraint, but at least there it is safe to lift F ~> G, which would not be the case here.

armanbilge avatar Feb 21 '23 05:02 armanbilge

After writing all of that 😂 maybe the best we can do (and we can do today, without breaking compat!) is introduce a new API on IO only, which is simply noAutoCede(...).

armanbilge avatar Feb 21 '23 05:02 armanbilge

I'm wondering if a syncStep-like construct would make sense here: "run this without rescheduling until you can, then return the rest (if any) in an F[A]"?

durban avatar Feb 21 '23 09:02 durban

Interesting idea, but I'm not sure. syncStep is helpful because if it works, you can "short-circuit" with the synchronous result and take a different path of execution with that result, that is not possible otherwise.

In this case, I don't think there is really a branch (unless I missed your point :). Either way in the end, you end up with F[A], and it either executed with the semantics you wanted, or it didn't.

armanbilge avatar Feb 21 '23 10:02 armanbilge

I don't know... yes, if you want no rescheduling, you probably absolutely need it. So you're probably right.

durban avatar Feb 21 '23 21:02 durban

The undecedable operation is really only safe in the context of an effect that is at most Concurrent+Sync, and not Temporal or Async. But we don't have a great way to express that right now.

I understand Sync, but if it's Concurrent, how do you do join?

durban avatar Feb 21 '23 21:02 durban

Hmm you are right! join also wouldn't work, because it requires asynchronous suspension 🤔 so that rules out Concurrent and Spawn. But then we don't even have a concept of cede ... and maybe that's the point. So Sync is the best you can do.

armanbilge avatar Feb 21 '23 21:02 armanbilge