Closed bradparker closed 8 years ago
Hi @bradparker. I'm not sure that the purpose of Task#concat
is to alias Task.of(concat).ap(Task.of(...)).ap(Task.of(...))
(where concat
is a method from something like Ramda that dispatches to concat
on objects).
As I understand your question, that's the way you're hoping to use it. Please correct me if that's wrong!
The purpose of Task#concat
(I think) is to provide the value of the first Task to resolve
Hm, so, there are many ways of implementing a .concat
method for Task. For the one in the library we've chosen non-deterministic concurrency as the "most likely use case for this". This will probably change in the redesign of the library (https://github.com/origamitower/folktale/issues/5), and .concat will probably do deterministic concurrency instead (which is similar to what you propose), renaming the current function to race
or something.
This basically means that Task#concat
is implemented today such that:
delay(10).concat(delay(1000)) // ==> delay(10)
delay(1000).concat(delay(10)) // ==> delay(10)
So it just selects the task that finishes first, and propagates that value. This is pretty much the same behaviour as Promise.race
in the standard JS library.
Could we have implemented Task#concat
differently? Sure. We could have instead treated the contents of the tasks as semigroups. This would require people to make sure the value the task resolves to has a .concat
method:
Task.of([1]).concat(Task.of([2])) // ==> Task.of([1, 2])
Task.of(1).concat(Task.of([2])) // ==> "No method `.concat` for 1"
This is also a reasonable approach. I think .ap
addresses that use case right now, and also allows you to decide what "concatenation" means, so you can handle values that are not necessarily semi-groups, although it takes a bit more of effort:
const t1 = Task.of(1);
const t2 = Task.of(2);
Task.of(a => b => [a, b]).ap(t1).ap(t2) // ==> Task.of([1, 2])
The problem with deciding between one or the other is that JavaScript only really allows you to have one implementation (you need wrappers for anything else), and for this library we thought having a behaviour like Promise.race
would be more useful (you can see the discussion we had here: https://github.com/folktale/data.task/pull/15). Also because writing a Monoid instance requires either return-type polymorphism (to select the proper type to fill the Task with), or converting Task to a free monoid (which is pretty cool, but also pretty complicated: http://lambda-the-ultimate.org/node/5109).
core.async
does indeed provide a few more convenient utility methods over these. With the redesign I want to make that more convenient to use, and more reliable. But that's still going to take a couple of months.
So glad I asked!
Task#ap is exactly it, I just didn't see it, thanks for the explanation. It allows for more interesting methods for combining values, without having to write a wrapper with a specific implementation of concat. You could provide a function which merges arrays of resources based on an identifier, adds fields calculated from one result to the records in another... Perfect.
Thanks again :)
N.B. I'm not at all familiar with this style of programming. But I'm trying to learn :)
The logic might be similar to
Task#ap
but rather than applying a function, it calls some concatenating method with both resolved values (provided they both resolve).You could even say that values in concat-able Tasks have to themselves be members of Semigroup?
So any value takes care of its own implementation of concat:
... being that will only work out-of-the-box for Arrays and Strings you could alternatively provide the function used for the concatenation in the call to Task#concat?
I'm not thrilled with the above... it'd be pretty easy to wrap network responses in arrays when needed, and alternatively leave completed file-reads as Strings.
I see that my specific use-case is covered by
core.async
but I noticedconcat
in the source and thought I'd ask.Thanks for the cool and thought-provoking lib :)