« Home

Tasks in Modern Elm

Ossi Hanhinen @ohanhi


Read this to

I expect you to know


Why tasks matter

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.

Task basics

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

“Get the 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.perform and 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 toTask and Task.attempt underneath (source).

A full compiling and working example using an HTTP task can be found on Ellie.

Chaining tasks

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.

Maybe your followers would be interested in this post? Get tweety with it!