Previous Entry Share Next Entry
Explaining Language Design (Part 4: Lambdas in D)
anime
he_the_great

In part 3 I went over C++ lambda which have the ability to capture variables by values. This is not an option in D, this means there is no parallel to the issues brought up by Scott.

C++
    auto lamPrime2 = [&]() mutable { cxprime = 10; };

The C++ lambda which captures variables by reference automatically is the semantics D provides.

dlang
void main() {
    int cxprime = 1;
    auto lamPrime2 = () { cxprime = 10; };
    lamPrime2();
    assert(cxprime == 10);

Not to complicated here, it is very similar to the C++ version above in both syntax and semantics. So I'll move on to D and lambda syntax.

dlang
    lamPrime2 = delegate void() { cxprime = 10; }; // 1

    lamPrime2 = delegate() { cxprime = 20; }; // 2

    lamPrime2 = () { cxprime = 30; }; // 3

    lamPrime2 = { cxprime = 40; }; // 4

    auto lamPrime3 = () => cxprime = 50// 5
    assert(is(typeof(lamPrime3) : int delegate()));

WTF D has five ways to declare a lambda! Actually there is also function and I've got a library option to go over too.

The majority of these are just reductions of the full delegate grammar. The return type isn't required to be specified, the delegate can be dropped (function can be used when a context pointer isn't needed and want compile time checks). With a zero argument delegate the parentheses can also be dropped leaving just the curly braces. This essentially comes from the desire to make writing delegates more appealing. The full grammar is way too long to drive people to use them inline.

The 5th option is fairly recent and unapologetically stolen from C#. The main issue with all the previous options was that a return statement was required if a value needed to be provided by the lambda. The library option just wasn't appealing enough (only handled simple cases). This lead to implementing the lambda syntax from C# which will return the last statement. For this reason it is not the same as the previous versions which were void delegate().

I will not be explaining the is Expression.

dlang
    import std.functional : binaryFun, unaryFun;
    alias lamPrime4 = binaryFun!"a + b";
    static assert(lamPrime4(2,2) == 4);

The library function isn't really a lambda, it is a template. This means we can't store our lambda into a variable. It also only comes in two forms, binary (two arguments) and unary (one argument. And most importantly, it was a string! It does have the benefit that you can define it once and utilize it multiple times at compile-time.

dlang
    auto lamPrime5 = (int a, int b) => a + b;
    static assert(((a, b) => a + b)(2,2) == 4);
}

To store a lambda, the parameters must be defined since this is a runtime variable and the compiler needs to be able to type check its usage. To use the lambda at compile-time it must be know at compile-time, lamPrime5 is not. These really aren't huge limitations that will cause someone to reach for std.function.binaryFun. As I'll show, D provides some decent template options.

dlang
void definedDelegate(void delegate(int) foo) {}

This is just an ordinary function which takes a delegate, the delegate takes an int and will return void.

dlang
void byNameDelegate(alias lmda)() {}

This template utilizes the alias parameter, which provides the parameter by name at compile-time.

dlang
void lambdaTypeInference() {
    definedDelegate((var) { var = 10; });
    byNameDelegate!((var) { var = 10; });
}

The define functions are now called with a very unspecific delegate. I didn't declare the type of var, nor the return value. The compiler doesn't complain. When I call definedDelegate the compiler knows the needed type and tries to "instantiate" my lambda, if the body has any type violations compilation will fail.

By passing the lambda as an alias parameter the compiler doesn't do anything with it. It is waiting for byNameDelegate to utilize it, however it has an empty body. If definedDelegate were called inside the function then the compiler would then have to instantiate lmda (which is my passed in lambda).

Conclusion

I just spent a lot of time explaining tool use and not tool application. I think this is of value to those familiar with a tool and just need to know how a given language uses it. From all I've gone over here, I think teaching application would make it clear how to use the tool. Aside from the multitude of syntax, the semantics are fairly straight forward.

  1. Part 1: Default Initialization
  2. Part 2: Const Inference
  3. Part 3: Lambdas in C++
  4. Part 4: Lambdas in D
  5. Part 5: Type Inference
  6. Part 6: Inheritance
  7. Part 7: Algorithm Complexity
  8. Part 8: Essential Complexity

?

Log in