-
Notifications
You must be signed in to change notification settings - Fork 102
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
Implements Data.Task #50
Conversation
This allows people to use Futures as a data structure directly, instead of Promises. The usage of Futures is pretty low level, however, since one has to acquire a Deferred first, and then settle that deferred in some resolution state.
This gives us async/await
matchWith has the expectation that the pattern matching is performed in the current snapshot of the data structure, and returns whatever the provided branch function does. Futures can't do that since they might not be resolved yet, and matching on pending snapshots leads to awkwardly easy race conditions.
This allows these things to be chained.
Why are we using |
Mmh, because The Semigroup instance was removed for similar reasons: it's not deterministic. Both of those operations were moved to different methods, so they're still there, their behaviour when composed with arbitrary expressions is just less confusing. For concurrent executions, where you had: const a = delay(100).map(x => y => f(x, y));
const b = delay(120);
const value = await a.ap(b).run().promise(); You'd now have: const a = delay(100);
const b = delay(120);
const value = await a.and(b).map(([x, y]) => f(x, y)).run().promise(); For Semigroup, where you had: const a = Task.empty();
const b = delay(100);
const value = await a.concat(b).run().promise(); You'd now have: const a = Task.task(resolver => {});
const b = delay(100);
const value = await a.or(b).run().promise(); |
We can provide a ParallelTask object that always combines tasks in Parallel, though, so it's always an Applicative, rather than a monad, that way you'd be able to write: const a = delay(100).map(x => y => f(x, y));
const b = delay(120);
const value = await Parallel(a).ap(Parallel(b)).run().promise(); |
Awesome work, robotlolita! I particularly like the idea to have an API, that is somewhat similar to the one of the other ADT's ( Also nice that Tasks can be converted to promises. I imagine that I can write one module of my app in a pure setting using Tasks, and convert them to promises from another, impure module. One question: what is the point of the |
@boris-marinov hm, you'd probably want to have a As for the TaskExecution, its main purpose is to control who can and can't The idea is that you'd mostly pass |
I think we should at least have About a.and(b).and(c).and(d).map(([[[a, b], c], d]) => ...) And as we are using array what would be it's type? and :: Task e a ~> Task e b -> Task e (Array ?) |
@safareli yeah, there'll be a As for the types, even though it and :: forall a, b, c: (Task a b).(Task a c) => Task a (b, c) Where |
The new implementation of Data.Task, which aims to be more lawful, make automatic resource management better, and allows one to take advantage of
async/await
with promises.Tasks are now divided in many components:
Task
— represents any asynchronous action, together with how resources are allocated and collected;TaskExecution
— represents the execution of a Task, allowing that execution to be cancelled, or its value to be depended on (which means you get memoisation for free here);Future
— represents the eventual result of running a task. This might be a successful resolution, a rejection, or a cancellation;Deferred
— a low-level mechanism for asynchronously providing values to futures. These are usually used under-the-hood, but they're exposed to user code as well.Creating tasks
In order to construct asynchronous actions, users would use Task, as before. The new
task
function used to construct Tasks has a different signature however:So
task
now takes a computation, which is a one-argument function taking aresolver
object, which provides the methodsresolver.resolve(value)
,resolver.reject(reason)
, andresolver.cancel()
. The computation returns an object that tracks which resources it allocated in the process, and which must be collected when the task finishes executing or is cancelled.The second argument to
task
is an object which may provide any of the two optional methods (Task falls back to a noop if they aren't provided), both are invoked with the resources allocated by the computation, and both are invoked asynchronously.The
onCancelled
method is invoked if the task is cancelled, and is always invoked beforecleanup
. Because attempting to resolve a Task that has been cancelled is an error, Task authors must take special care to handle cancellations in their code.The
cleanup
method is invoked after the task is settled (either by cancelling, rejecting, or resolving), and should collect any resources allocated by the task. In the example above, the only resource allocated is a timer, so the timer gets collected.Running tasks
Tasks are now all ran with the new
run()
method. This returns a TaskExecution object, which has methods to deal with the execution of that task. In order to get the final value of a Task, one has to either get a Promise or a Future for the execution. Promises allow one to use async/await, but cancellations are handled as rejections, and objects with a.then
method have to be wrapped in something so they don't get assimilated by the promise:Futures are safer, and also allow one to work with them as monads, but they don't get a special syntax:
Finally, executions can be cancelled by calling
.cancel()
. Cancellation is always "safe", meaning it won't throw an exception if the execution has already settled.Combining tasks
For now, Tasks can be combined with the
or
andand
methods. The first selects the first task to resolve, while the latter waits both tasks:This closes #17