You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
The text was updated successfully, but these errors were encountered:
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
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
in2.0-M1
was simply counting the synchronous calls done toTask.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 lazyCons
based stream whosetail
would be evaluated byTask
(similar with Scala'sStream
, but with aTask
instead of a by-name param). The implementation of such a stream is heavy on recursive calls and theTask
wasn't up to the challenge, even though in simple tests it was fine.The solution: the
Task
was redesigned internally, inspired by theFree
monad, to use a trampoline for synchronous execution, while also forking logical threads by means of theScheduler
when asynchronous boundaries are reached. The result are glorious, as all the goodies I promised inTask
are there and it works marvelously.Inspired by
cats.Eval
, I also introducedCoeval
, for expressing evaluations that can be processed synchronously, a sort of less capable alternative to task that doesn't need asynchronous boundaries. And convertion betweenCoeval
andTask
is seamless. In the initial implementationTask
was actually a subtype ofCoeval
(asCoeval
is like aTask
but with extra restrictions). But inheritance / sub-typing can be problematic, soCoeval
ended up being a different type. But you can abstract over bothTask
andCoeval
because both are instances of the newmonix.types.Evaluable
type-class.As part of this process I renamed the
monix.async
package tomonix.eval
. BecauseCoeval
isn't async and now themonix-eval
subproject isn't about asynchronous things anymore, but about lazy evaluation.Even better, you can now pick the
Scheduler.executionModel
forTask
. You can choose to prefer synchronous execution (only for as long as possible, so synchronousflatMap
loops remain synchronous), you can choose to fork on each operation (like Scala'sFuture
), 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 from2.0-M1
).The text was updated successfully, but these errors were encountered: