Using map, apply, bind and sequence in practice
A real-world example that uses all the techniques
This post is the fifth in a series. In the first two posts, I described some of the core functions for dealing with generic data types: map, bind, and so on. In the third post, I discussed "applicative" vs "monadic" style, and how to lift values and functions to be consistent with each other. In the previous post, I introduced traverse and sequence as a way of working with lists of elevated values.
In this post, we'll finish up by working through a practical example that uses all the techniques that have been discussed so far.
Series contents
Here's a list of shortcuts to the various functions mentioned in this series:
Part 1: Lifting to the elevated world
Part 2: How to compose world-crossing functions
Part 3: Using the core functions in practice
Part 5: A real-world example that uses all the techniques
Part 6: Designing your own elevated world
Part 7: Summary
Part 5: A real-world example that uses all the techniques
Example: Downloading and processing a list of websites
The example will be a variant of the one mentioned at the beginning of the third post:
Given a list of websites, create an action that finds the site with the largest home page.
Let's break this down into steps:
First we'll need to transform the urls into a list of actions, where each action downloads the page and gets the size of the content.
And then we need to find the largest content, but in order to do this we'll have to convert the list of actions into a single action containing a list of sizes. And that's where traverse or sequence will come in.
Let's get started!
The downloader
First we need to create a downloader. I would use the built-in System.Net.WebClient class, but for some reason it doesn't allow override of the timeout. I'm going to want to have a small timeout for the later tests on bad uris, so this is important.
One trick is to just subclass WebClient and intercept the method that builds a request. So here it is:
Notice that I'm using units of measure for the timeout value. I find that units of measure are invaluable to distiguish seconds from milliseconds. I once accidentally set a timeout to 2000 seconds rather than 2000 milliseconds and I don't want to make that mistake again!
The next bit of code defines our domain types. We want to be able to keep the url and the size together as we process them. We could use a tuple, but I am a proponent of using types to model your domain, if only for documentation.
Yes, this might be overkill for a trivial example like this, but in a more serious project I think it is very much worth doing.
Now for the code that does the downloading:
Notes:
The .NET library will throw on various errors, so I am catching that and turning it into a
Failure.The
use client =section ensures that the client will be correctly disposed at the end of the block.The whole operation is wrapped in an
asyncworkflow, and thelet! html = client.AsyncDownloadStringis where the download happens asynchronously.I've added some
printfns for tracing, just for this example. In real code, I wouldn't do this of course!
Before moving on, let's test this code interactively. First we need a helper to print the result:
And then we can try it out on a good site:
and a bad one:
Extending the Async type with map and apply and bind
map and apply and bindAt this point, we know that we are going to be dealing with the world of Async, so before we go any further, let's make sure that we have our four core functions available:
These implementations are straightforward:
I'm using the
asyncworkflow to work withAsyncvalues.The
let!syntax inmapextracts the content from theAsync(meaning run it and await the result).The
returnsyntax inmap,retn, andapplylifts the value to anAsyncusingreturn.The
applyfunction runs the two parameters in parallel using a fork/join pattern.If I had instead written
let! fChild = ...followed by alet! xChild = ...that would have been monadic and sequential, which is not what I wanted.
The
return!syntax inbindmeans that the value is already lifted and not to callreturnon it.
Getting the size of the downloaded page
Getting back on track, we can continue from the downloading step and move on to the process of converting the result to a UriContentSize:
If the input html is null or empty we'll treat this an error, otherwise we'll return a UriContentSize.
Now we have two functions and we want to combine them into one "get UriContentSize given a Uri" function. The problem is that the outputs and inputs don't match:
getUriContentisUri -> Async<Result<UriContent>>makeContentSizeisUriContent -> Result<UriContentSize>
The answer is to transform makeContentSize from a function that takes a UriContent as input into a function that takes a Async<Result<UriContent>> as input. How can we do that?
First, use Result.bind to convert it from an a -> Result<b> function to a Result<a> -> Result<b> function. In this case, UriContent -> Result<UriContentSize> becomes Result<UriContent> -> Result<UriContentSize>.
Next, use Async.map to convert it from an a -> b function to a Async<a> -> Async<b> function. In this case, Result<UriContent> -> Result<UriContentSize> becomes Async<Result<UriContent>> -> Async<Result<UriContentSize>>.

And now that it has the right kind of input, so we can compose it with getUriContent:
That's some gnarly type signature, and it's only going to get worse! It's at times like these that I really appreciate type inference.
Let's test again. First a helper to format the result:
And then we can try it out on a good site:
and a bad one:
Getting the largest size from a list
The last step in the process is to find the largest page size.
That's easy. Once we have a list of UriContentSize, we can easily find the largest one using List.maxBy:
Putting it all together
We're ready to assemble all the pieces now, using the following algorithm:
Start with a list of urls
Turn the list of strings into a list of uris (
Uri list)Turn the list of
Uris into a list of actions (Async<Result<UriContentSize>> list)Next we need to swap the top two parts of the stack. That is, transform a
List<Async>into aAsync<List>.

Next we need to swap the bottom two parts of the stack -- transform a
List<Result>into aResult<List>.But the two bottom parts of the stack are wrapped in an
Asyncso we need to useAsync.mapto do this.

Finally we need to use
List.maxByon the bottomListto convert it into a single value. That is, transform aList<UriContentSize>into aUriContentSize.But the bottom of the stack is wrapped in a
Resultwrapped in anAsyncso we need to useAsync.mapandResult.mapto do this.

Here's the complete code:
This function has signature string list -> Async<Result<UriContentSize>>, which is just what we wanted!
There are two sequence functions involved here: sequenceAsyncA and sequenceResultA. The implementations are as you would expect from all the previous discussion, but I'll show the code anyway:
Adding a timer
It will be interesting to see how long the download takes for different scenarios, so let's create a little timer that runs a function a certain number of times and takes the average:
Ready to download at last
Let's download some sites for real!
We'll define two lists of sites: a "good" one, where all the sites should be accessible, and a "bad" one, containing invalid sites.
Let's start by running largestPageSizeA 10 times with the good sites list:
The output is something like this:
We can see immediately that the downloads are happening in parallel -- they have all started before the first one has finished.
Now what about if some of the sites are bad?
The output is something like this:
Again, all the downloads are happening in parallel, and all four failures are returned.
Optimizations
The largestPageSizeA has a series of maps and sequences in it which means that the list is being iterated over three times and the async mapped over twice.
As I said earlier, I prefer clarity over micro-optimizations unless there is proof otherwise, and so this does not bother me.
However, let's look at what you could do if you wanted to.
Here's the original version, with comments removed:
The first two List.maps could be combined:
The map-sequence can be replaced with a traverse:
and finally the two Async.maps can be combined too:
Personally, I think we've gone too far here. I prefer the original version to this one!
As an aside, one way to get the best of both worlds is to use a "streams" library that automatically merges the maps for you. In F#, a good one is Nessos Streams. Here is a blog post showing the difference between streams and the standard seq.
Downloading the monadic way
Let's reimplement the downloading logic using monadic style and see what difference it makes.
First we need a monadic version of the downloader:
This one uses the monadic sequence functions (I won't show them -- the implementation is as you expect).
Let's run largestPageSizeM 10 times with the good sites list and see if there is any difference from the applicative version:
The output is something like this:
There is a big difference now -- it is obvious that the downloads are happening in series -- each one starts only when the previous one has finished.
As a result, the average time is 955ms per run, almost twice that of the applicative version.
Now what about if some of the sites are bad? What should we expect? Well, because it's monadic, we should expect that after the first error, the remaining sites are skipped, right? Let's see if that happens!
The output is something like this:
Well that was unexpected! All of the sites were visited in series, even though the first one had an error. But in that case, why is only the first error returned, rather than all the the errors?
Can you see what went wrong?
Explaining the problem
The reason why the implementation did not work as expected is that the chaining of the Asyncs was independent of the chaining of the Results.
If you step through this in a debugger you can see what is happening:
The first
Asyncin the list was run, resulting in a failure.Async.bindwas used with the nextAsyncin the list. ButAsync.bindhas no concept of error, so the nextAsyncwas run, producing another failure.In this way, all the
Asyncs were run, producing a list of failures.This list of failures was then traversed using
Result.bind. Of course, because of the bind, only the first one was processed and the rest ignored.The final result was that all the
Asyncs were run but only the first failure was returned.
Treating two worlds as one
The fundamental problem is that we are treating the Async list and Result list as separate things to be traversed over. But that means that a failed Result has no influence on whether the next Async is run.
What we want to do, then, is tie them together so that a bad result does determine whether the next Async is run.
And in order to do that, we need to treat the Async and the Result as a single type -- let's imaginatively call it AsyncResult.
If they are a single type, then bind looks like this:

meaning that the previous value will determine the next value.
And also, the "swapping" becomes much simpler:

Defining the AsyncResult type
OK, let's define the AsyncResult type and it's associated map, return, apply and bind functions.
Notes:
The type alias is optional. We can use
Async<Result<'a>>directly in the code and it wil work fine. The point is that conceptuallyAsyncResultis a separate type.The
bindimplementation is new. The continuation functionfis now crossing two worlds, and has the signature'a -> Async<Result<'b>>.If the inner
Resultis successful, the continuation functionfis evaluated with the result. Thereturn!syntax means that the return value is already lifted.If the inner
Resultis a failure, we have to lift the failure to an Async.
Defining the traverse and sequence functions
With bind and return in place, we can create the appropriate traverse and sequence functions for AsyncResult:
Defining and testing the downloading functions
Finally, the largestPageSize function is simpler now, with only one sequence needed.
Let's run largestPageSizeM_AR 10 times with the good sites list and see if there is any difference from the applicative version:
The output is something like this:
Again, the downloads are happening in series. And again, the time per run is almost twice that of the applicative version.
And now the moment we've been waiting for! Will it skip the downloading after the first bad site?
The output is something like this:
Success! The error from the first bad site prevented the rest of the downloads, and the short run time is proof of that.
Summary
In this post, we worked through a small practical example. I hope that this example demonstrated that map, apply, bind, traverse, and sequence are not just academic abstractions but essential tools in your toolbelt.
In the next post we'll working through another practical example, but this time we will end up creating our own elevated world. See you then!
Last updated
Was this helpful?