-
Notifications
You must be signed in to change notification settings - Fork 508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add IO#unsafeRunSyncOrFuture #47
Conversation
Codecov Report
@@ Coverage Diff @@
## master #47 +/- ##
==========================================
+ Coverage 86.94% 87.13% +0.19%
==========================================
Files 17 17
Lines 268 272 +4
Branches 10 10
==========================================
+ Hits 233 237 +4
Misses 35 35 |
I don't object to the fact that First, the boxing of the closure for a signature like Second, I'm assuming you have a Finally, this seems like a relatively small thing to optimize, overall. Even if it's not possible to avoid the active thread bounce in Monix, the only thing you're optimizing is the limited case where an |
I was not talking about the boxing, which is irrelevant here. I'm saying that within that version you have to build your callback before making the call, which is a problem in case that In other words you don't have to be in an
We do have that, however I cannot expose this stack unsafety in Monix. Here's the reason why: import monix.eval._
import cats.effect._
def fromIO[A](io: IO[A]): Task[A] =
Task.unsafeCreate[A] { (context, callback) =>
io.unsafeRunAsync {
case Right(a) => callback.onSuccess(a)
case Left(e) => callback.onError(e)
}
}
def signalIO[A](a: A): IO[A] =
IO(a)
def signalTask[A](a: A): Task[A] =
fromIO(signalIO(a))
def sumN(n: Int, acc: Long = 0): Task[Long] =
signalTask(n).flatMap { x =>
if (x <= 0) Task.now(acc)
else sumN(n-1, acc+n)
}
sumN(10000).runAsync.onComplete(println)
//=> java.lang.StackOverflowError
//=> ...
//=> at cats.effect.IO$$anonfun$async$1.apply(IO.scala:502)
//=> at cats.effect.IO$$anonfun$async$1.apply(IO.scala:500)
//=> at cats.effect.IO.unsafeRunAsync(IO.scala:296)
//=> ...
//=> at monix.eval.internal.TaskRunLoop$.executeOnFinish$1(TaskRunLoop.scala:158)
//=> at monix.eval.internal.TaskRunLoop$.monix$eval$internal$TaskRunLoop$$loop$1(TaskRunLoop.scala:224)
//=> at monix.eval.internal.TaskRunLoop$RestartCallback$1.onSuccess(TaskRunLoop.scala:119)
//=> ... Notice here how the In my opinion this kind of mistake is a booby trap for users that the library should not expose without big warning signs with "here be dragons". This is why Monix's stack-unsafe
This isn't a small thing to optimize, this problem will be hit by every implementation that will want to convert from an
And if indeed I do understand the concerns that this increases the API surface, but in this case it's worth it because |
Ok, that's compelling.
Except I'll give this some thought. I think you've convinced me that we need some way to externally decompose the special case of a fully-synchronous |
How about something like this? (avoids adding another unsafe function, and exposes exactly and only as much internals as necessary to distinguish the meaningful cases) // name tbd
def fold[R](async: ((Either[Throwable, A] => IO[Unit]) => IO[Unit]) => R, sync: Either[Throwable, A] => R): IO[R] As a sidebar, we could do even better if you wanted to deconstruct the full internals in a safe way: trait Deconstructor[F[_]] {
def error[A](t: Throwable): F[A]
def sync[A](thunk: => A): F[A]
def async[A](k: (Either[Throwable, A] => Unit) => Unit): F[A]
}
def foldMap[F[_]](d: Deconstructor[F]): IO[F[A]] That's obviously more powerful, but it has the advantage of allowing you to retrampoline the internals, converting a stack-unsafe |
Oh, actually an even better idea! // name tbd; I would accept something like `to`
def foldMap[F[_]: Effect]: F[A] We can define this safely, because we know that |
OK, will modify the PR. |
Or I'll just create a new one and close this. |
This PR adds a new evaluation strategy called
unsafeRunSyncOrFuture
.What it does is that it evaluates the effect and tries to return the result immediately, returning
Right(a)
in case of success, or throwing the exception in case of failure, otherwise upon hitting an asynchronous boundary it returns the result as aLeft(future)
.The primary use-case for me is that in Monix I would like to express an efficient conversion from
IO
toTask
:In absence of this, what I can do currently is this:
Well, this is less efficient than it could be because this forces 2 "light" asynchronous boundaries, one before triggering
io.unsafeRunAsync
and one before triggering the final callback ("light" means trampolinedRunnable
managed by the scheduler) for thread-safety reasons - i.e. no matter what policy we have incats-effect
forAsync.async
, this conversion in Monix will have to be stack-safe. Also because of the handling ofCancelable
, we get unnecessary work that gets executed for async stuff.In other words, it would be great if I could evaluate
IO
until either (1) an immediate result is available or (2) an asynchronous boundary is hit. And without this new operation, I cannot do it, because everything that I need isprivate
(and for good reasons).Alternative (without Future)
This is also one of those cases where usage of
Future
in the signature is superior to the alternatives and why I've told you before that I actually loveFuture
. We could have described that function like this:Unfortunately this doesn't help my use-case, because in this case you need to build your callback before that function gets invoked.
In which case we can either describe our conversion like this:
But this is awful, isn't it? And ... it doesn't help us in optimizing anything.
Or option number 2, we use some kind of placeholder, some synchronized variable meant as a temporary location for our asynchronous results:
Well, it's slightly better, but we end up creating a
Promise
reference on each invocation, whether we want it or not. And we end up using Futures anyway.In other words, please don't dismiss this just because it has
Future
in its signature 😃