Inside F#

Brian's thoughts on F# and .NET

An example of the interplay between language features and library design, part two

Posted by Brian on April 17, 2008

Last time we iterated on the design of a dictionary lookup API, discussing how different language features like "generics", "exceptions", and "out parameters" could affect the design of the API.  By the end of the blog entry, we had reached this API:

    // if key not present, returns false
    // else returns true and sets value
    bool TryGetValue(string key, out int value);

which admits this client code:

    int r; 
    if (dict.TryGetValue("Brian", out r)) 
        Console.WriteLine("Brian’s favorite 32-bit integer is {0}", r); 

This design choice solved a large number of problems we encountered while iterating over possibilities in the design space of this simple API.

Nevertheless, there’s something I always found mildly irksome about the TryGetValue API.  It re-introduces a minor deficiency that had been previously solved by the ‘exception version’ of the API.  Recall that the client code in the exception version looks like this:

        int r = dict.GetValue("Brian"); 
        Console.WriteLine("Brian’s favorite 32-bit integer is {0}", r); 
    catch (KeyNotFoundException) 

For all its problems, the exception version of the API has one advantage over "TryGetValue", which is that you can only reference the result "r" when it has a meaningful value.  If the dictionary doesn’t contain the key, an exception is thrown, and the name "r" falls out of scope so there’s no chance that you’ll accidentally try to use it.  Whereas with TryGetValue, I could get careless and write code like:

    int r;   
    if (dict.TryGetValue("Brian", out r))   
        Console.WriteLine("Brian’s favorite 32-bit integer is {0}", r); 
    DoSomethingWith(r);  // oops


    int r;   
    dict.TryGetValue("Brian", out r);  // oops
    Console.WriteLine("Brian’s favorite 32-bit integer is {0}", r); 

The TryGetValue API is well-designed in that its signature steers you in the right direction for proper use, but it’s still possible to accidentally use a meaningless "result".

So let’s do one more iteration on the design of this API…

Pattern-matching to the rescue!

We can introduce another language feature to address this problem.  Pattern matching!  I wrote briefly about pattern-matching in general, and F#’s "option" type in particular, in a previous blog entry.  Let’s put it to use.  Our new API method, now using F# notation, has this signature:

    // TryGetValue : string -> option<int>

where the return type is an option<int>.  And thus the client code looks like:

    match dict.TryGetValue "Brian" with
    | Some(r) -> Console.WriteLine("Brian’s favorite 32-bit integer is {0}", r)
    | None    -> ()

Perfect!  Pattern matching is both a control structure (like an "if" – we either run the code in the Some branch or the None branch) and a binding mechanism (we associate the new name "r" with a value) at the same time.  As a result, this language mechanism directly addresses the final deficiency; now "r" is scoped only to the branch where it is meaningful. 

Whereas the "TryParse" pattern (with a boolean return value and an out parameter) is the preferred design pattern in a language like C#, in languages with pattern-matching, such as F#, the preferred design pattern is simply to return an option<‘a>.  Such an API has all the benefits we described in the previous blog entry, plus one more: you can’t accidentally misuse the API to access a meaningless result.

The end of the story

This time, that really is the end of the story.  However, having just demonstrated this lovely benefit of pattern-matching, I wanted to call out how it relates to writing idiomatic F# code.

Suppose we’re writing a very simple function to print out all the numbers in a list.  A very non-idiomatic way to write this in F# is:

// PrintNums : list<int> -> unit
let rec PrintNums (l : list<int>) =
    if not l.IsNil then
        printfn "%d" l.Head
        PrintNums l.Tail

The part that is most glaringly non-idiomatic is the use if the if-then along with calls to .IsNil, .Head, and .Tail.  You almost never write F# code that looks like this, because it’s easy to make mistakes… in fact, when writing this example up, I actually made exactly such a mistake, originally forgetting the "not" in the if condition.  Clearly, screwing up the if condition will cause the subsequent code to fail.  As a result, it is far more idiomatic to use pattern matching:

// PrintNums : list<int> -> unit
let rec PrintNums l =
    match l with
    | h :: t -> printfn "%d" h
                PrintNums t
    | [] -> ()

Now, if we write the code in terms of "h" and "t" that are bound by the pattern match, we cannot accidentally make the error I made before (accessing the head of the list in the "if nil" branch).  Pattern-matching helps prevent us from writing code containing a certain class of logic errors.  (As a side-benefit, when the code is written this way, we also get type-inference of the parameter type "l".)

The point is, when pattern matching is available, you should use it in preference to writing your own control constructs (e.g. if-then-else) or your own code to select portions of the data (e.g. accessing .Head), because the pattern-matching language syntax and type system are designed to constrain the ways you can write code so that it is much harder to make errors.

(I note finally that a more advanced, but still perfectly idiomatic way to write PrintNums is

// PrintNums : list<int> -> unit
let PrintNums = List.iter (fun x -> printfn "%d" x)

but writing it that way wouldn’t have let me talk about pattern matching.)

3 Responses to “An example of the interplay between language features and library design, part two”

  1. λ said

    hello, I\’m interesting in F# and I\’m going to write about it in my spaces too.

  2. Jonathan said

    Um, why wouldn\’t you just return an Option<T> from your C# dictionary? You can just as easily call Option<T>.SomeValue as you could pattern match on it.

  3. Brian said

    I\’m unclear what you\’re asking – you could add an extension method to IDictionary<K,V> that returns an option, if that\’s what you\’re asking.Yes, you can always defeat the checking by directly calling option<T>.Value without checking .IsSome, but using .Value directly is usually a code smell. The idiomatic way to deal with discriminated unions is to pattern-match over them, and when you use pattern-matching, you cannot accidentally introduce the \’accessed a meaningless value\’ bug.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: