Skip to content
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

Reimplement Task from scratch, introduce Coeval, introduce Scheduler.executionModel #141

alexandru opened this Issue Apr 8, 2016 · 1 comment


None yet
1 participant
Copy link

alexandru commented Apr 8, 2016

The implementation Task introduced as part of #88 is inadequate.

The RunLoop implementation piggybacks on top of the JVM's normal call-stack, by doing actual recursive calls upon reaching a limit and then forcing an asynchronous boundary, in order to avoid stack-overflows.

The problem: But this turns out to be error prone. This is because it's not possible to estimate the stack depth. The RunLoop in 2.0-M1 was simply counting the synchronous calls done to Task.unsafeRun. But if for each of those calls you go another 5-10 levels on that call stack by calling some regular functions outside of the Task implementation, you can easily blow the call-stack by not reaching the limit. This problem happened while trying to implement a lazy Cons based stream whose tail would be evaluated by Task (similar with Scala's Stream, but with a Task instead of a by-name param). The implementation of such a stream is heavy on recursive calls and the Task wasn't up to the challenge, even though in simple tests it was fine.

The solution: the Task was redesigned internally, inspired by the Free monad, to use a trampoline for synchronous execution, while also forking logical threads by means of the Scheduler when asynchronous boundaries are reached. The result are glorious, as all the goodies I promised in Task are there and it works marvelously.

Inspired by cats.Eval, I also introduced Coeval, for expressing evaluations that can be processed synchronously, a sort of less capable alternative to task that doesn't need asynchronous boundaries. And convertion between Coeval and Task is seamless. In the initial implementation Task was actually a subtype of Coeval (as Coeval is like a Task but with extra restrictions). But inheritance / sub-typing can be problematic, so Coeval ended up being a different type. But you can abstract over both Task and Coeval because both are instances of the new monix.types.Evaluable type-class.

As part of this process I renamed the monix.async package to monix.eval. Because Coeval isn't async and now the monix-eval subproject isn't about asynchronous things anymore, but about lazy evaluation.

Even better, you can now pick the Scheduler.executionModel for Task. You can choose to prefer synchronous execution (only for as long as possible, so synchronous flatMap loops remain synchronous), you can choose to fork on each operation (like Scala's Future), or you can choose to introduce artificial async boundaries in loops once every X iterations, in order to preserve liveliness (the default and the original Task behavior from 2.0-M1).

@alexandru alexandru added this to the 2.0 milestone Apr 8, 2016

@alexandru alexandru self-assigned this Apr 8, 2016


This comment has been minimized.

Copy link
Member Author

alexandru commented Apr 8, 2016

Shipped in 2.0-M2.

@alexandru alexandru closed this Apr 8, 2016

@alexandru alexandru changed the title Redesign Task from scratch, introduce Coeval, introduce Scheduler.executionModel Reimplement Task from scratch, introduce Coeval, introduce Scheduler.executionModel Apr 8, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.