mirror of
https://github.com/correl/correl.github.io.git
synced 2025-01-01 11:03:18 +00:00
269 lines
17 KiB
HTML
269 lines
17 KiB
HTML
---
|
|
title: "Cleaner Recursive HTTP Requests with Elm Tasks"
|
|
author: "Correl Roush"
|
|
tags: elm programming
|
|
---
|
|
<p>
|
|
<i>Continued from part one, <a href="{{ site.baseurl }}{% post_url 2018-01-22-recursive-http-with-elm %}">Recursive HTTP Requests with Elm</a>.</i>
|
|
</p>
|
|
|
|
<p>
|
|
In <a href="{{ site.baseurl }}{% post_url 2018-01-22-recursive-http-with-elm %}">my last post</a>, 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!
|
|
</p>
|
|
|
|
<div id="outline-container-org6a3dcab" class="outline-2">
|
|
<h2 id="org6a3dcab">What's a Task?</h2>
|
|
<div class="outline-text-2" id="text-org6a3dcab">
|
|
<p>
|
|
A <a href="http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task">Task</a> is a data structure in Elm which represents an asynchronous
|
|
operation that may fail, which can be mapped and <b>chained</b>. 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 <code>Task</code>, which we can then package up into a <a href="http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Platform-Cmd#Cmd">Cmd</a> and hand to
|
|
the Elm runtime to perform. You can think of it like building up a
|
|
<a href="https://en.wikipedia.org/wiki/Futures_and_promises">Future or Promise</a>, setting up a sort of <a href="https://en.wikipedia.org/wiki/Callback_(computer_programming)">callback</a> 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 <code>Msg</code>.
|
|
</p>
|
|
|
|
<p>
|
|
So, tasks sound great!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="outline-container-orgd62ff19" class="outline-2">
|
|
<h2 id="orgd62ff19">Moving to Tasks</h2>
|
|
<div class="outline-text-2" id="text-orgd62ff19">
|
|
<p>
|
|
Just to get things rolling, let's quit using <code>Http.send</code>, and instead
|
|
prepare a simple <code>toTask</code> function leveraging the very handy
|
|
<code>Http.toTask</code>. This'll give us a place to start building up some more
|
|
complex behavior.
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-elm"><span style="color: #ffd700;">send</span> <span style="color: #d18aff;">:</span>
|
|
(<span style="color: #00d7af;">Result</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">Response</span> a) <span style="color: #d18aff;">-></span> msg)
|
|
<span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Request</span> a
|
|
<span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Cmd</span> msg
|
|
<span style="color: #ffd700;">send</span> resultToMessage request <span style="color: #d18aff;">=</span>
|
|
toTask request
|
|
<span style="color: #d18aff;">|></span> <span style="color: #00d7af;">Task</span><span style="color: #d18aff;">.</span>attempt resultToMessage
|
|
|
|
|
|
<span style="color: #ffd700;">toTask</span> <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Request</span> a <span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Task</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">Response</span> a)
|
|
<span style="color: #ffd700;">toTask</span> <span style="color: #d18aff;">=</span>
|
|
httpRequest <span style="color: #d18aff;">>></span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span>toTask
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="outline-container-orga73c528" class="outline-2">
|
|
<h2 id="orga73c528">Shifting the recursion</h2>
|
|
<div class="outline-text-2" id="text-orga73c528">
|
|
<p>
|
|
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 <code>Complete</code> 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.
|
|
</p>
|
|
|
|
<p>
|
|
All that needs to be done here is to chain our response handling using
|
|
<code>Task.andThen</code>, and either recurse to continue the chain with the next
|
|
<code>Task</code>, or wrap up the final results with <code>Task.succeed</code>!
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-elm"><span style="color: #ffd700;">recurse</span> <span style="color: #d18aff;">:</span>
|
|
<span style="color: #00d7af;">Task</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">Response</span> a)
|
|
<span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Task</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">Response</span> a)
|
|
<span style="color: #ffd700;">recurse</span> <span style="color: #d18aff;">=</span>
|
|
<span style="color: #00d7af;">Task</span><span style="color: #d18aff;">.</span>andThen
|
|
(<span style="color: #d18aff;">\</span>response <span style="color: #d18aff;">-></span>
|
|
<span style="color: #a1db00;">case</span> response <span style="color: #a1db00;">of</span>
|
|
<span style="color: #00d7af;">Partial</span> request _ <span style="color: #d18aff;">-></span>
|
|
httpRequest request
|
|
<span style="color: #d18aff;">|></span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span>toTask
|
|
<span style="color: #d18aff;">|></span> recurse
|
|
|
|
<span style="color: #00d7af;">Complete</span> _ <span style="color: #d18aff;">-></span>
|
|
<span style="color: #00d7af;">Task</span><span style="color: #d18aff;">.</span>succeed response
|
|
)
|
|
</pre>
|
|
</div>
|
|
|
|
<p>
|
|
That wasn't so bad. The function recursion almost seems like cheating:
|
|
I'm able to build up a whole chain of requests <i>based</i> on the results
|
|
without actually <i>having</i> the results yet! The <code>Task</code> 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.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="outline-container-org58dfec7" class="outline-2">
|
|
<h2 id="org58dfec7">Accumulating results</h2>
|
|
<div class="outline-text-2" id="text-org58dfec7">
|
|
<p>
|
|
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.
|
|
</p>
|
|
|
|
<p>
|
|
Cleaning up the types is pretty easy. It's just a matter of switching
|
|
out some instances of <code>Response a</code> with <code>List a</code> in our type
|
|
declarations…
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-elm"><span style="color: #ffd700;">send</span> <span style="color: #d18aff;">:</span>
|
|
(<span style="color: #00d7af;">Result</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">List</span> a) <span style="color: #d18aff;">-></span> msg)
|
|
<span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Request</span> a
|
|
<span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Cmd</span> msg
|
|
|
|
|
|
<span style="color: #ffd700;">toTask</span> <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Request</span> a <span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Task</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">List</span> a)
|
|
|
|
|
|
<span style="color: #ffd700;">recurse</span> <span style="color: #d18aff;">:</span>
|
|
<span style="color: #00d7af;">Task</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">Response</span> a)
|
|
<span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Task</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">List</span> a)
|
|
</pre>
|
|
</div>
|
|
|
|
|
|
<p>
|
|
…then changing our <code>Complete</code> case to return the actual items:
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-elm"><span style="color: #00d7af;">Complete</span> xs <span style="color: #d18aff;">-></span>
|
|
<span style="color: #00d7af;">Task</span><span style="color: #d18aff;">.</span>succeed xs
|
|
</pre>
|
|
</div>
|
|
|
|
<p>
|
|
The final step, then, is to accumulate the results. Turns out this is
|
|
<b>super</b> easy. We already have an <code>update</code> function that combines two
|
|
responses, so we can map <i>that</i> over our next request task so that it
|
|
incorporates the previous request's results!
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-elm"><span style="color: #00d7af;">Partial</span> request _ <span style="color: #d18aff;">-></span>
|
|
httpRequest request
|
|
<span style="color: #d18aff;">|></span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span>toTask
|
|
<span style="color: #d18aff;">|></span> <span style="color: #00d7af;">Task</span><span style="color: #d18aff;">.</span>map (update response)
|
|
<span style="color: #d18aff;">|></span> recurse
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="outline-container-orgb190c8c" class="outline-2">
|
|
<h2 id="orgb190c8c">Tidying up</h2>
|
|
<div class="outline-text-2" id="text-orgb190c8c">
|
|
<p>
|
|
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 <code>Response</code> structure,
|
|
<code>update</code> method, and <code>httpRequest</code> no longer need to be exposed.
|
|
<code>toTask</code> can be exposed now as a convenience to anyone who wants to
|
|
perform further chaining on their calls.
|
|
</p>
|
|
|
|
<p>
|
|
Now that there's a cleaner interface to the module, the example app is
|
|
looking a lot cleaner now, too:
|
|
</p>
|
|
|
|
<div class="org-src-container">
|
|
<pre class="src src-elm"><span style="color: #a1db00;">module</span> <span style="color: #00d7af;">Example</span> <span style="color: #a1db00;">exposing</span> (<span style="color: #d18aff;">..</span>)
|
|
|
|
<span style="color: #a1db00;">import</span> <span style="color: #00d7af;">Html</span> <span style="color: #a1db00;">exposing</span> (<span style="color: #00d7af;">Html</span>)
|
|
<span style="color: #a1db00;">import</span> <span style="color: #00d7af;">Http</span>
|
|
<span style="color: #a1db00;">import</span> <span style="color: #00d7af;">Json</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Decode</span> <span style="color: #a1db00;">exposing</span> (field<span style="color: #d18aff;">,</span> string)
|
|
<span style="color: #a1db00;">import</span> <span style="color: #00d7af;">Paginated</span>
|
|
|
|
|
|
<span style="color: #a1db00;">type</span> <span style="color: #a1db00;">alias</span> <span style="color: #00d7af;">Model</span> <span style="color: #d18aff;">=</span>
|
|
{ repositories <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Maybe</span> (<span style="color: #00d7af;">List</span> <span style="color: #00d7af;">String</span>) }
|
|
|
|
|
|
<span style="color: #a1db00;">type</span> <span style="color: #00d7af;">Msg</span>
|
|
<span style="color: #d18aff;">=</span> <span style="color: #00d7af;">GotRepositories</span> (<span style="color: #00d7af;">Result</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span><span style="color: #00d7af;">Error</span> (<span style="color: #00d7af;">List</span> <span style="color: #00d7af;">String</span>))
|
|
|
|
|
|
<span style="color: #ffd700;">main</span> <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Program</span> <span style="color: #00d7af;">Never</span> <span style="color: #00d7af;">Model</span> <span style="color: #00d7af;">Msg</span>
|
|
<span style="color: #ffd700;">main</span> <span style="color: #d18aff;">=</span>
|
|
<span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>program
|
|
<span style="color: #a1db00;">{</span> init <span style="color: #d18aff;">=</span> init
|
|
<span style="color: #a1db00;">,</span> update <span style="color: #d18aff;">=</span> update
|
|
<span style="color: #a1db00;">,</span> view <span style="color: #d18aff;">=</span> view
|
|
<span style="color: #a1db00;">,</span> subscriptions <span style="color: #d18aff;">=</span> <span style="color: #d18aff;">\</span>_ <span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Sub</span><span style="color: #d18aff;">.</span>none
|
|
<span style="color: #a1db00;">}</span>
|
|
|
|
|
|
<span style="color: #ffd700;">init</span> <span style="color: #d18aff;">:</span> ( <span style="color: #00d7af;">Model</span><span style="color: #d18aff;">,</span> <span style="color: #00d7af;">Cmd</span> <span style="color: #00d7af;">Msg</span> )
|
|
<span style="color: #ffd700;">init</span> <span style="color: #d18aff;">=</span>
|
|
( { repositories <span style="color: #d18aff;">=</span> <span style="color: #00d7af;">Nothing</span> }
|
|
<span style="color: #a1db00;">,</span> getRepositories
|
|
)
|
|
|
|
|
|
<span style="color: #ffd700;">update</span> <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Msg</span> <span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Model</span> <span style="color: #d18aff;">-></span> ( <span style="color: #00d7af;">Model</span><span style="color: #d18aff;">,</span> <span style="color: #00d7af;">Cmd</span> <span style="color: #00d7af;">Msg</span> )
|
|
<span style="color: #ffd700;">update</span> msg model <span style="color: #d18aff;">=</span>
|
|
<span style="color: #a1db00;">case</span> msg <span style="color: #a1db00;">of</span>
|
|
<span style="color: #00d7af;">GotRepositories</span> result <span style="color: #d18aff;">-></span>
|
|
( { model <span style="color: #d18aff;">|</span> repositories <span style="color: #d18aff;">=</span> <span style="color: #00d7af;">Result</span><span style="color: #d18aff;">.</span>toMaybe result }
|
|
<span style="color: #a1db00;">,</span> <span style="color: #00d7af;">Cmd</span><span style="color: #d18aff;">.</span>none
|
|
)
|
|
|
|
|
|
<span style="color: #ffd700;">view</span> <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Model</span> <span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Html</span> <span style="color: #00d7af;">Msg</span>
|
|
<span style="color: #ffd700;">view</span> model <span style="color: #d18aff;">=</span>
|
|
<span style="color: #a1db00;">case</span> model<span style="color: #d18aff;">.</span>repositories <span style="color: #a1db00;">of</span>
|
|
<span style="color: #00d7af;">Nothing</span> <span style="color: #d18aff;">-></span>
|
|
<span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>div [] [ <span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>text <span style="color: #ff4ea3;">"Loading"</span> ]
|
|
|
|
<span style="color: #00d7af;">Just</span> repos <span style="color: #d18aff;">-></span>
|
|
<span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>ul [] <span style="color: #d18aff;"><|</span>
|
|
<span style="color: #00d7af;">List</span><span style="color: #d18aff;">.</span>map
|
|
(<span style="color: #d18aff;">\</span>x <span style="color: #d18aff;">-></span> <span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>li [] [ <span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>text x ])
|
|
repos
|
|
|
|
|
|
<span style="color: #ffd700;">getRepositories</span> <span style="color: #d18aff;">:</span> <span style="color: #00d7af;">Cmd</span> <span style="color: #00d7af;">Msg</span>
|
|
<span style="color: #ffd700;">getRepositories</span> <span style="color: #d18aff;">=</span>
|
|
<span style="color: #00d7af;">Paginated</span><span style="color: #d18aff;">.</span>send <span style="color: #00d7af;">GotRepositories</span> <span style="color: #d18aff;"><|</span>
|
|
<span style="color: #00d7af;">Paginated</span><span style="color: #d18aff;">.</span>get
|
|
<span style="color: #ff4ea3;">"http://git.phoenixinquis.net/api/v4/projects?per_page=5"</span>
|
|
(field <span style="color: #ff4ea3;">"name"</span> string)
|
|
</pre>
|
|
</div>
|
|
|
|
<p>
|
|
So, there we have it! Feel free to check out the my complete
|
|
<code>Paginated</code> library on the <a href="http://package.elm-lang.org/packages/correl/elm-paginated/latest">Elm package index</a>, or on <a href="https://github.com/correl/elm-paginated">GitHub</a>. 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 :)
|
|
</p>
|
|
</div>
|
|
</div>
|