The official guide is great for most things in Elm 0.18, but it still doesn’t cover a very important feature of the language:
Task. With the introduction of Elm 0.17, use cases for tasks shrunk from an everyday occurrence to a “you most likely don’t need this” status.
The use case that does remain is tying several side-effects together. A common example of this is when people need a time stamp to go with their HTTP request.
Cmds cannot be set to run one after another. One could add a subscription to
Time.every (Time.second) and have the current time always in the model for example, but a whole bunch of requests can happen in a second, and a millisecond timer is just out of the question. So how do we solve this? Tasks.
Let’s first take a look at how tasks work on their own, before we start bundling them up with each other. One of the simplest tasks in
elm-lang/core just so happens to relate to the example above:
Time.now. Here’s what the current documentation says about it:
now : Task x Time
Time at the moment when this task is run.”
If you haven’t been dealing with tasks before, you might find it strange that this is not a function that returns a
Cmd msg. In fact, it is not a function at all but a constant value! What is going on?
I find it helpful to think of tasks as if they were shopping lists. A shopping list contains detailed instructions of what should be fetched from the grocery store, but that doesn’t mean the shopping is done. I need to use the list while at the grocery store in order to get an end result. Similarly, the
Time.now task is an instruction for the Elm runtime to find the current time, but the instruction does not do anything until it is turned into a command and returned in a branch of
update. Elm is running all the errands with the outside world here, we only need to make sure the command gets returned.
So to use a task, we need to turn it into a command. There are two ways to do this:
Task.attempt. As you might guess from the naming,
perform simply does the thing, while
attempt has an expectation of failure involved. In our case,
Time.now cannot really fail, so let’s use perform.
import Time exposing (Time) import Task type Msg = TimeUpdated Time getTime : Cmd Msg getTime = Time.now |> Task.perform TimeUpdated
We can use the
getTime command just like any other now and when it completes, it results in a
TimeUpdated message with the current time. Great!
What about tasks that can fail? Let’s use
Http.get as an example. There are plenty of ways for an HTTP request to fail, ranging from network issues to expired authentication tokens. These are all categorized under the
Http.Error type. If you’ve made HTTP calls in Elm before, you have probably used
Http.send to convert the request to a
Cmd Msg. Instead of doing that, let’s take a look at how we can do the same using
Http.toTask. For simplicity’s sake we will use
getString, which does not need a decoder.
import Http import Task exposing (Task) -- The message is just like usual, containing a result type Msg = GotResponse (Result Http.Error String) -- Here we are defining the task getResponseTask : Task Http.Error String getResponseTask = Http.getString "https://jsonplaceholder.typicode.com/posts/1" |> Http.toTask -- And here we turn the task into a regular old command getResponseCmd : Cmd Msg getResponseCmd = getResponseTask |> Task.attempt GotResponse
We could have put the whole thing in a single pipeline, of course:
Http.getString "https://jsonplaceholder.typicode.com/posts/1" |> Http.toTask |> Task.attempt GotResponse
That would be the same as using
Http.send in the first place, though. As a matter of fact,
Http.send is using
Task.attempt underneath (source).
A full compiling and working example using an HTTP task can be found on Ellie.
Now that we’ve established how we can use single tasks on their own, it is time to use them for their true purpose: effects depending on others. For this we will use a function called
andThen. Let’s see what it looks like.
From the official documentation:
andThen : (a -> Task x b) -> Task x a -> Task x b
“Chain together a task and a callback. The first task will run, and if it is successful, you give the result to the callback resulting in another task. This task then gets run.”
Okay, maybe using the tasks from the previous chapter will make this clearer. Let’s imagine we have an API where we can ask for events from the past hour by providing a timestamp of “one hour ago”. This example doesn’t show the imports, decoders etc. to keep it down to the point.
getEventsFromPastHour : Cmd Msg getEventsFromPastHour = Time.now |> Task.andThen (\currentTime -> getEventsFrom (currentTime - Time.hour) ) |> Task.attempt GotResult getEventsFrom : Time -> Task Http.Error (List Event) getEventsFrom time = Http.get (apiUrl ++ "?from=" ++ toString time) eventsListDecoder |> Http.toTask
Let’s try to put this in words.
And again, in Elm code:
Time.now |> Task.andThen (\currentTime -> getEventsFrom (currentTime - Time.hour) ) |> Task.attempt GotResult
A full compilable example of this is again on Ellie. Sadly I couldn’t find a suitable open API to showcase this, but you can see the requests in your browser dev tools.