correl.github.io/_posts/2018-01-23-cleaner-recursive-http-with-elm-tasks.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;">-&gt;</span> msg)
<span style="color: #d18aff;">-&gt;</span> <span style="color: #00d7af;">Request</span> a
<span style="color: #d18aff;">-&gt;</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;">|&gt;</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;">-&gt;</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;">&gt;&gt;</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;">-&gt;</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;">-&gt;</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;">-&gt;</span>
httpRequest request
<span style="color: #d18aff;">|&gt;</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span>toTask
<span style="color: #d18aff;">|&gt;</span> recurse
<span style="color: #00d7af;">Complete</span> _ <span style="color: #d18aff;">-&gt;</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&#x2026;
</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;">-&gt;</span> msg)
<span style="color: #d18aff;">-&gt;</span> <span style="color: #00d7af;">Request</span> a
<span style="color: #d18aff;">-&gt;</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;">-&gt;</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;">-&gt;</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>
&#x2026;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;">-&gt;</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;">-&gt;</span>
httpRequest request
<span style="color: #d18aff;">|&gt;</span> <span style="color: #00d7af;">Http</span><span style="color: #d18aff;">.</span>toTask
<span style="color: #d18aff;">|&gt;</span> <span style="color: #00d7af;">Task</span><span style="color: #d18aff;">.</span>map (update response)
<span style="color: #d18aff;">|&gt;</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;">-&gt;</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;">-&gt;</span> <span style="color: #00d7af;">Model</span> <span style="color: #d18aff;">-&gt;</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;">-&gt;</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;">-&gt;</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;">-&gt;</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;">-&gt;</span>
<span style="color: #00d7af;">Html</span><span style="color: #d18aff;">.</span>ul [] <span style="color: #d18aff;">&lt;|</span>
<span style="color: #00d7af;">List</span><span style="color: #d18aff;">.</span>map
(<span style="color: #d18aff;">\</span>x <span style="color: #d18aff;">-&gt;</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;">&lt;|</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>