mirror of
https://github.com/correl/correl.github.io.git
synced 2025-01-01 11:03:18 +00:00
227 lines
7 KiB
Markdown
227 lines
7 KiB
Markdown
|
+++
|
||
|
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 :)
|