« Reactive Extensions - Wellington .NET user group | Main | F# computation expressions for beginners, part 2: keeping it simple »

December 12, 2010

F# computation expressions for beginners, part 1: what’s the problem?

One of F#’s most intriguing and baffling features is computation expressions, also known as workflows.  Computation expressions are intriguing because they override the normal F# control flow, allowing user code to decide when and if to proceed to the next step.  And they’re baffling for a number of reasons, but the first is probably, ‘why the heck would I want to override the normal F# control flow?’

To answer that, let’s look at a couple of examples.

Null-safe member calls

If I had a dollar for every time someone asked for a null-safe member operator in C#, I’d have $18.50.  The idea is that if I should be able to write a long chain of member calls in C#, but if one of the intermediate calls returns null then the whole expression would be null instead of throwing an exception.

    string ppname = p.Partner.Puppy.Name;
   
// but what if p or p.Partner or p.Partner.Puppy is null?

A few people have suggested that a new operator, typically written .? or ?., be added to C# to handle this situation.

    string ppname = p.?Partner.?Puppy?.Name;

And maybe these people will get lucky in 2015 or whenever, but what if you need to write some code before 2015?  You have to do a bunch of explicit control flow:

    string ppname = null;
    if (p != null)
    {
      if (p.Partner != null)
      {
        if (p.Partner.Puppy != null)
        {
          ppname = p.Partner.Puppy.Name;
        }
      }
    }

This works fine, but (a) is boring to write and (b) obscures the core logic (the partner’s puppy’s name) in the mess of if statements.

Asynchronous programming

How do you access a remote or slow resource in C# or Visual Basic, without blocking?  Through the joy of callbacks, of course.

    private static void GetWebStuff1()
    {
      WebClient client = new WebClient();
      client.DownloadStringCompleted += OnDownloadStringCompleted;
      client.DownloadStringAsync(new Uri("http://www.google.co.nz/"));
    }

    private static void OnDownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs e)
    {
      Console.WriteLine(e.Result);
    }

As has been observed by everyone and his dog, the problem with this is that it splits the logical flow (download this page and write it to the console) across multiple methods, making it hard to understand and reason about.

As with the null-safe chain, it’s possible to address this at the language level, and in fact C# 5.0 will do exactly that via the async and await keywords:

    private static async void GetWebStuff2()
    {
      WebClient client = new WebClient();
      string response = await
client.DownloadStringTaskAsync(new Uri("http://www.google.co.nz/"));
      Console.WriteLine(response);
    }

This will be jolly nice when it arrives, but once again, it puts developers at the mercy of the language designers.  Even if the C# team do decide to support your control flow scenario, you’ll probably have to wait a few years before that support arrives.  By which time your project has long since failed and you are living in a shopping trolley and yelling at clouds.

Computation expressions

On the surface, these two examples seem entirely unrelated.  But both of them involve wanting to write code in a simple, linear, top-to-bottom or left-to-right style, but being foiled by the fact that you can’t proceed naively from one statement to the next.  In the first example, we want to proceed to the next member call only if the previous one returned non-null; in the second, we want to proceed to the next statement only when the async statement has finished doing its work.

F# computation expressions are an attempt to handle this at a library level instead of a language level.  Roughly speaking, computation expressions allow you to control if and when – and indeed how – control passes from one statement to the next.  (They actually do more than this, but this will do for now.)

And, unlike C#, you can create your own kinds of computation expression to handle control flow idioms that are particular to your own projects.

Which brings us to the second baffling thing about F# computation expressions, which is that they do my head in.  (Technical term.)  Computation expressions are a bit like LINQ.  They’re dead easy and incredibly convenient to use.  But implementing your own kind of computation expression is a whole other matter.  So that’s what I’ll be trying to get my head around in this series.

December 12, 2010 in Software | Permalink

TrackBack

TrackBack URL for this entry:
https://www.typepad.com/services/trackback/6a00d8341c5c9b53ef0147e097c121970b

Listed below are links to weblogs that reference F# computation expressions for beginners, part 1: what’s the problem?:

Comments

Just about every piece of software I start these days includes these extension methods for this scenario:

(Imagine '[' and ']' are less-than and greater-than)

public static TResult Eval[T, TResult](this T obj, Func[T, TResult] func, TResult defaultValue)
{
if (obj == null) return defaultValue;
return func(obj);
}

public static TResult Eval[T, TResult](this T obj, Func[T, TResult] func)
{
return obj.Eval(func, default(TResult));
}

Which allows me to write
string ppname = p.Eval(p => p.Partner)
.Eval(p => p.Puppy)
.Eval(p => p.Name);

which isn't quite as nice as string ppname = p.?Partner.?Puppy.?Name; but it is supported and I don't need to shout at clouds - even though they are bastards - from the comfort of my shopping trolley.

Posted by: Ian Randall at Dec 12, 2010 11:44:42 AM

oops - forgot to put the 'where T : class' constraint on the generic methods...

Posted by: Ian Randall at Dec 12, 2010 11:46:38 AM

Yes, I did originally discuss that approach, but took it out because:

(a) it doesn't actually do what the nested ifs do. In your example, Eval(p => p.Puppy) gets evaluated even if Partner was null -- harmless in this case, but what if Eval had side effects? Or was expensive?

(b) it's a point solution for the specific case of null safe member calls: as soon as you come across another case, such as async or iterators, you have to start over.

(c) I know some people who balk at even such mild warts as brackets in 30.Seconds().Ago(), and I thought that such people would be appalled by the ugliness of writing Eval and lambda boilerplate all over the place and would flame me to bits if I suggested it.

(There is also an issue which is beyond the scope of basic null safety, but becomes highly relevant to ease of use: consider using chained calls to Eval with a "square root if positive" function that took a double to a nullable double. Yes, the forbidden five letter 'M' word is making its unstealthy way towards this conversation.)

I mean, yes, ultimately of course you can do this in C#. You can do pretty much anything in C#. That's not really the point I'm trying to argue. The point is that it's sometimes useful to be able to override control flow, and I'm interested in F#'s notion of a flexible, general, concise, encapsulated mechanism for doing so. Sure, it's always possible to implement custom control flows in C# if you're willing to throw enough lambdas at the problem and don't mind the boilerplate (you'll have read Eric Lippert's series on CPS so you already know how flexible you can get in C#... and how ugly it quickly gets!). My interest is in gaining an understanding of an alternative approach and whether it can help me do this stuff *without* the cruft.

Posted by: Ivan at Dec 12, 2010 12:50:34 PM

Thinking about it, we don't even need to hypothesise an expensive or side-effect-ful Eval to see that the control flow is different from the nested ifs. Consider:

var partner = p.Eval(pp => pp.Partner);
var puppy = partner.Eval(pp => pp.Puppy);
SendToTheKnackers(puppy);
var name = puppy.Eval(pp => pp.Name);

Now SendToTheKnackers will always be called. So I need to put a null check around SendToTheKnackers, and the top-to-bottom flow that Eval restored gets broken up again.

I realise this goes beyond the simple member chaining that I used as the initial motivation, but I hope it clarifies why I'm thinking about control flow as a general issue rather than the specific case I outlined.

Posted by: Ivan at Dec 12, 2010 1:16:43 PM

Sorry, yes - I'd hoped this was implicit, but I probably should've stated clearly:

In no way was this an attempt to detract you from edutaining us with your insights into F#. I realise it sounded like "pfft... why would you even bother - C# gives me all the conditional logic I crave, it empowers me, keeps me safe and warm at night and I shall never forgo its beauty for the false gods of your functional language"

Actually - it was more along the lines of "Fascinating, for whilst I *can* achieve such things in C# it tends to set my OCD off - if you have a better way, I'm all ears!"


Also:
- Yes - 'Eval' is named 'Eval' for brevity, but is more acurately 'SafelyEvaluateEvenIfTheInstanceIsNull', which is a fairly specific point solution.
- Along with 'Eval' I usually write 'Execute' which takes an Action of T, not a Func of T, TResult, so no I don't need to put a null check in SendToTheKnackers, and logically, why would you even *try* and get the name of a puppy that might have just been sent to the knackers? Surely an 'if' statement at this point in the logical flow is justified?
- I, for one, would *never* flame you. Even if I would shout slogans such as "Extension Properties... yeah.." without thinking it through.

Posted by: Ian Randall at Dec 12, 2010 8:30:32 PM

The reason for getting the name of the Puppy after sending it to the knackers is, of course, to print out a comforting message to the Partner. It would be embarrassing to print out that message if they never had a Puppy. Or if you couldn't remember the Puppy's name.

Yes, an if statement is absolutely justified in the example as given. But as the number of checks starts mounting up -- consider a flow of business rules for, say, credit scoring -- the if-based approach leads to potentially deep nesting (or explicit check-and-return after each step). Whereas if you can encapsulate the idea of "quit on failure" at the control flow level, then you may be able to write the code more readably.

This would actually be an interesting example to implement, both in terms of getting my head around how to implement it and in terms of showing how it changes the appearance of code to be more happy-path-tastic. I'll try to put a sample together for a future part -- please hold my feet to the fire on this and thanks for the suggestion!

Posted by: Ivan at Dec 12, 2010 8:55:23 PM

6 weeks later... Are your feet burning yet? ;-)

Posted by: Ian Randall at Jan 23, 2011 9:46:34 AM