mirror of
https://github.com/correl/correl.github.io.git
synced 2025-03-18 17:00:05 -09:00
220 lines
7.2 KiB
Org Mode
220 lines
7.2 KiB
Org Mode
#+TITLE: Cleaner Recursive HTTP Requests with Elm Tasks
|
|
#+AUTHOR: Correl Roush
|
|
#+STARTUP: indent showall inlineimages
|
|
#+OPTIONS: toc:nil num:nil
|
|
#+PROPERTY: header-args :exports code :eval never
|
|
#+KEYWORDS: elm programming
|
|
|
|
/Continued from part one, [[post:2018-01-22-recursive-http-with-elm.org][Recursive HTTP Requests with Elm]]./
|
|
|
|
In [[post:2018-01-22-recursive-http-with-elm.org][my last post]], 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?
|
|
|
|
A [[http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task][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 [[http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Platform-Cmd#Cmd][Cmd]] and hand to
|
|
the Elm runtime to perform. You can think of it like building up a
|
|
[[https://en.wikipedia.org/wiki/Futures_and_promises][Future or Promise]], setting up a sort of [[https://en.wikipedia.org/wiki/Callback_(computer_programming)][callback]] 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
|
|
|
|
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.
|
|
|
|
#+BEGIN_SRC 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
|
|
#+END_SRC
|
|
|
|
* 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=!
|
|
|
|
#+BEGIN_SRC 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
|
|
)
|
|
#+END_SRC
|
|
|
|
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
|
|
|
|
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...
|
|
|
|
#+BEGIN_SRC 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)
|
|
#+END_SRC
|
|
|
|
|
|
...then changing our =Complete= case to return the actual items:
|
|
|
|
#+BEGIN_SRC elm
|
|
Complete xs ->
|
|
Task.succeed xs
|
|
#+END_SRC
|
|
|
|
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!
|
|
|
|
#+BEGIN_SRC elm
|
|
Partial request _ ->
|
|
httpRequest request
|
|
|> Http.toTask
|
|
|> Task.map (update response)
|
|
|> recurse
|
|
#+END_SRC
|
|
|
|
* 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:
|
|
|
|
#+BEGIN_SRC 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)
|
|
#+END_SRC
|
|
|
|
So, there we have it! Feel free to check out the my complete
|
|
=Paginated= library on the [[http://package.elm-lang.org/packages/correl/elm-paginated/latest][Elm package index]], or on [[https://github.com/correl/elm-paginated][GitHub]]. 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 :)
|