IO#syncStep + IO#unsafeRun = IO#unsafeRunHere? - #2478
Conversation
|
Reminded myself of this relevant excerpt from the comparative benchmark:
Obviously the semantics for JVM and JS are different around this, and if this goes forward maybe it should be a JS-only method. |
|
So… I'm skeptical, but then I don't use React. :-P Intuitively, it almost feels like this belongs on // bikeshed name
def unsafeRunSyncAndContinue[B](
cb: Either[Throwable, B] => Unit)(
implicit runtime: unsafe.IORuntime, ev: A <:< Either[IO[B], B])
: Unit =
ev(unsafeRunSync).fold(_.unsafeRunAsync(cb), a => cb(Right(a)))The def continueAsync[B](
cb: Either[Throwable, B] => Unit)(
implicit runtime: unsafe.IORuntime,
ev: A <:< Either[IO[B], B])
: SyncIO[Unit] = {
val executed = flatMap { a =>
SyncIO {
ev(a).fold(
_.unsafeRunAsync(cb),
b => cb(Right(b)))
}
}
executed.handleErrorWith(e => SyncIO(cb(Left(e))))
}This feels even nicer. Then to get the val ioa: IO[A] = ...
ioa.syncStep.continueAsync(e => ...).unsafeRunSync() |
|
I mean, at least my thinking here was to make this easy to do, and IMHO your proposals seem kind of confusing /shrug.
That's fair. Except, the event loop is a very different runtime than any EC on JVM. So, it's not really fair at all :P |
|
I think we have to accept that since 3.3.0, we're serious about Scala.js as a separate target platform for CE (it's not the case that it was just convenient to support it due to the build plugins being good), which has its own rules, quirks and different execution semantics. With issues like #2530 popping up, I'm starting to think that maybe JS should be executed differently. And it's not going to be the only issue like this. The more we develop JS support, the more people will use it. Splitting the runtime will be beneficial to us too, in the long run, each with its own focus, tradeoffs and optimizations. |
|
By-and-far, the current execution model in CE lends itself very well to the JS runtime. In fact, I'm fairly sure that all the problems on JS boil down to sometimes needing manual control of ceding/yielding. Or more specifically, being able to guarantee there isn't a cede/yield at certain points. #1862 and #2530 are specifically about avoiding that initial yield and the really weird stuff I had to do in typelevel/fs2#2663 (comment) is because of the risk of auto-cedes while registering listeners. So if we could find a safe way to solve this, that would be marvelous. Just as we have |
I think what I'm getting at is just trying to be really up-front about where "magic" is happening and we're breaking the rules that normally hold with respect to Auto-yielding in general can be a bit of a problem in a few places. It honestly makes me wonder if we need something more general here, though anything we do is going to quickly run up against the fact that we can't extend the typeclasses without default implementations. |
Just to be clear, what rule(s) is this specifically? That unsafely running an |
Both. There's also a performance cost because the |
|
Thanks. It sounds like to me that if we could just run the |
|
It definitely matters for There's also consistency with |
What about this #2530 (comment) and this #2530 (comment). This is library code. |
|
@vasilmkd I took a look in scalajs-react and, while I didn't find |
|
I did try tagging @japgolly on the original issue, but without success. |
|
David (japgolly) is less available these days. FWIW I don't really see this issue as specific to scalajs-react, it's just caught in the middle. But maybe I'm missing something. |
|
It's not specific, but it's a successful and well used library which implicitly depends on this behavior (at least on first look), so it is an argument for offering some configuration or making more fundamental changes to the execution behavior. |
|
I feel like we need a broader conversation with Scala.js folks. A lot of this comes down to what is even expected here. |
|
Fair but it's not clear to me that everyone involved understands the tradeoffs. All of these issues seem just focused on the most visible current outcome. |
|
That includes me I guess. What is the tradeoff for avoiding that initial shift? |
For starters, foo()
IO(bar()).unsafeRunAndForget()
baz()Does The correctness one is worth dwelling on as well. As you pointed out, people are coming to us right now complaining that Or rather, it would be arbitrary, except for the fact that I really think the right answer is to lean into |
|
Thanks for the detailed response! Yes please, let's focus on I do see the confusion with your snippet, but I'm not sure how realistic it is. I genuinely believe users want to adopt CE3/ (IO(foo()) *> IO(bar()).start *> IO(baz())).unsafeRunAndForget()Once it's written like this, I think reasoning about this code becomes much easier. I'm not read-up enough to say for sure if this makes sense, but: I think the idea is to rely more heavily on laws (i.e., replace
Well, sure. Except with the current semantics, everyone gets stuck with the shift. However, if we remove that, it should be possible to explicitly opt-in with e.g.
I absolutely agree that having two semantics for the same method is pretty terrible. My best remark about this is that if we (perhaps unrealistically) assume a purely-CE cross-platform-Scala stack, the only place users should have to work with these unsafe methods is when they are calling into JVM-only or JavaScript-only APIs. In which case, that code is most certainly not cross-platform anyway, and it gives us some leeway for different semantics. But ... I'm still not entirely convinced that the semantics are different here. Let's look back at this snippet: foo()
IO(bar()).unsafeRunAndForget()
baz()
On the JVM, the answer is ... non-determinstic!! So, how would changing the semantics on JS contradict something that is non-determinstic on JVM? Apologies if I'm being foolish here.
💯 to this. Let me spin it out into an issue. |
Ah sorry, I must've missed it. Would you mind re-sharing your question? A quick ctrl-f here in this issue doesn't turn anything up |
|
@japgolly great to see you! @vasilmkd was referring to this comment I believe: #2530 (comment) |
|
Hi @armanbilge !!! Thanks and you too! You know I drop into to scalajs-dom from time to time to see if there's anything I can do but you're such a force, everything's always already merged! hehe, love it mate 😊 Ah ok cool thanks for the link, i'll reply now |
|
@japgolly Tldr, |
|
@japgolly Welcome to the discussion. I feel like I need to say this, but my previous comment wasn't snarky in any way, nor did it have some hidden meaning. I wasn't expecting anything from you with regards to your time. I really just want your opinion if you would lend it. 😄 Your input as a maintainer of Thank you again. |
Drafting to get feedback on this. The basic idea is to create a convenience method that starts with
IO#syncStepbefore falling back to the usualIO#unsafeRunmethods.The motivation for
syncStepitself is already covered in #1862, the goal here is just to make it easy to use while also running theIOto completion.Name is subject to much bikeshed of course 😁 for now, I put run "here" to mean its not shifting/yielding immediately, so in some sense it's running "here".