Monday, December 24, 2007

Encapsulating the Lazy-Load Pattern

In a previous post I suggested a helper method to wrap the Lazy-Load pattern. I'm now convinced that this was the wrong approach to take. For the specific case I mentioned (lazy-loading a property value), the straightforward test for nil is irreducibly simple. Trying to wrap it in a generic procedure introduces more complexity than it alleviates.

For the more general case in which you have a value which you don't want to compute more than once, the correct pattern is to create a generic wrapper class or method which bolts added functionality onto the type in question. This is a common pattern in the .Net framework: some examples are Nullable<T>, Expression<T>, and Future<T> in the next version of Chrome.

So what we want is some sort of Lazy which will wrap the lazy evaluation. There is one example implementation here. You can consult the article for the (mostly trivial) implementation details. But it enables you to write code like this:

    1 int a = 22, b = 20;

    2 var lazy = Lazy.New(() => {

    3     Console.WriteLine("calculating...");

    4     return new { Mul = a*b, Sum = a+b };

    5 });

    6 Console.WriteLine("Mul = {0}, Sum = {1}",

    7 lazy.Value.Mul, lazy.Value.Sum);

The block passed to Lazy.New is evaluated only once, in the first call to lazy.value. The Lazy.New function is a helper which basically exists to trick the compiler into providing type inference for us so that we don't have to specify the generic parameter.

The main downside to this is that Lazy<T> is a custom class. In order for a function to accept one of these lazy objects as a parameter, it needs to be written specifically with that type in mind. Wouldn't it be cleaner if Lazy.New returned an Func<T> delegate instead?

Well, it can. What we basically want to do is memoize a function of no arguments. And for that the solution presented here is much cleaner:

    1 static class FuncLib

    2 {

    3     public static Func<R> Memoize<R>(this Func<R> f)

    4     {

    5         R value = default(R);

    6         bool hasValue = false;

    7         return () =>

    8         {

    9             if (!hasValue)

   10             {

   11                 hasValue = true;

   12                 value = f();

   13             }

   14             return value;

   15         };

   16     }

   17 }

Let's walk through this. Memoize<T> takes a function with no parameters, and returns a function of the same signature. Here's where it gets sneaky: whereas Lazy<T> defined a new class to hold the cached function value, Memoize<T> takes advantage of the fact that closures have access to their containing scope. The return function carries around a pointer to the local variables value and hasValue, which allows it to remember if the result has been calculated or not.

As an added bonus Memoize<T> is an extension method, which allows us to use it in two ways: either as a helper method similar to Lazy.New, or as a method on a Func<T> object. We can then create lazily-evaluated functions like so:

   32 //using Memoize as a helper method, with type inference

   33 var lazy = FuncLib.Memoize( () =>

   34 {

   35     Console.WriteLine( "calculating..." );

   36     return 42;

   37 } );


   39 //using Memoize as an extension method,

   40 //  but without type inference

   41 Func<int> lazy2 = ( () =>

   42 {

   43     Console.WriteLine( "calculating..." );

   44     return 42;

   45 } );

   46 lazy2 = lazy2.Memoize();


   48 Console.WriteLine( "starting..." );

   49 Console.WriteLine( "result = {0}", lazy() );

   50 Console.WriteLine( "result (again) = {0}", lazy() );

   51 Console.Read();

This can't be beaten for elegance, in my opinion.

No comments: