Adventures in F#--Lazy Like a Fox

Jomo Fisher--This is the next part of the open-notebook series documenting my exploration of F#. Today, I'm looking at the lazy keyword. When an F# function is invoked its value is immediately evaluated. F# does not do lazy evaluation by default. However, laziness can be useful and F# provides an easy way to make a lazy function:

let f n = lazy (n+1)

This represents a function named 'f' that takes a parameter named 'n'. It returns a placeholder for the result of adding 1 to n. If the actual value is needed later during program evaluation then it is computed then. If the value is never needed then its never computed.

Turning to Lutz Roeder's Reflector, we can see the equivalent C#:

     public static Lazy<int> f(int n) {
        return new Lazy<int>(new LazyStatus.LazyStatus<int>._Delayed(new f@2(n)));
    }

    // Nested Types
    [Serializable]
    internal class f@2 : FastFunc<Unit, int> {
        // Fields
        public int n;

        // Methods
        public f@2(int n) {
            this.n = n;
        }

        public override int Invoke(Unit _delay1) {
            return (this.n + 1);
        }
    }

That first static function is the 'f' function I wrote in F#. It returns a Lazy<int> which is the placeholder I mentioned earlier.

Next, there's a class called f@2 that inherits from FastFunc<Unit, int>. If f@2 looks like a fishy identifier name to you the reason is that its not a legal C# or F# identifier. It is, however, a legal identifier for the CLR. I think the compiler is choosing this name to avoid conflicts with other identifiers in the project. I came across FastFunc before here--it's a class that represents a function call. This class holds an int called 'n'--this is the value captured from the call to function f.

The first type parameter to FastFunc<,> is Unit (my eyes have confused this with Uint several times now). Functions in F# aren't allowed to take or return no parameters. In the case of our function above, the returned lazy placeholder has had its parameter specified already so the resulting FastFunc needs to accept no parameter. The solution is Unit which is a type that has exactly one possible value (I assume this value is '1' because of the name of the type).

Let's take a peek at the Lazy class in fslib.dll:

     [Serializable, DefaultAugmentation(false), CompilationMapping(SourceLevelConstruct.RecordType)]
    public sealed class Lazy<A> : IStructuralHash, IComparable {
        // Fields
        public LazyStatus.LazyStatus<A> _status;

        // Methods
        public Lazy(LazyStatus.LazyStatus<A> _status);
        public sealed override int CompareTo(Lazy<A> that);
        public override int CompareTo(object that);
        public static Lazy<A> Create(FastFunc<Unit, A> f);
        public A Force();
        public sealed override int GetStructuralHashCode(ref int nRemainingNodes);
        public A SynchronizedForce();

        // Properties
        public bool IsDelayed { get; }
        public bool IsException { get; }
        public bool IsForced { get; }
        [CompilationMapping(SourceLevelConstruct.Field, 0)]
        public LazyStatus.LazyStatus<A> status { get; set; }
        public A Value { get; }
    }

The property Value is what evaluates and returns the computed value. That _status is essentially an enumeration that describes the current state: Delayed, Value, Exception. The 'Delayed' state means the function is currently uncomputed, the 'Value' state means the value has already been computed, finally 'Exception' means that computation was attempted and an exception resulted.

From this, it seems clear that the computation is only performed once and a cached value is returned thereafter. If there was an exception, then this exception is cached and rethrown each time the result is accessed.

This posting is provided "AS IS" with no warranties, and confers no rights.