back

by uecker·1d ago·view on hn ↗
Lambdas are just anonymous nested functions. But I like named nested functions more because they are more readable and would prefer them in most cases. Ideally you have both as most languages have.

I always wondered why C++ only added lambdas, but observing WG21 for a while, I assume this is just a random walk in language design. (not that it is different in WG14)

4 comments
> Lambdas are just anonymous nested functions.

The important feature of lambdas is that they are expressions, not that they lack a name. The advantage of function expressions is you can write the body of the function exactly at the place where it is used. With GCC nested functions you either have to write the body of the function before its first use or else write the declaration of the function twice.

This matters for long chains of continuation passing:

  foo(arg1, arg2, [](){
          // do some work
          bar(arg3, arg4, [](){
                  // do some more work
                  baz(arg5, arg6, [](){
  
                  });
          });
  });
Compare to the following, where the control flow is all out of order:

  void cb(void){
          // Do some work
          void cb2(void){
                  // do some more work
                  void cb3(void){

                  }
                  baz(arg5, arg6, cb3);
          }
          bar(arg3, arg4, cb2);
  }
  foo(arg1, arg2, cb);
I agree with your point.

But I usually prefer the later anyway, because the code usually is not as nested anyway and having a name is often helpful, and also because I find the nested code with lambdas also not too readable. Other languages have better syntax for chaining functions in this way, i.e. with lambdas I would like to write like this:

  foo(arg1, arg2, _)
     .(int(int x)) { ... }
     .(int(int y)) { ... };
(edit: or something, I think I got it a bit wrong, but you get the idea)

But I agree, sometimes lambdas are better so it would be good to have both.

(There is the classical hack to define lambdas using statement expressions and nested functions.)

I can write numbers like three by just writing 3 in my code. When I want a named number I use a syntax like x = 3. Why should functions be any different? A language doesn't need different ways to name things for each type of thing. Integers, strings, functions etc: they can all use the same mechanism for naming.
Fair enough, But I am of the opinion that blocks of code and magic numbers should almost always be named, There is a lot of intent that is lost when the name is skipped.

It is also my main complaint about traditional math notation, They enjoy trying to make the notation as concise as possible, single letter symbols, embedded subparts, no names for anything. I understand that having super a tight concise notation is a massive benefit while in the flow state, thinking about the problem. But man it makes it rough for us casual math enjoyers coming in fresh to the topic.

I agree if your language is designed like this from the beginning as functional languages are, but in C you already have different syntax for functions. (edit: rephrased)
No exactly. "Lambdas" are usually function closures [1]. Which do not exist in C and were quite "late" in C++, because decent support of closures require automatic memory management (GC).

C++ lambda/closures are a bit clunky because you have to specify if the captures are by reference or by value, and you're better of having a good idea of what you're doing.

[1] https://en.wikipedia.org/wiki/Closure_(computer_programming)

GCC's nested functions can also capture variables by reference. A closure combines the function with the environment which is essentially what my wide pointer is that contains the static chain that points to the environment. But for full support of first-class functions you would want return functions even below the level of where the captured variables live which is not possible with GCC's nested functions because they are on the stack and then go out of scope. So yes, this would require moving them to the heap and generally GC. Which is why the attempts to put C++'s "lambdas" into C are problematic, because naively copied lambda design will work even less well in C compared to C++ where you at least have smart pointers.

If you are interested, I explore the design space here. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3654.pdf

Lambda expressions in C++ are simply syntactic sugar for defining function objects (aka functors): structs that overload operator() so you can call them as functions. Once you realize this, their features and limitations become immediately clear.

For example, here is a typical use of a lambda expression to filter a vector of values:

    #include <iostream>
    #include <vector>

    int main() {
        std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5};

        int threshold = 5;
        std::erase_if(v, [&](int i) { return i < threshold; });

        // prints 5 9 6 5
        for (int i : v) std::cout << i << '\n';
    }

The lambda expression is essentially shorthand for:

        ...
        int threshold = 5;
        struct lambda_t {
            int &threshold;
            bool operator()(int i) {
                return i < threshold;
            }
        };
        std::erase_if(v, lambda_t{threshold});
        ...
You could always do this in C++. The added value of the lambda expression syntax is that the compiler generates the boilerplate, and generates a unique name for lambda_t.

The important takeaway is that every lambda expression corresponds with a unique type that is _not_ a function type, but a class type. Consequently, lambda expressions can only be passed to template functions like std::erase_if, which are parameterized with the callback type.

You cannot pass a lambda expression to a function that expects a function pointer (e.g. bool(*)(int) in this example), and that's where they differ from GCC-style nested functions, which actually behave like functions. It also explains why lambda expressions don't need a trampoline.

As an aside, you _can_ pass lambdas to non-generic functions using a type-erasing wrapper like std::function, but std::function is itself a class type too, so that still doesn't allow you to convert it to a plain function pointer.

Finally, you can of course assign a name to a lambda expression value, using this common pattern:

    auto greet = [](const char *name) { std::cout << "Hello " << name << "!\n"; }
    greet("Alice");
    greet("Bob");
(Note that `auto` is necessary here because there is no way to explicitly refer to the compiler-generated name for the lambda type.)

This is the closest you can get to a local function definition in C++. Admittedly the syntax is a little odd. You might wonder why there wasn't some additional syntactic sugar to make the definition look more normal. I suspect that wasn't a random decision, but rather intentionally avoiding conflicts with existing language extensions like GCC's local function syntax.

>You cannot pass a lambda expression to a function that expects a function pointer (e.g. bool(*)(int) in this example)

To be pedantic, you can: as long as the lambda doesn't close over any local variable, the object will decay to a function pointer.

Regarding function syntax in C++: GCC's C++ frontend does not support nested functions. But more importantly, even if it did, I do not think there would be any conflict at all. While lambdas are lowered to function objects with an unique anonymous type in C++, the semantics of a lambda that uses lvalue capture

  auto f = [&](int x) - > int { return x + z; };
is the same as GCC's nested function

  int f(int x) { return x + z; }
except that latter can be converted via a trampoline to a regular function pointer (and maybe the observable type). But you could do just the same with a lambda using a trampoline! In any case, there is no conflict, either this conversion is allowed and one needs some hack to make it work such as a trampoline or it is not.

So in C++ you could simply lower such nested functions to lambdas and it would cause no confusion with GCC's nested functions at all, because from a user's point of view they would work identically.

> lambda expressions can only be passed to template functions

> there is no way to explicitly refer to the compiler-generated name for the lambda type.

"Voldemort" types. While intellectually I get the explanation for why C++/Rust lambdas are like this, I still strongly dislike them. Occasionally being unable to even articulate what something is feels like a failure in language design.

C recently got type inference via the "auto" keyword and it seemed like almost immediately there was a proposal to add voldemort types to the language.

I know it's not the same, but you can "kind of" name them by using them as a template parameter, then, scoped to the template, you can reference them under the template parameter name.

Anyway, I don't see it mentioned anywhere in this discussion, but imo the area where c++ lambdas shine is the way they interact with copy constructors and move constructors.