0.17 Tasks vs Cmds

1,911 views
Skip to first unread message

James Wilson

unread,
Apr 28, 2016, 4:02:15 AM4/28/16
to elm-dev
Having a brief look through the APIs, I noticed that some things, for example Time.now, come back as Tasks, so you can chain them nicely using the Task API before converting them into commands for the runtime to handle, and others, for example Random.generate hand back Cmds, which can't be composed in the same way.

I have not had a chance to play around properly, but my gut would be that almost everything in the API should return a Task, so that you can compose them nicely before turning into commands. A very few things, like Websockets, one could argue have no meaningful response to compose with other tasks (though perhaps I want to run some tasks after I have sent a socket message (dubious!).

Is there a reason for the difference in APIs? I am relatively new to Elm so I might be missing something glaringly obvious :)

Just as a general thing, 0.17 is a very exciting release for mel, and I'd like to thank everyone involved for the awesome work!

James Wilson

unread,
Apr 28, 2016, 5:31:53 AM4/28/16
to elm-dev
Thinking about this more, and from having a bit of a look at the websocket code, I get the impression (might be wrong) that custom effect managers can tie into the Cmd/Sub model but cant allow for the creation of custom composable Tasks. I can see the challenge here (some things might not return - though you could enforce that a return value is provided and have it be Unit in those cases?), but it would be amazing to have the composition that Tasks provide us (much like Promises in Javascript) combined with a way to hook in custom effects via effect managers. *ahem* anyway just thinking out loud :)

Luke Westby

unread,
Apr 29, 2016, 5:10:52 PM4/29/16
to elm-dev
I think you can get that composability you are looking for using `Cmd.map` and `Task.perform`, although it might not be as clear as when using `Task.andThen`. For example, if you wanted to grab a random int and then convert that into a request for a random amount of padding on a string via http://left-pad.io/ you could do:

type Msg
  = PaddingSuccess String
  | PaddingFailed

decoder : Decode.Decoder String
decoder =
  Decode.at ["str"] Decode.string

randomIntCmd : Cmd Int
randomIntCmd =
  Random.generate identity <| Random.int 0 10

randomPaddingRequest : Int -> Cmd Msg
randomPaddingRequest amount =
  Http.get decoder ("https://api.left-pad.io/?str=hello&ch=!&len=" ++ (toString amount)) 
    |> Task.perform (always PaddingFailed) PaddingSuccess

randomRequestCmd : Cmd Msg
randomRequestCmd =
  Cmd.map randomPaddingRequest randomIntCmd

Too be clear, this is just a demonstration that Cmds can be composed, I'm not sure whether using `identity` as a Cmd tagger is an anti-pattern or if there's a better way to compose Cmds coming from different managers.

Geoff H

unread,
Apr 29, 2016, 5:40:35 PM4/29/16
to elm-dev
The problem is that Cmd is not monadic so the type of randomRequestCmd will be Cmd (Cmd Msg) rather than Cmd Msg.

Luke Westby

unread,
Apr 30, 2016, 9:54:29 AM4/30/16
to elm-dev
Oh shoot, you're absolutely right! I must have left off the type annotations when compiling the example =(

Sorry James!

Frank Bonetti

unread,
Dec 6, 2016, 7:13:10 PM12/6/16
to elm-dev
> Having a brief look through the APIs, I noticed that some things, for example Time.now, come back as Tasks, so you can chain them nicely using the Task API before converting them into commands for the runtime to handle, and others, for example Random.generate hand back Cmds, which can't be composed in the same way.

I ran into this issue recently. I wanted to get a random number and feed it into `Process.sleep`, but I couldn't because `generate` returns a Cmd instead of a Task. It's possible to work around this limitation but the code ends up being really clunky.

Does anyone know why Random is designed this way? Would anyone else like to see `generate` return `Task Never a` instead of `Cmd a`?

Max Goldstein

unread,
Dec 6, 2016, 10:23:08 PM12/6/16
to elm-dev
Does anyone know why Random is designed this way?

Random numbers were originally generated by explicitly managing seeds (which are still available and that's the workaround). Evan wanted to make Random much easier to use so commands entered the picture. I don't think the intermediate solution of a task has ever come up.
 
Would anyone else like to see `generate` return `Task Never a` instead of `Cmd a`?

On the grounds of, everything effectful should be available as a task so we have a common language to compose them and don't have to think about Cmd.andThen, I'm on board. On the grounds of, don't speculatively add API, wait until there's a concrete use case, I'm a bit more concerned. Why do you need to generate random numbers with Process.sleep? 

Evan Czaplicki

unread,
Dec 7, 2016, 11:52:36 AM12/7/16
to elm-dev
You can do something like:

Time.now and then generate the random number and then Process.sleep

Basically, you can generate a seed like in other languages and go from there.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elm-dev/f0be0899-a75e-40a3-af1b-966fba2526b2%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Mark Hamburg

unread,
Dec 7, 2016, 12:43:31 PM12/7/16
to elm...@googlegroups.com
Commands are decidedly inferior to tasks when it comes to structuring computations in that while they batch they do not compose. Does this matter? I would say yes because this forces any sequenced computation to run through a state machine in the model and that will generally be much more spread out in the code — and hence harder to maintain — than a simple task chain.

Evan used to talk about tasks as being like JavaScript promises only "better". Since 0.17, there is a heavy push toward doing things as commands which feel decidely worse. Yes, they are simpler and for cases where we need to route the results Cmd.map is probably better because it operates more generically. But for doing real work, they are decidedly worse.

For the case cited in this thread, consider wanting to sleep for a random length of time and then receive a message. We need to get a random number — requested via a command and delivered via a message — and then generate the sleep task and generate the response. That means that the model now needs to respond to a message in its update function to take the result of the random number request and do the rest of the work. That message doesn't arrive at the time when the model was in the condition where it wanted to start the random delay but rather at some time later. Hence, that's a potential testing nightmare leading to sequence dependent bugs that are potentially difficult to reproduce.

Mark

Evan Czaplicki

unread,
Dec 7, 2016, 1:48:46 PM12/7/16
to elm-dev
Commands don't stop you from using tasks. That's what I was trying to say in my message before.

Here's the code I was hoping folks would figure out from the description above:

randomSleep : Time -> Time -> Task x a
randomSleep lo hi =
  Time.now
    |> Task.map (\time -> Random.step (Random.float lo hi) (Random.initialSeed (round time))
    |> Process.sleep

I believe this is the kind of thing that Math.random does in JavaScript behind the scenes.

Mark Hamburg

unread,
Dec 7, 2016, 2:22:37 PM12/7/16
to elm...@googlegroups.com
But contrast this with what this would look like if we had:

Random.task : Generator a -> Task Never a

Then we could write:

randomSleep : Time -> Time -> Task x a
randomSleep lo hi =
  Random.task (Random.float lo hi)
    |> Task.andThen Process.sleep

Presumably, the point of Random.generate is to make random numbers easier to use because it encapsulates the seed in the effects manager. If we could access this same functionality via tasks, that would make it easier to compose and in this case would spare the code from needing to generate and use its own seed.

There may or may not be pressure to eliminate commands, but there's a pretty strong pressure that crops up with things like this and web sockets and probably other effects managers to say that anything exposed as a command should probably be exposed as a task as well. If this pressure doesn't exist, then why does Window provide a Task-based rather than Command-based interface?

Mark

Yosuke Torii

unread,
Dec 7, 2016, 2:26:56 PM12/7/16
to elm...@googlegroups.com
Ohhh, nice. This is what I was looking for!

Time.now
    |> Task.map (\time -> Random.step (Random.float lo hi) (Random.initialSeed (round time))

But, how many users can reach there? This looks a bit tricky and needs some additional knowledge.

Also, I'm curious if not having Task API in Random leads better programming or not. Is chaining random a bad practice? Is this intentionally avoided? (I know if Elm lacks some functionalities, there is a reason. I'm also agree with Max that there should be a concrete use case.)


Evan Czaplicki

unread,
Dec 7, 2016, 2:34:21 PM12/7/16
to elm-dev
I considered adding it to Random with the 0.17 changes, but I wasn't sure about how these functions typically work in imperative languages. Specifically, do JS, Java, etc. use a new seed on each call? Or does it make one and keep stepping it?

I also assumed folks would see how to make a task if they wanted. I am quite familiar with this module though, so I guess I was wrong about that.

So I'd want to get a link that answers the question about using new seeds or stepping the same one.

Evan Czaplicki

unread,
Dec 7, 2016, 2:36:33 PM12/7/16
to elm-dev
Also, I just wrote the code for how to define Random.task in my email. Part of why I left it out was because I knew it could be defined pretty easily and I could make the final decision about how it'd work in core later.

Roland Kuhn

unread,
Dec 7, 2016, 2:39:42 PM12/7/16
to elm-dev
The most efficient is to use one persistent generator per thread (https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadLocalRandom.html). Seeding anew on every call may be used (with https://docs.oracle.com/javase/7/docs/api/java/util/Random.html), but is not the best way.

Regards, Roland

To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elm-dev/CAF7GuPGMTCQf3czFa0oFZT4VXHzOa6MDx2Ug%2BD9PEtkeXqc8ng%40mail.gmail.com.

Yosuke Torii

unread,
Dec 7, 2016, 2:51:44 PM12/7/16
to elm...@googlegroups.com
I see. Thanks for clarifying it, Evan!

Mark Hamburg

unread,
Dec 7, 2016, 5:14:13 PM12/7/16
to elm...@googlegroups.com
I assume meaning fetch the time, make a seed, etc which works but as noted later in the thread isn't necessarily viewed as best practice. In particular, it would fail badly if operating on a machine that was so fast that successive requests for the time might return the same number.

Mark

P.S. The case that I ran into in this regard the other week had nothing to with random but rather stemmed from a co-worker wanting to join a Phoenix channel, post a message, wait for the response, and leave the channel. Since in the community Phoenix library join, post/wait, and leave are all commands one can't readily string them together without running a state machine to manage this inside ones model. (Okay. I can imagine hacks based around an "AndThen" message which just computes a new command but these get ugly from a type system standpoint.) The Phoenix library uses commands because it sits on top of the web sockets library which uses commands.

James Wilson

unread,
Dec 7, 2016, 7:24:04 PM12/7/16
to elm...@googlegroups.com
On a related tangent, this composability of Tasks is also why, a while ago, I proposed the idea of Task ports, which would essentially be ports that have a Task type signature rather than a Cmd/Sub, with the idea that the JS side would return immediately (or via a callback/Promise) when invoked. This would allow us to hook Javascript into elm in a much more natural way in many places, and their Tasky nature in Elm would allow for that same level of composability. My main motivation was to allow myself to make shims for the various things that just don’t exist in Elm yet, as well as hooks to external libraries that contain one-shot functions (eg generating a random value, or obtaining a response from a backend) rather than the more stream-like things that the Cmd/Sub port interface is suited for.

Having such a feature would also mean that, when an Elm implementation of the same library/API comes along (eg Web APIs which I believe would be implemented at a relatively low level as Tasks before being built upon), it would be super easy to switch from Shim to pure Elm. This would overcome possibly the largest roadblock I have with Elm currently, which is the lack of support for many Web APIs which force a less-composable and often less-well-suited port interface instead.

James

You received this message because you are subscribed to a topic in the Google Groups "elm-dev" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-dev/MEBzD3f7Bq8/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-dev+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elm-dev/CAFdvD8Xa1JyK6q-3bGE1f_L4ABmtxayXU8MAZQ%2B3tdgDcSJayQ%40mail.gmail.com.

Jakub Hampl

unread,
Dec 8, 2016, 8:23:54 AM12/8/16
to elm-dev
Would it make sense to define the inverses of Task.perform and Task.attempt for solving this problem generically?

toSucceedingTask : ((a -> msg) -> Cmd msg) -> Task Never a

toTask : ((Result x a -> msg) -> Cmd msg) -> Task x a
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

Max Goldstein

unread,
Dec 8, 2016, 11:28:47 AM12/8/16
to elm-dev
I doubt it's possible, but if it is, that would be a great way to assure that one can always fall back to tasks if necessary.

Fedor Nezhivoi

unread,
Dec 8, 2016, 11:44:24 AM12/8/16
to elm-dev
If we would have conversions in both ways (Task <-> Cmd) then why would we have 2 things at all? Wouldn't it be better to just use more powerful one?

Sorry if the question is stupid.
On Thu, 8 Dec 2016 at 18:28, Max Goldstein <maxgol...@gmail.com> wrote:
I doubt it's possible, but if it is, that would be a great way to assure that one can always fall back to tasks if necessary.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.
--
Best regards,
Fedor Nezhivoi

Frank Bonetti

unread,
Dec 9, 2016, 2:37:31 PM12/9/16
to elm-dev
> If we would have conversions in both ways (Task <-> Cmd) then why would we have 2 things at all? Wouldn't it be better to just use more powerful one?

I think this is a really interesting thing to bring up. Please correct me if I'm wrong, but I conceptually view Tasks as computation builders and Cmds as Task runners. The only purpose of a Cmd is to execute something externally/asynchronously and then return a Msg. Why do we need a separate data type to do this? Imagine if we just used tasks instead:

Html.program

program :
  { init : (model, List (Task msg msg))
  , update : msg -> model -> (model, List (Task msg msg))
  , subscriptions : model -> Sub msg, view : model -> Html msg
  } -> Program Never model msg


If we removed Cmds, we wouldn't need Task.perform or Task.attempt at all. We could instead use map and mapError to produce tasks of type Task msg msg.

myTask : Task Msg Msg
myTask =
    |> Http.toTask
    |> Task.map HandleSuccess
    |> Task.mapError HandleFailure


I don't know if this is a better approach, but it's worth asking if Cmds are necessary when we are already have Tasks.

Mark Hamburg

unread,
Dec 9, 2016, 3:09:49 PM12/9/16
to elm...@googlegroups.com
You beat me to it on posting this suggestion. That said, I might define Cmd msg as an alias for either Task msg msg or Task Never msg — I'm not sure which without writing more code.

Mark

Steve Schafer

unread,
Feb 23, 2017, 5:30:05 PM2/23/17
to elm-dev
I'd like to add my two cents regarding a real-world use case where Tasks would be easier than Cmds: I'm building a web app where a dataset is stored in encrypted form on a remote server (much as you would have with a typical cloud storage system). To enable sharing of the data, the dataset is encrypted with a symmetric key, and that key is in turn encrypted with the asymmetric public keys of whoever is authorized to have access to that dataset. Those encrypted keys are stored on the server along with the encrypted dataset.

So, as a user, to fetch a dataset:
  1. Issue a GET request to retrieve the dataset plus the dataset decryption key that has been encrypted with my public key.
  2. Call out to a JS crypto library to decrypt the encrypted dataset decryption key with my private key.
  3. Call out to the JS crypto library to decrypt the encrypted dataset with the freshly decrypted key.
Each of these three steps is a Cmd. If they were Tasks, I would of course be able to string them together directly. Instead, I have to pass them through the update loop via two additional Msgs whose sole purpose is to maintain intermediate pseudo-states that will never be used anywhere else.

(It's true that the second and third steps could be combined into one, but then I'm writing application logic in JavaScript instead of Elm, which kind of defeats the purpose.)

-Steve

To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

-- 
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

d...@pisys.eu

unread,
Feb 25, 2017, 4:11:07 PM2/25/17
to elm-dev
i have a related issue. In my case it's Elm running inside node (Electron) and accessing LevelDB. Since LevelDB runs as part of the node process my app cannot communicate via HTTP. Instead I use ports and hence I'm forced to have commands for the async parts. Before 0.17, ports yielded Tasks which was better, in my view.
Reply all
Reply to author
Forward
0 new messages