Querying Last.fm Web API With F#

Introduction

This blog shows how to query top 50 most listened musicians of last.fm users with F#. The complete source code which sheds more light on function composition, unit testing, property-based testing and railway-oriented programming can be accessed on GitHub.
 
Setting up type providers

Type providers is a useful F# feature that allows strongly-type responses from REST APIs, CSV files, and HTML tables etc. and thus, is useful for REST APIs interaction, data analysis tasks, and much more.

To enable type providers in our project, we perform the following steps.
  1. Import FSharp.Data 
  2. Provide snippet of API response.
  1. let [<Literal>] TopArtistsSample = """{    
  2.    "topartists":{    
  3.       "artist":[    
  4.          {    
  5.             "name":"Porcupine Tree",  
  6.             //skipped for the sake of breivety  
  7.          }  
  8.       ],  
  9.       "@attr":{    
  10.          "user":"Morbid_soul",  
  11.          "page":"1",  
  12.          "perPage":"2",  
  13.          "totalPages":"165",  
  14.          "total":"330"  
  15.       }  
  16.    }  
  17.     }"""  
Create type from the sample above.
  1. type TopArtists = JsonProvider<TopArtistsSample>  
Querying the API

Now, let us define some constants.
  1. [<Literal>]  
  2. let userName = "<login here>"  
  3.   
  4. [<Literal>]  
  5. let apiKey = "<api key here>"  
  6.   
  7. [<Literal>]  
  8. let baseUrl = "http://ws.audioscrobbler.com"  
  9.   
  10. [<Literal>]  
  11. let getTopArtistsPattern = "{0}/2.0/?method=user.gettopartists&user={1}&api_key={2}&period=12month&format=json"  
With these constants, we can construct URL for API call, as shown below.
  1. let path = String.Format(getTopArtistsPattern, baseUrl, userName, apiKey)  
We cast a response to our type defined in "Type providers" section.
  1. TopArtists.Parse(text)  
The complete function call looks like this.
  1. let getTopArtists =   
  2.         let path = String.Format(getTopArtistsPattern, baseUrl, userName, apiKey)  
  3.         let data = Http.Request(path)  
  4.         match data.Body with  
  5.         | Text text -> TopArtists.Parse(text).Topartists.Artist   
  6.         | _ -> null  
Handling errors

You may have noticed that we didn't handle any possible exceptions when performing the call. To do this, we will wrap our result type inside the monadic type which indicates whether the function executed successfully or not.
  1. type Result<'TSuccess,'TFailure> =   
  2.     | Success of 'TSuccess  
  3.     | Failure of 'TFailure  
This can be useful to chain functions in order to handle errors gracefully.

Now, our final function makes use of declared type, as shown below.
  1. let getTopArtists () =   
  2.         try  
  3.             let path = String.Format(getTopArtistsPattern, baseUrl, userName, apiKey)  
  4.             let data = Http.Request(path)  
  5.             match data.Body with  
  6.             | Text text -> Success(TopArtists.Parse(text).Topartists.Artist)  
  7.             | _ -> Failure "getTopArtists. Unexpected format of reponse message"  
  8.         with  
  9.         | ex -> Failure ex.Message