In this section, we will see how that fits into a system that talks to servers, uses websockets, writes to databases, etc. The following diagram gives a high-level overview:
The main idea is that the vast majority of our application logic can be described in terms of the Elm Architecture, allowing us to make the most of stateless functions and immutability. In this realm we use Signals to route events to the right place.
When we want to have some effect on the world, we create a service totally separate from our core logic. Each service may be in charge of a specific task, like managing database connections or communicating via websockets. We script all of these effects with Tasks.
We get a lot of benefits by cleanly separating out these services. Most obviously, testing is much simpler. We can make sure our UI and application logic work without doing crazy tricks to mock a database.
Tasks make it easy to describe asynchronous operations that may fail, like HTTP requests or writing to a database. Tons of browser APIs are described as tasks in Elm:
Tasks also work like light-weight threads in Elm, so you can have a bunch of tasks running at the same time and the runtime RTS will hop between them if they are blocked.
import Task exposing (Task, andThen) port runner : Task x () port runner = getCurrTime `andThen` putCurrTime main = ... getCurrTime : Task x Time putCurrTime : Time ...
First, notice the infrequently-used backtick syntax which lets us treat normal functions as infix operators.
Okay, now that we know that `andThen` is a normal function that takes two arguments, let’s see the type.
andThen : Task x a -> (a -> Task x b) -> Task x b
The first argument is a task that we want to happen, in our example this is `getCurrentTime`. The second argument is a callback that creates a brand new task. In our case this means taking the current time and printing it.
The `andThen` function is extremely important when using tasks because it lets us build complex chains.
One of the most common things you will want to do in a web app is talk to servers. The elm-http library provides everything you need for that, so let’s try to get a feel for how it works with the `Http.getString` function.
Http.getString : String -> Task Http.Error String
We provide a URL, and it will create a task that tries to fetch the resource that lives at that location as a `String`. Looking at the type of the `Task`, finally that darn `x` is filled in with a real error type! This task will either fail with some `Http.Error` or succeed with a `String`.
We have seen `andThen` used to chain two tasks together, but what if we want to chain lots of tasks? This can end up looking a bit odd, so you can bend the typical rules about indentation to make it look nicer. Let’s look at an example that chains a bunch of tasks together to measure how long it takes to evaluate the `(fibonacci 20)` expression:
This reads fairly naturally. Get the current time, run the fibonacci function, get the current time again, and then succeed with the difference between the start and end time.import Task exposing (Task, andThen, succeed) import Time exposing (Time) getDuration : Task x Time getDuration = getCurrTime `andThen` \start -> succeed (fibonacci 20) `andThen` \fib -> getCurrTime `andThen` \end -> succeed (end - start) fibonacci : Int -> Int fibonacci n = if n <= 2 then 1 else fibonacci (n - 1) + fibonacci (n - 2) port runner : Task x () port runner = getDuration `andThen` printIt main = .. printIt :
You might be wondering “why is `start` in scope two tasks later?” The trick here is that an anonymous function includes everything after the arrow. So if we were to put parentheses on our `getDuration` function, it would look like this:
getDuration : Task x Time getDuration = getCurrentTime `andThen` (\start -> succeed (fibonacci 20) `andThen` (\fib -> getCurrTime `andThen` (\end -> succeed (end - start))))Now you can really see how weird our indentation is! The point is that you will see this chaining pattern relatively often because it lets you keep a bunch of variables in scope for many different tasks.
Error HandlingSo far we have only really considered tasks that succeed, but what happens when an HTTP request comes back with a 404 or some JSON cannot be decoded? There are two main ways to handle errors with tasks. The first is the `onError` function:
onError : Task x a -> (x -> Task y a) -> Task y a
Notice that it looks very similar to `andThen` but it only gets activated when there is an error. So if we want to recover from a bad JSON request, we could write something like this:
With the `get` task, we can potentially fail with an `Http.Error` but when we add recovery with `onError` we end up with the `safeGet` task which will always succeed. When a task always succeeds, it is not possible to pin down the error type. The type could be anything, we will never know because it will never happen. That is why you see the free type variable `x` in the type of `safeGet`.import Http import Json.Decode as Json import Task exposing (Task, andThen, onError, succeed) get : Task Http.Error (List String) get = Http.get (Json.list Json.string) "http://example.com/hat-list.json" safeGet : Task x (List String) safeGet = get `onError` (\err -> succeed) port runner : Task x () port runner = safeGet `andThen` printIt main =
The second approach to error handling is to use functions like `Task.toMaybe` and `Task.toResult`.
toMaybe : Task x a -> Task y (Maybe a) toMaybe task = Task.map Just task `onError` \_ -> succeed Nothing toResult : Task x a -> Task y (Result x a) toResult task = Task.map Ok task `onError` \msg -> succeed (Err msg)
This is essentially promoting any errors to the success case.