Hacker Timesnew | past | comments | ask | show | jobs | submitlogin

"Zig doesn’t have lambdas"

This surprises me (as a C++ guy). I use lambdas everywhere. What's the standard way of say defining a comparator when sorting an array in Zig?



Normal function declarations.

This is indeed a point which makes Zig inflexible.


By adopting a syntax like

    fn add(x: i32, i32) i32

they have said perma-goodbye to lambdas. They should have at-least considered

    fn add(x: i32, i32): i32


Why “perma goodbye”?

Go has a similar function declaration, and it supports anonymous functions/lambdas.

E.g. in go, an anonymous func like this could be defined as

foo := func(x int, _ int) int { … }

So I’d imagine in Zig it should be feasible to do something like

var foo = fn(x: i32, i32) i32 { … }

unless I’m missing something?


Anonymous functions aren't the same as lambda functions. People in the Go community keep asking for lambda functions and never get them. There should be no need for func/fn and explicit return. Because the arrow would break stuff is one of the reasons.

See

https://github.com/golang/go/issues/59122

https://github.com/golang/go/issues/21498

    res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
             Reduce(func(a, b int) int { return a + b })

vs

    res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)


They could still fix it with arrow functions, but it’s always gonna look weird.

Some other people have tried to explain how they prefer types before variable declarations, and they’ve done a decent job of it, but it’s the function return type being buried that bothers me the most. Since I read method signatures far more often than method bodies.

fn i32 add(…) is always going to scan better to me.


OK, but with generics return type first tends to becomes a monster.


I used to be very enthusiastic about generic types. Now, well what else would you do? I don’t mean that as a rhetorical question. If someone came up with another way to represent functions that can take multiple types and knows what it will return, I’d be all over them.

Elixir is trying something, I don’t know yet whether it will be better. But their solution is based on a decision about how to do overloading that I suspect makes for maintenance problems later. So it’s gonna have to be good to offset the consequence.


You can declare an anonymous struct that has a function and reference that function inline (if you want).

There's a little more syntax than a dedicated language feature, but not a lot more.

What's "missing" in zig that lambda implementations normally have is capturing. In zig that's typically accomplished with a context parameter, again typically a struct.


So basically, Zig doesn't have lambdas, but because you still need lambdas, you need to reinvent the wheel each time you need it?

Why don't they just add lambdas?


> So basically...

Well, not really.

Consider lambdas in C++ (that was the perspective of the post I replied to). Before lambdas, you used functors to do the same thing. However, the syntax was slightly cumbersome and C++ has the design philosophy to add specialized features to optimize specialized cases, so they added lambdas, essentially as syntactic sugar over functors.

In zig the syntax to use an anonymous struct like a functor and/or lambda is pretty simple and the language has the philosophy to keep the language small.

Thus, no need for lambdas. There's no re-inventing anything, just using the language as it designed to be used.


Because to use lambdas you're asking the language to make implicit heap allocations for captured variables. Zig has a policy that all allocation and control flow are explicit and visible in the code, which you call re-inventing the wheel.

Lambdas are great for convenience and productivity. Eventually they can lead to memory cycles and leaks. The side-effect is that software starts to consume gigabytes of memory and many seconds for a task that should take a tiny fraction of that. Then developers either call someone who understands memory management and profiling, or their competition writes a better version of that software that is unimaginably faster. ex. https://filepilot.tech/


Nothing about lambdas requires heap allocation. See also: C++, Rust


If the lambda captures some value, and also outlives the current scope, then that captured value has to necessarily be heap allocated.


No, (in C++) the lambda can capture the the variable by value, and the lambda itself can be passed around by value. If you capture a variable by reference or pointer that your lambda outlives, your code got a serious bug.


And in Rust, it will enforce correct usage via the borrow checker - the outlive case simply will not compile.

If you do want it, you have the option to, say, heap allocate.


That would be a bug, so just... don't do that?

If you return a pointer to a local variable that outlives the scope, the pointer would be dangling. Does that mean we should ban pointers?

If you close over a pointer to a local variable that outlives the scope, the closure would be dangling. Does that mean we should ban closures?


Same as C, define a named function, and pass a pointer to the sorting function.


Unlike C you can stamp out a specialized and typesafe sort function via generics though:

https://ziglang.org/documentation/master/std/#std.sort.binar...


And what if you need to close over some local variable?


Not possible, you'll need to pass the captured variables explicitly into the 'lambda' via some sort of context parameter.

And considering the memory management magic that would need to be implemented by the compiler for 'painless capture' that's probably a good thing (e.g. there would almost certainly be a hidden heap allocation required which is a big no-no in Zig).


As far as I know lambdas in C++ will not heap allocate. Basically equivalent to manually defining a struct, with all your captures, and then stack allocating it. However if you assign a lambda to a std::function, and it's large enough, then you may get a heap allocation.

Making all allocations explicit is one thing I do really like about Zig.


If the lambda is a value type, you can just store whatever captures you want in the fields of this type, no need for heap allocations - they'll go on the stack just like anything else. You can even ask the user to explicitly specify which variables to capture, like in C++ lambdas, to be very explicit about the size of the lambda structure.


Why would capturing require a heap allocation? Neither Rust nor C++ do this.


You need to store the captured data somewhere if the lambda is called after the outer function returns. AFAIK C++ (or rather std::function) will heap-allocate if the capture size goes above some arbitrary limit (similar to small-string optimizations in std::string). Not sure how Rust handles this case, probably through some "I can't let you do that, Dave" restrictions ;)


The trick is that “if”. Rust won’t ever automatically heap allocate, but if your closure does get returned to a place where the capture would be dangling, it will fail to compile. You can then choose to heap allocate it if you wish, or do something else instead.

Heap allocating them is fairly rare, because most usages are in things like combinators, which have no reason to enlarge their scope like that.


But that's not a closure, a closure/lambda is not an std::function. It's its own type, basically syntactic sugar for a struct with the captures vars and operator().

Of course if you want to store it on a type-erased container like std::function then you may need to heap allocate. Rust's equivalent would be a Box<dyn Fn>.


...or escape analysis and lifetimes, but we already have Rust %)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: