Task.map2 and independent asynchronous operations


#1

According to current documentation, Task.map2 (and other mapN functions) combine tasks such that they “run in order so the first task will be completely finished before the second task starts”. In general, such constraint is not imposed by type of this function, i.e. the same function could have semantics to run provided tasks in parallel.

Are these current semantics intentional? Or is this, maybe, constrained by the WIP Process library (section Future Plans) in which case it would be reconsidered once the Process library reaches stability? Is it possible to implement (a -> b -> result) -> Task x a -> Task x b -> Task x result function type, with parallel semantics, without relying on native code, right now?

Related issues:


Warning! Reference to Haskell-like FP abstractions ahead. If you are not interested in this kind of content, please skip reading.

Task could be seen as an instance of Applicative typeclass, and map2 function could be implementation of liftA2 function type. In general, Applicative describes combining independent computations, so in case of Task that would mean running asynchronous processes in parallel. For example Task from the Monix scala library is an instance of Applicative, and it’s function family zipMapN have parallel semantics.
I believe parallel semantics of Task.map2 function would be more inline with similar abstractions in FP community.


#2

If you look at the source code for Task.map2 you’ll see that it’s built on top of Task.andThen, which is known as the monadic bind or >>= in Haskell.

So it already is an applicative. It’s just that the semantics of andThen are sequential.

Elm does have monads and applicatives but tends to focus on how to use individual instances of those rather than pushing people to learn the full general concept before starting to get things done. An applicative in Elm is “a type that has map2, map3 etc.”

However Cmd.batch doesn’t have sequential semantics so you can use that instead.


#3

Task is roughly equivalent to IO in Haskell. Looking at the IO instance that defines liftA2 here, you see that it is sequential there as well (liftA2 = liftM2). This is because Task and IO are both general to all kinds of effects. Having things go in parallel is often acceptable for HTTP, but it would be quite a problem for reading/writing files.

Adding a way to run tasks in parallel may be nice, but having it be the default for map2 means that a Task will be quite confusing if it is ever used for effects that are not naturally independent. (And even with HTTP, it is not always the case that requests are independent!)


#4

Is there a way to map two independent HTTP requests that are done in parallel, then return a Msg currently? I have a use case for that where they really can and should be parallel, if possible.


#5

The path for now is to use two different commands.

It is conceivable that there should be an abstraction for HTTP that accounts for parallel requests that should happen together, but it then becomes much more complicated to (1) track errors and (2) track progress. I have tried to do it, but to fully account for these kinds of situations, I have found that it complicates the simple cases quite a bit. My current feeling is that it might want an abstraction of its own, and the starting point there is to get a couple real examples where people feel this might be the best path.