The AsyncArrow

All text and code copyright (c) 2016 by Troy Kershaw. Used with permission.

Original post dated 2016-12-14 available at https://troykershaw.com/the-asyncarrow/

By Troy Kershaw

'Tis the season for giphing, so for my F# advent post this year we're going to get the top trending gif on giphy.

That's one excited girl

Giphy has a nice little api, all we need to do is send a request, then we'll get a response. Sounds pretty straight forward, huh? In fact, you've probably created a function that takes a request and returns an asynchronous result before, à la 'Req -> Async<'Res>. Interestingly, in doing so, you've defined the 'Arrow'.

John Hughes, in his paper Generalising Monads to Arrows, describes an arrow as 'a -> M<'b> where M is any monad. We treat this function as a single type until it's time to apply the argument. In fact, you can create a type alias for the async arrow like so:

type AsyncArrow<'a,'b> = 'a -> Async<'b>

I won't be using this type alias for any actual work in this post, but visualising it like this certainly helped me gain an understanding of how they work.

What you'll see in this post is that by representing our async operations as arrows we get monad like properties on the input/output group.

Below is a bit of setup for your fsx file if you're following along and running the code. There's a few functions to help us work with Async and a few functions in an AsyncArrow module. Don't worry about those yet, we'll look at how they work later.

#r "System.Net.Http"

open System.Net.Http  
open System.Text.RegularExpressions  
open System.Diagnostics

// These aliases are just so that I don't wear out my keyboard.
type HttpReq = HttpRequestMessage  
type HttpRes = HttpResponseMessage

// We're calling giphy, remember?
let giphyTrending = "http://api.giphy.com/v1/gifs/trending"

// A few Async functions to make bind and map easy to use
type Async with  
  static member bind (f:'b -> Async<'c>) (a:Async<'b>) : Async<'c> =
    async.Bind(a, f)
  static member map (f:'b -> 'c) (a:Async<'b>) : Async<'c> =
    async.Bind(a, f >> async.Return)

// Don't worry too much about this yet, we'll get to it later
module AsyncArrow =  
  let mapOut (f:'b -> 'c) (a:'a -> Async<'b>) : 'a -> Async<'c> =
    a >> Async.map f

  let mapOutAsync (f:'b -> Async<'c>) (a:'a -> Async<'b>) : 'a -> Async<'c> =
    a >> Async.bind f

  let mapIn (f:'a2 -> 'a) (a:'a -> Async<'b>) : 'a2 -> Async<'b> =
    f >> a

  let after (f:'a * 'b -> _) (g:'a -> Async<'b>) : 'a -> Async<'b> =
    fun a -> g a |> Async.map (fun b -> let _ = f (a,b) in b)

I have a little request...

Okay, now that's out of the way, let's write a function that takes a HttpReq and returns an Async<HttpRes>.

let makeHttpReq : HttpReq -> Async<HttpRes> =  
  fun (req:HttpReq) -> async {
    use client = new HttpClient ()
    return! client.SendAsync req |> Async.AwaitTask }

See the HttpReq -> Async<HttpRes> up there? That's an arrow. By the end of this post you'll hopefully be an arrow fan and you'll start seeing the 'a -> M<'b> pattern everywhere.

We could have written the function signature with the AsyncArrow type alias to be really obvious that we're returning an arrow.

let makeHttpReq : AsyncArrow<HttpReq, HttpRes> = ...

Personally I find HttpReq -> Async<HttpRes> easier to read and I use this pattern through the rest of the post.

Back to our makeHttpRequest function. You can run it simply by applying the input argument, in this case a HttpReq.

new HttpReq (HttpMethod.Get, giphyTrending)  
|> arrow
|> Async.RunSynchronously

That's nice, it works, but I don't care for anything in the HttpRes, I just want the content. Let's get that as a string next.

Map out a route

In our AsyncArrow module above we have a mapOutAsync function that looks like this:

val mapOutAsync :  
  f:('b -> Async<'c>) -> a:('a -> Async<'b>) -> ('a -> Async<'c>)

Let's follow the types and see how it works.

If we put our input a type and the output type next to each other it becomes obvious how to make it work.

'a -> Async<'b> // this is the input 'a'  
'a -> Async<'c> // this is our output

We can clearly see that a solution is to require a function 'b -> 'c, and that's exactly what the mapOut function is defined as in the AsyncArrow module.

Another solution is to require a function 'b -> Async<'c>, which is what we need here, and it lines up perfectly with the signature for mapOutAsync.

let arrow =  
    makeHttpRequest
    |> AsyncArrow.mapOutAsync (fun res ->
        res.Content.ReadAsStringAsync ()
        |> Async.AwaitTask)

And we call it simply by giving it a Req

new HttpReq (HttpMethod.Get, giphyTrending)  
|> arrow
|> Async.RunSynchronously

You can call it the same way as we did above.

Once we accept our limits, we go beyond them

  • Albert Einstein

Let's be good citizens and add in an Accept header.

What we want to do here is create a new arrow that takes an HttpReq, adds the Accept header to the request and then applies the request to the original arrow, which will return an Async<HttpRes>.

The function that does this is called mapIn...

val mapIn : f:('a2 -> 'a) -> a:('a -> Async<'b>) -> ('a2 -> Async<'b>)

...and here's how you use it.

let arrow =  
    makeHttpRequest
    |> AsyncArrow.mapOutAsync (fun res ->
        res.Content.ReadAsStringAsync () |> Async.AwaitTask)
    |> AsyncArrow.mapIn (fun (req:HttpReq) ->
        req.Headers.Add ("Accept", "application/json")
        req)

May your inputs be forever changing

Let's do another mapIn, but this time change it's type. It's now time to get rid of that pesky HttpReq.

let arrow =  
    makeHttpRequest
    |> AsyncArrow.mapOutAsync (fun res ->
        res.Content.ReadAsStringAsync () |> Async.AwaitTask)
    |> AsyncArrow.mapIn (fun (req:HttpReq) ->
        req.Headers.Add ("Accept", "application/json")
        req)
    |> AsyncArrow.mapIn (fun (url:string) ->
        new HttpRequestMessage(HttpMethod.Get, url))

We call this by passing in a string url.

giphyTrending  
|> arrow
|> Async.RunSynchronously

We're getting errors about authentication, let's add the api key to the request with another mapIn.

let arrow =  
    makeHttpRequest
    |> AsyncArrow.mapOutAsync (fun res ->
        res.Content.ReadAsStringAsync () |> Async.AwaitTask)
    |> AsyncArrow.mapIn (fun (req:HttpReq) ->
        req.Headers.Add ("Accept", "application/json")
        req)
    |> AsyncArrow.mapIn (fun (url:string) ->
        new HttpRequestMessage(HttpMethod.Get, url))
    |> AsyncArrow.mapIn (fun url -> url + "?api_key=dc6zaTOxFJmzC")

Run that and you'll get a lovely json response.

The home stretch

Let's get the first url from the list using mapOut.

let arrow =  
    makeHttpRequest
    |> AsyncArrow.mapOutAsync (fun res ->
        res.Content.ReadAsStringAsync () |> Async.AwaitTask)
    |> AsyncArrow.mapIn (fun (req:HttpReq) ->
        req.Headers.Add ("Accept", "application/json")
        req)
    |> AsyncArrow.mapIn (fun (url:string) ->
        new HttpRequestMessage(HttpMethod.Get, url))
    |> AsyncArrow.mapIn (fun url -> url + "?api_key=dc6zaTOxFJmzC")
    |> AsyncArrow.mapOut (fun res ->
        Regex.Match(res, "\"url\":\"(.+?)\"").Groups.[1].Value.Replace ("\\", ""))

Sorry about the regex, I didn't want to add a dependency on a Json parser.

After all is said and done...

Let's open the gif in your browser.

let arrow =  
    makeHttpRequest
    |> AsyncArrow.mapOutAsync (fun res ->
        res.Content.ReadAsStringAsync () |> Async.AwaitTask)
    |> AsyncArrow.mapIn (fun (req:HttpReq) ->
        req.Headers.Add ("Accept", "application/json")
        req)
    |> AsyncArrow.mapIn (fun (url:string) ->
        new HttpRequestMessage(HttpMethod.Get, url))
    |> AsyncArrow.mapIn (fun url -> url + "?api_key=dc6zaTOxFJmzC")
    |> AsyncArrow.mapOut (fun res ->
        Regex.Match(res, "\"url\":\"(.+?)\"").Groups.[1].Value.Replace ("\\", ""))
    |> AsyncArrow.after (fun (input, output) ->
        Process.Start output)

What is really nice about the after function is that you get the output, but also the input that caused the output. Very helpful for logging.

Next steps

Conferences

I'll be speaking about the AsyncArrow at NDC London, 16 - 20 January 2017, so come and listen and say hi.

Projects

Finagle by Twitter While different to our use in this post, the 'Req -> Async<'Res> signature implements a server.

Jet is planning to open source our AsyncArrow module. We're also discussing naming and there's a chance it will be released as AsyncFunc, so look out for that too.

Papers

People

I was introduced to arrows by Leo Gorodinski, and he said you could reach out to him.

Feel free to reach out to me as well.


Fun, huh?

results matching ""

    No results matching ""