Article summary
I recently attended elm-conf (videos of the elm-conf presentations), which was hosted at Strange Loop on its preconference day. I’d been meaning to play around with Elm for years and was finally sparked to do so to prepare for the conference.
As I often do when trying to learn a new language, I came up with a little side project that I could implement in Elm. The project is a very basic reporting app that would pull data from a REST API.
I ran into trouble when I wanted to make a request to the API. This was immediately followed by another request that makes use of the data returned from the first. There are plenty of examples out there showing how to make a single HTTP request in Elm—but I couldn’t find anything that showed how to chain multiple requests together.
It turns out, it’s really easy to do! To help the next developer who wants to chain HTTP requests together in Elm, I’m going to walk through an example here.
(Note: The code snippets might say they’re Haskell, but they’re really Elm. Our snippet highlighter doesn’t recognize Elm yet, and the Haskell highlighter does a pretty good job.)
HTTP.get
The README for the evancz/elm-http
package has a good example of making a single HTTP request:
import Http
import Json.Decode as Json exposing ((:=))
import Task exposing (..)
lookupZipCode : String -> Task Http.Error (List String)
lookupZipCode query =
Http.get places ("http://api.zippopotam.us/us/" ++ query)
places : Json.Decoder (List String)
places =
let place =
Json.object2 (\city state -> city ++ ", " ++ state)
("place name" := Json.string)
("state" := Json.string)
in
"places" := Json.list place
Elm Architecture Cmd
In order to make an HTTP request within The Elm Architecture, you need to use Task.perform
. The Tasks section of the Elm Tutorial provides the following example:
module Main exposing (..)
import Html exposing (Html, div, button, text)
import Html.Events exposing (onClick)
import Html.App
import Http
import Task exposing (Task)
import Json.Decode as Decode
-- MODEL
type alias Model =
String
init : ( Model, Cmd Msg )
init =
( "", Cmd.none )
-- MESSAGES
type Msg
= Fetch
| FetchSuccess String
| FetchError Http.Error
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Fetch ] [ text "Fetch" ]
, text model
]
decode : Decode.Decoder String
decode =
Decode.at [ "name" ] Decode.string
url : String
url =
"http://swapi.co/api/planets/1/?format=json"
fetchTask : Task Http.Error String
fetchTask =
Http.get decode url
fetchCmd : Cmd Msg
fetchCmd =
Task.perform FetchError FetchSuccess fetchTask
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Fetch ->
( model, fetchCmd )
FetchSuccess name ->
( name, Cmd.none )
FetchError error ->
( toString error, Cmd.none )
-- MAIN
main : Program Never
main =
Html.App.program
{ init = init
, view = view
, update = update
, subscriptions = (always Sub.none)
}
In short, you call Task.perform
, passing it a constructor for the failure message, a constructor for the success message, and the task to run. Cutting out everything else, here’s (a slightly modified version of) the Task.perform
on a single line:
Task.perform FetchError FetchSuccess (Http.get decode "http://swapi.co/api/planets/1/?format=json")
Chaining Tasks with `andThen`
The documentation for andThen states:
This is useful for chaining tasks together. Maybe you need to get a user from your servers and then lookup their picture once you know their name.
Exactly what I wanted to do!
Once I came across andThen
, I still struggled a bit thinking that I was supposed to be chaining two Task.perform
results together. It wasn’t until I finally understood that an Http.get
call returns a Task, and so does an andThen
call, that I realized I wanted to andThen
the two Http.get
calls together and pass that result to the Task.perform
.
So here it is. A very simple (once I finally understood what to do) example of chaining two HTTP requests together in a single command.
decodePlanet : Decode.Decoder String
decodePlanet =
Decode.at [ "name" ] Decode.string
decodePerson : Decode.Decoder String
decodePerson =
Decode.at [ "homeworld" ] Decode.string
fetchPlanet : String -> Task Http.Error String
fetchPlanet url =
Http.get decodePlanet url
fetchPerson : Task Http.Error String
fetchPerson =
Http.get decodePerson "http://swapi.co/api/people/1"
fetchCmd : Cmd Msg
fetchCmd =
Task.perform FetchError FetchSuccess (fetchPerson `andThen` fetchPlanet)
The trick is to call Task.perform
once, passing a Task
to it that’s a sequence of the first Http.get
(fetchPerson
) followed by the second Http.get
(fetchPlanet
). It’s worth noting that the fetchPlanet
function takes a String argument, which is the URL parsed from the first request.
Hopefully, this will help someone stuck on the same problem I was having.