Part of the "Why use F#?" series (more)

In this example, we will compare the F# and C# code for downloading a web page, with a callback to process the text stream.

We'll start with a straightforward F# implementation.

// "open" brings a .NET namespace into visibility
open System.Net
open System
open System.IO

// Fetch the contents of a web page
let fetchUrl callback url =        
    let req = WebRequest.Create(Uri(url)) 
    use resp = req.GetResponse() 
    use stream = resp.GetResponseStream() 
    use reader = new IO.StreamReader(stream) 
    callback reader url

Let's go through this code:

  • The use of "open" at the top allows us to write "WebRequest" rather than "System.Net.WebRequest". It is similar to a "using System.Net" header in C#.
  • Next, we define the fetchUrl function, which takes two arguments, a callback to process the stream, and the url to fetch.
  • We next wrap the url string in a Uri. F# has strict type-checking, so if instead we had written: let req = WebRequest.Create(url) the compiler would have complained that it didn't know which version of WebRequest.Create to use.
  • When declaring the response, stream and reader values, the "use" keyword is used instead of "let". This can only be used in conjunction with classes that implement IDisposable. It tells the compiler to automatically dispose the resource when it goes out of scope. This is equivalent to the C# "using" keyword.
  • The last line calls the callback function with the StreamReader and url as parameters. Note that the type of the callback does not have to be specified anywhere.

Now here is the equivalent C# implementation.

class WebPageDownloader
{
    public TResult FetchUrl<TResult>(
        string url,
        Func<string, StreamReader, TResult> callback)
    {
        var req = WebRequest.Create(url);
        using (var resp = req.GetResponse())
        {
            using (var stream = resp.GetResponseStream())
            {
                using (var reader = new StreamReader(stream))
                {
                    return callback(url, reader);
                }
            }
        }
    }
}

As usual, the C# version has more 'noise'.

  • There are ten lines just for curly braces.*
  • There is the added visual complexity of 5 levels of indenting.
  • All the parameter types have to be explicitly declared, and the generic TResult type has to be repeated three times.

* Yes, in this particular case, some of the braces and indenting could be removed, but in the general case they are needed.

Testing the code

Back in F# land, we can now test the code interactively:

let myCallback (reader:IO.StreamReader) url = 
    let html = reader.ReadToEnd()
    let html1000 = html.Substring(0,1000)
    printfn "Downloaded %s. First 1000 is %s" url html1000
    html      // return all the html

//test
let google = fetchUrl myCallback "http://google.com"

Finally, we have to resort to a type declaration for the reader parameter (reader:IO.StreamReader). This is required because the F# compiler cannot determine the type of the “reader” parameter automatically.

A very useful feature of F# is that you can "bake in" parameters in a function so that they don’t have to passed in every time. This is why the url parameter was placed last rather than first, as in the C# version. The callback can be setup once, while the url varies from call to call.

// build a function with the callback "baked in"
let fetchUrl2 = fetchUrl myCallback 

// test
let google = fetchUrl2 "http://www.google.com"
let bbc    = fetchUrl2 "http://news.bbc.co.uk"

// test with a list of sites
let sites = ["http://www.bing.com";
             "http://www.google.com";
             "http://www.yahoo.com"]

// process each site in the list
sites |> List.map fetchUrl2 

The last line (using List.map) shows how the new function can be easily used in conjunction with list processing functions to download a whole list at once.

Here is the equivalent C# test code:

[Test]
public void TestFetchUrlWithCallback()
{
    Func<string, StreamReader, string> myCallback = (url, reader) =>
    {
        var html = reader.ReadToEnd();
        var html1000 = html.Substring(0, 1000);
        Console.WriteLine(
            "Downloaded {0}. First 1000 is {1}", url,
            html1000);
        return html;
    };

    var downloader = new WebPageDownloader();
    var google = downloader.FetchUrl("http://www.google.com",
                                      myCallback);
            
    // test with a list of sites
    var sites = new List<string> {
        "http://www.bing.com",
        "http://www.google.com",
        "http://www.yahoo.com"};

    // process each site in the list
    sites.ForEach(site => downloader.FetchUrl(site, myCallback));
}

Again, the code is a bit noisier than the F# code, with many explicit type references. More importantly, the C# code doesn't easily allow you to bake in some of the parameters in a function, so the callback must be explicitly referenced every time.

Comments

blog comments powered by Disqus