correl.github.io/content/blog/cleaner-recursive-http-with-elm-tasks.md

227 lines
7 KiB
Markdown
Raw Normal View History

+++
title = "Cleaner Recursive HTTP Requests with Elm Tasks"
author = ["Correl Roush"]
date = 2018-01-23T00:00:00-05:00
keywords = ["emacs", "org-mode", "themes"]
tags = ["programming", "elm"]
draft = false
+++
_Continued from part one, [Recursive HTTP Requests with Elm]({{< relref "recursive-http-requests-with-elm.md" >}})._
In [my last post]({{< relref "recursive-http-requests-with-elm.md" >}}), I described my first pass at building a library to
fetch data from a paginated JSON REST API. It worked, but it wasn't
too clean. In particular, the handling of the multiple pages and
concatenation of results was left up to the calling code. Ideally,
both of these concerns should be handled by the library, letting the
application focus on working with a full result set. Using Elm's
Tasks, we can achieve exactly that!
## What's a Task? {#what-s-a-task}
A [Task](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task) is a data structure in Elm which represents an asynchronous
operation that may fail, which can be mapped and **chained**. What this
means is, we can create an action, transform it, and chain it with
additional actions, building up a complex series of things to do into
a single `Task`, which we can then package up into a [Cmd](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Platform-Cmd#Cmd) and hand to
the Elm runtime to perform. You can think of it like building up a
[Future or Promise](https://en.wikipedia.org/wiki/Futures%5Fand%5Fpromises), setting up a sort of [callback](https://en.wikipedia.org/wiki/Callback%5F(computer%5Fprogramming)) chain of mutations
and follow-up actions to be taken. The Elm runtime will work its way
through the chain and hand your application back the result in the
form of a `Msg`.
So, tasks sound great!
## Moving to Tasks {#moving-to-tasks}
Just to get things rolling, let's quit using `Http.send`, and instead
prepare a simple `toTask` function leveraging the very handy
`Http.toTask`. This'll give us a place to start building up some more
complex behavior.
```elm
send :
(Result Http.Error (Response a) -> msg)
-> Request a
-> Cmd msg
send resultToMessage request =
toTask request
|> Task.attempt resultToMessage
toTask : Request a -> Task Http.Error (Response a)
toTask =
httpRequest >> Http.toTask
```
## Shifting the recursion {#shifting-the-recursion}
Now, for the fun bit. We want, when a request completes, to inspect
the result. If the task failed, we do nothing. If it succeeded, we
move on to checking the response. If we have a `Complete` response,
we're done. If we do not, we want to build another task for the next
request, and start a new iteration on that.
All that needs to be done here is to chain our response handling using
`Task.andThen`, and either recurse to continue the chain with the next
`Task`, or wrap up the final results with `Task.succeed`!
```elm
recurse :
Task Http.Error (Response a)
-> Task Http.Error (Response a)
recurse =
Task.andThen
(\response ->
case response of
Partial request _ ->
httpRequest request
|> Http.toTask
|> recurse
Complete _ ->
Task.succeed response
)
```
That wasn't so bad. The function recursion almost seems like cheating:
I'm able to build up a whole chain of requests _based_ on the results
without actually _having_ the results yet! The `Task` lets us define a
complete plan for what to do with the results, using what we know
about the data structures flowing through to make decisions and tack
on additional things to do.
## Accumulating results {#accumulating-results}
There's just one thing left to do: we're not accumulating results yet.
We're just handing off the results of the final request, which isn't
too helpful to the caller. We're also still returning our Response
structure, which is no longer necessary, since we're not bothering
with returning incomplete requests anymore.
Cleaning up the types is pretty easy. It's just a matter of switching
out some instances of `Response a` with `List a` in our type
declarations...
```elm
send :
(Result Http.Error (List a) -> msg)
-> Request a
-> Cmd msg
toTask : Request a -> Task Http.Error (List a)
recurse :
Task Http.Error (Response a)
-> Task Http.Error (List a)
```
...then changing our `Complete` case to return the actual items:
```elm
Complete xs ->
Task.succeed xs
```
The final step, then, is to accumulate the results. Turns out this is
**super** easy. We already have an `update` function that combines two
responses, so we can map _that_ over our next request task so that it
incorporates the previous request's results!
```elm
Partial request _ ->
httpRequest request
|> Http.toTask
|> Task.map (update response)
|> recurse
```
## Tidying up {#tidying-up}
Things are tied up pretty neatly, now! Calling code no longer needs to
care whether the JSON endpoints its calling paginate their results,
they'll receive everything they asked for as though it were a single
request. Implementation details like the `Response` structure,
`update` method, and `httpRequest` no longer need to be exposed.
`toTask` can be exposed now as a convenience to anyone who wants to
perform further chaining on their calls.
Now that there's a cleaner interface to the module, the example app is
looking a lot cleaner now, too:
```elm
module Example exposing (..)
import Html exposing (Html)
import Http
import Json.Decode exposing (field, string)
import Paginated
type alias Model =
{ repositories : Maybe (List String) }
type Msg
= GotRepositories (Result Http.Error (List String))
main : Program Never Model Msg
main =
Html.program
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
init : ( Model, Cmd Msg )
init =
( { repositories = Nothing }
, getRepositories
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotRepositories result ->
( { model | repositories = Result.toMaybe result }
, Cmd.none
)
view : Model -> Html Msg
view model =
case model.repositories of
Nothing ->
Html.div [] [ Html.text "Loading" ]
Just repos ->
Html.ul [] <|
List.map
(\x -> Html.li [] [ Html.text x ])
repos
getRepositories : Cmd Msg
getRepositories =
Paginated.send GotRepositories <|
Paginated.get
"http://git.phoenixinquis.net/api/v4/projects?per_page=5"
(field "name" string)
```
So, there we have it! Feel free to check out the my complete
`Paginated` library on the [Elm package index](http://package.elm-lang.org/packages/correl/elm-paginated/latest), or on [GitHub](https://github.com/correl/elm-paginated). Hopefully
you'll find it or this post useful. I'm still finding my way around
Elm, so any and all feedback is quite welcome :)