Here's another way that takes inspiration from the other answers here as well as the linked folktale/task. Instead of implementing a complicated tAll
which takes care of iterating a list of tasks and combining tasks, we'll separate the concerns into individual functions.
Here's a simplified tAnd
-
const tAnd = (t1, t2) =>
{ const acc = []
const guard = (res, i) => x =>
( acc[i] = x
, acc[0] !== undefined && acc[1] !== undefined
? res (acc)
: void 0
)
return Task
( (res, rej) =>
( t1 .runTask (guard (res, 0), rej) // rej could be called twice!
, t2 .runTask (guard (res, 1), rej) // we'll fix this below
)
)
}
It works like this -
tAnd
( delay (2000, 'a')
, delay (500, 'b')
)
.runTask (console.log, console.error)
// ~2 seconds later
// [ 'a', 'b' ]
Now tAll
is a breeze to implement -
const tAll = (t, ...ts) =>
t === undefined
? tOf ([])
: tAnd (t, tAll (...ts))
Wups, don't forget to flatten along the way -
const tAll = (t, ...ts) =>
t === undefined
? tOf ([])
: tMap
( ([ x, xs ]) => [ x, ...xs ]
, tAnd (t, tAll(...ts))
)
It works like this -
tAll
( delay (2000, 'a')
, delay (500, 'b')
, delay (900, 'c')
, delay (1500, 'd')
, delay (1800, 'e')
, delay (300, 'f')
, delay (2000, 'g')
)
.runTask (console.log, console.error)
// ~2 seconds later
// [ 'a', 'b', 'c', 'd', 'e', 'f', 'g' ]
tAll
properly handles errors as well -
tAll
( delay (100, 'test failed')
, Task ((_, rej) => rej ('test passed'))
)
.runTask (console.log, console.error)
// test passed
Getting tAnd
right is surprisingly difficult, even though we've limited the scope of our program, when compared to our original tAll
. The combined task should only resolve once, or reject once - not both. This means double resolve/reject should also be avoided. Enforcing these constraints requires a bit more code -
const tAnd = (t1, t2) =>
{ let resolved = false
let rejected = false
const result = []
const pending = ([ a, b ] = result) =>
a === undefined || b === undefined
const guard = (res, rej, i) =>
[ x =>
( result[i] = x
, resolved || rejected || pending ()
? void 0
: ( resolved = true
, res (result)
)
)
, e =>
resolved || rejected
? void 0
: ( rejected = true
, rej (e)
)
]
return Task
( (res, rej) =>
( t1 .runTask (...guard (res, rej, 0))
, t2 .runTask (...guard (res, rej, 1))
)
)
}
Expand the snippet below to verify the result in your own browser -
const Task = k =>
({ runTask: (res, rej) => k (res, rej) })
const tOf = v =>
Task ((res, _) => res (v))
const tMap = (f, t) =>
Task
( (res, rej) =>
t.runTask
( x => res (f (x))
, rej
)
)
const tAnd = (t1, t2) =>
{ let resolved = false
let rejected = false
const result = []
const pending = ([ a, b ] = result) =>
a === undefined || b === undefined
const guard = (res, rej, i) =>
[ x =>
( result[i] = x
, resolved || rejected || pending ()
? void 0
: ( resolved = true
, res (result)
)
)
, e =>
resolved || rejected
? void 0
: ( rejected = true
, rej (e)
)
]
return Task
( (res, rej) =>
( t1 .runTask (...guard (res, rej, 0))
, t2 .runTask (...guard (res, rej, 1))
)
)
}
const tAll = (t, ...ts) =>
t === undefined
? tOf ([])
: tMap
( ([ x, xs ]) => [ x, ...xs ]
, tAnd (t, tAll (...ts))
)
const delay = (ms, x) =>
Task (r => setTimeout (r, ms, x))
tAnd
( delay (2000, 'a')
, delay (500, 'b')
)
.runTask (console.log, console.error)
tAll
( delay (2000, 'a')
, delay (500, 'b')
, delay (900, 'c')
, delay (1500, 'd')
, delay (1800, 'e')
, delay (300, 'f')
, delay (2000, 'g')
)
.runTask (console.log, console.error)
// ~2 seconds later
// [ 'a', 'b' ]
// [ 'a', 'b', 'c', 'd', 'e', 'f', 'g' ]
tAll
( delay (100, 'test failed')
, Task ((_, rej) => rej ('test passed'))
)
.runTask (console.log, console.error)
// Error: test passed
Serial processing
The trickiest bit is in the parallel processing requirement. If the requirements asked for a serial behavior, the implementation is dramatically easier -
const tAnd = (t1, t2) =>
Task
( (res, rej) =>
t1 .runTask
( a =>
t2 .runTask
( b =>
res ([ a, b ])
, rej
)
, rej
)
)
Implementation for tAll
stays the same, of course. Note the difference in delays now as the tasks are now run sequentially -
tAnd
( delay (2000, 'a')
, delay (500, 'b')
)
.runTask (console.log, console.error)
// ~2.5 seconds later
// [ 'a', 'b' ]
And many tasks with tAll
-
tAll
( delay (2000, 'a')
, delay (500, 'b')
, delay (900, 'c')
, delay (1500, 'd')
, delay (1800, 'e')
, delay (300, 'f')
, delay (2000, 'g')
)
.runTask (console.log, console.error)
// ~ 9 seconds later
// [ 'a', 'b', 'c', 'd', 'e', 'f', 'g' ]
Expand the snippet below to verify the results in your own browser -
const Task = k =>
({ runTask: (res, rej) => k (res, rej) })
const tOf = v =>
Task ((res, _) => res (v))
const tMap = (f, t) =>
Task
( (res, rej) =>
t.runTask
( x => res (f (x))
, rej
)
)
const tAnd = (t1, t2) =>
Task
( (res, rej) =>
t1 .runTask
( a =>
t2 .runTask
( b =>
res ([ a, b ])
, rej
)
, rej
)
)
const tAll = (t, ...ts) =>
t === undefined
? tOf ([])
: tMap
( ([ x, xs ]) => [ x, ...xs ]
, tAnd (t, tAll (...ts))
)
const delay = (ms, x) =>
Task (r => setTimeout (r, ms, x))
tAnd
( delay (2000, 'a')
, delay (500, 'b')
)
.runTask (console.log, console.error)
// ~2.5 seconds later
// [ 'a', 'b' ]
tAll
( delay (2000, 'a')
, delay (500, 'b')
, delay (900, 'c')
, delay (1500, 'd')
, delay (1800, 'e')
, delay (300, 'f')
, delay (2000, 'g')
)
.runTask (console.log, console.error)
// ~ 9 seconds later
// [ 'a', 'b', 'c', 'd', 'e', 'f', 'g' ]
tAll
( delay (100, 'test failed')
, Task ((_, rej) => rej ('test passed'))
)
.runTask (console.log, console.error)
// Error: test passed
How to implement tOr
and tRace
For sake of completeness, here's tOr
. Note tOr
here is equivalent to folktale's Task.concat
-
const tOr = (t1, t2) =>
{ let resolved = false
let rejected = false
const guard = (res, rej) =>
[ x =>
resolved || rejected
? void 0
: ( resolved = true
, res (x)
)
, e =>
resolved || rejected
? void 0
: ( rejected = true
, rej (e)
)
]
return Task
( (res, rej) =>
( t1 .runTask (...guard (res, rej))
, t2 .runTask (...guard (res, rej))
)
)
}
Which resolves or rejects the first-to-complete of two tasks -
tOr
( delay (2000, 'a')
, delay (500, 'b')
)
.runTask (console.log, console.error)
// ~500 ms later
// 'b'
And tRace
-
const tRace = (t = tOf (undefined), ...ts) =>
ts .reduce (tOr, t)
Which resolves or rejects the first-to-complete of many tasks -
tRace
( delay (2000, 'a')
, delay (500, 'b')
, delay (900, 'c')
, delay (1500, 'd')
, delay (1800, 'e')
, delay (300, 'f')
, delay (2000, 'g')
)
.runTask (console.log, console.error)
// ~300 ms later
// 'f'
Expand the snippet below to verify the results in your own browser -
const Task = k =>
({ runTask: (a, b) => k (a, b) })
const tOr = (t1, t2) =>
{ let resolved = false
let rejected = false
const guard = (res, rej) =>
[ x =>
resolved || rejected
? void 0
: ( resolved = true
, res (x)
)
, e =>
resolved || rejected
? void 0
: ( rejected = true
, rej (e)
)
]
return Task
( (res, rej) =>
( t1 .runTask (...guard (res, rej))
, t2 .runTask (...guard (res, rej))
)
)
}
const tRace = (t = tOf (undefined), ...ts) =>
ts. reduce (tOr, t)
const delay = (ms, x) =>
Task (r => setTimeout (r, ms, x))
tOr
( delay (2000, 'a')
, delay (500, 'b')
)
.runTask (console.log, console.error)
// ~500 ms later
// 'b'
tRace
( delay (2000, 'a')
, delay (500, 'b')
, delay (900, 'c')
, delay (1500, 'd')
, delay (1800, 'e')
, delay (300, 'f')
, delay (2000, 'g')
)
.runTask (console.log, console.error)
// ~300 ms later
// note `f` appears in the output first because this tRace demo finishes before the tOr demo above
// 'f'
tRace
( delay (100, 'test failed')
, Task ((_, rej) => rej ('test passed'))
)
.runTask (console.log, console.error)
// Error: test passed
How to implement tAp
In the comments, we're talking about applicative, tAp
. I think tAll
makes the implementation rather easy -
const tAp = (f, ...ts) =>
tMap
( ([ f, ...xs ]) => f (...xs)
, tAll (f, ...ts)
)
tAp
accepts a task-wrapped function and any number of task-wrapped values, and returns a new task -
const sum = (v, ...vs) =>
vs.length === 0
? v
: v + sum (...vs)
tAp
( delay (2000, sum)
, delay (500, 1)
, delay (900, 2)
, delay (1500, 3)
, delay (1800, 4)
, delay (300, 5)
)
.runTask (console.log, console.error)
// ~2 seconds later
// 15
Unless the tasks have a side effect, I cannot see a reason why a "parallel" implementation of tAp
breaks the applicative laws.
Expand the snippet below to verify the results in your own browser -
const Task = k =>
({ runTask: (res, rej) => k (res, rej) })
const tOf = v =>
Task ((res, _) => res (v))
const tMap = (f, t) =>
Task
( (res, rej) =>
t.runTask
( x => res (f (x))
, rej
)
)
const tAp = (f, ...ts) =>
tMap
( ([ f, ...xs ]) => f (...xs)
, tAll (f, ...ts)
)
const tAnd = (t1, t2) =>
{ let resolved = false
let rejected = false
const result = []
const pending = ([ a, b ] = result) =>
a === undefined || b === undefined
const guard = (res, rej, i) =>
[ x =>
( result[i] = x
, resolved || rejected || pending ()
? void 0
: ( resolved = true
, res (result)
)
)
, e =>
resolved || rejected
? void 0
: ( rejected = true
, rej (e)
)
]
return Task
( (res, rej) =>
( t1 .runTask (...guard (res, rej, 0))
, t2 .runTask (...guard (res, rej, 1))
)
)
}
const tAll = (t, ...ts) =>
t === undefined
? tOf ([])
: tMap
( ([ x, xs ]) => [ x, ...xs ]
, tAnd (t, tAll (...ts))
)
const delay = (ms, x) =>
Task (r => setTimeout (r, ms, x))
const sum = (v, ...vs) =>
vs.length === 0
? v
: v + sum (...vs)
tAp
( delay (2000, sum)
, delay (500, 1)
, delay (900, 2)
, delay (1500, 3)
, delay (1800, 4)
, delay (300, 5)
)
.runTask (console.log, console.error)
// ~2 seconds later
// 15
Promise.all
does.Promise.all
doesn't execute anything. By the time you're usingPromise.all
, the operations in question have already been started. AllPromise.all
does is wait for them to complete, gathering up the results. – RichelleEither
type. My statement was imprudent in connection withPromise
s. – ImpracticableawaitAll
, at least with the current implementation. aTask
as far as I can tell above isn't necessarily asynchronous. Some are, but not all. So ifawaitAll
's job is to start the tasks and run them in parallel, it has the problem that some tasks are synchronous. The only way I see out of that is to require that all tasks be asynchronous, in which case the implementation is fairly trivial: Loop through the tasks starting them and hooking their completion, then fail if any of them fails, or filling in the task's slot in the result array if it works. – RichelleTasks
are functions that take a (binary) continuation and the monad describes how to compose such functions. So theTask
type deals with functions that take continuations (callbacks) and I guess it doesn't really matter if such functions are asynchronous or not.Task
just abstracts from the callback pattern. – Impracticablethrow
from the same stack thatawaitAll
is on, in which case you might have to implement redundant error handling. – Mescalmap
won't complete until the task has finished, holding up other tasks. (Mind you: Unless they're going to be offloaded to a worker thread, they're going to hold up other tasks at some point.) – RichelleTasks
, if you need non-blocking behavior. – ImpracticabletAp
andtChain
, so why do it forawaitAll
? – ZymogenicawaitAll
could be just a rather trivial applicative computation. However,tAp
doesn't run its effects in parallel. Maybe the reason is that it takes its applicative arguments sequentially. But that is what applicative functors do, right? – Impracticable