The future of lambda delegates

Mikola Lysenko mclysenk at mtu.edu
Wed Aug 16 07:55:38 PDT 2006


D's delegates are great.  They are vastly superior to C++'s confused 
pointer-to-member functions, and much more useful than Java's adapter 
classes.  In the world of system programs, nothing comes close.  One 
interesting type of delegate is the anonymous-nested-delegate-literal, or 
"lambda" delegate.  For those unacquainted with such niceties, here are the 
documentation links:
http://www.digitalmars.com/d/function.html#nested
http://www.digitalmars.com/d/expression.html#FunctionLiteral

Recent D releases (notably 0.161) have updated the syntax and improved the 
overall usability of lambda delegates. All the syntactic sugar is nice, but 
it also lays a few traps for the unwary.  Here is a simple example:

// The Fibonacci numbers are an integer sequence such that
//        F(n) = F(n - 1) + F(n - 2)
//        And F(0) = 0, F(1) = 1
int delegate() fibs()
{
    int a = 0;            // Initialize the last two terms of the Fibonacci 
sequence
    int b = 1;

    return
    {
        int c = a + b;     // Calculate the next term in the sequence
        a = b;               // Shift the previous terms back
        b = c;
        return c;            // Return the result
    };
}

This function returns a function which will sequentially evaluate all of the 
Fibonacci numbers.  Notice that the inner delegate modifies the variables a 
and b in fibs() scope.  Because of this, it is not guaranteed to work after 
fibs returns.  This is most irritating, and it greatly restricts the use of 
this technique. Another potential use for lambda delegates is to create 
one-line adapter methods.  Consider the following attempt to create a button 
which will display an arbitrary message when clicked:

Button createButton(char[] click_msg)
{
    Button b = new Button();
    b.mouseClickCallback = { MsgBox(click_msg); };
    return b;
}

Once more, this sort of method relies on access to the outer function scope 
from within the lambda delegate.  Given the current semantics, this code is 
doomed to fail in any number of random ways.  As a final example, suppose we 
want to perform a lengthy calculation in a separate thread, and only wait 
for the value once it is needed.  In this case, one could try to do the 
following:

int[] delegate() sort_threaded(int[] array)
{
    int[] result = array.dup;

    //Create and run a worker thread to perform an expensive operation, (in 
this case a sort)
    Thread tsort = new Thread(
     {
        result.sort;
        return 0;
     });
    tsort.start();

    //The returned lambda delegate waits for the thread's calculation to 
finish, then returns the result.
    return { tsort.wait; return result; };
}

In this situation, we can let the thread execute in the background while the 
program performs other tasks, and only wait for the result once we need it. 
This type of deferred calculation can be very useful for improving the 
amount of parallelism within an application without adding much 
synchronization overhead.  It is very sad that none of these examples work, 
since there are so many nice solutions just like them.

One possible solution is to allocate the frame for each function containing 
nested-functions on the heap.  This allows any returned delegates to use the 
member variables freely.  One could think of it as translating the function 
into a class with a thunk.  Here is above Fibonacci function rewritten in 
this way:

class fibs_
{
    int a = 0;
    int b = 1;

    int lambda1()
    {
        int c = a + b;
        a = b;
        b = c;
        return c;
    }

    int delegate() func()
    {
        return &lambda1;
    }
}

int delegate() fibs()
{
    return (new fibs_()).func();
}

Such a translation is possible for any of these examples, albeit quite 
tedious.  From what I gather C# applies a similar technique with its lambda 
delegates and inner classes.  For a machine code compiler, such a rewrite is 
not even necessary, since it could simply overwrite the frame pointer at the 
start of the function with a GC allocated block.  This would preserve 
frame-based addressing for arguments and variables, requiring no change in 
any of the internally generated machine code.  The run time overhead is 
fairly low, only on the order of one extra memory allocation per function 
call.  Practically, such an implementation would be extremely simple.

Any thoughts or comments?

-Mikola Lysenko 





More information about the Digitalmars-d mailing list