How is the situation with closures?

Hello everyone!
Today I found out that there is no closure/arrow function in Zig. I found some proposals, all closed, but It’s very difficult for me from an external point-of-view to understand if there is or not the intention to include closures in Zig.

I read https://github.com/ziglang/zig/issues/1717#issuecomment-1627790251 where Andrew rejected the proposal previously accepted, what is the situation right now with the feature? Officially rejected forever?

I’d recommend starting with this thread if you haven’t seen it already: Anonymous functions/lambdas

You’ve probably gathered that it’s a contentious issue because of Zig’s explicit allocation model. Anonymous functions in other languages can be stateful, so we can talk about the “capture” of an anonymous function. Stateless anonymous functions are fairly easy to solve but are verbose (there’s examples in that link I sent you).

One last point about the language here - closures are used quite a lot in Zig (specifically if you are talking about type-erased callbacks). From your original post, it looks like what you’re referring to is a terse-syntax for defining function objects.

4 Likes

Yes, I was thinking about a more compact syntax, but the link you gave me sheds much light on why it is like it is right now and how to do classical closures in Zig.
After reading it, it makes sense after all!

Thank you very much!

3 Likes

Implementing closures in Zig is quite easy. It requires only 5 implementation steps:

  1. Describe the closure’s state type.
  2. Describe the closure’s signature type.
  3. Bind the closure’s state to its implementation.
  4. Describe the function returning the closure.
  5. Export the function name returning the closure.

Look at the article on Zig.new describing how to implement closures in Zig. The article includes a complete example and template.

1 Like

Hey @houghtonap, thanks for the article.

It looks like you’ve got a small bug in your program:

const len = text.len;
...
const last = if (this.end > len) len else end;
return text[first .. last + 1];

If last is equal to text.len, then this will read one index beyond the end of the string.

I’d recommend changing those lines to:

const last = @min(this.end + 1, text.len);
return text[first..last];

Personally, I’d just keep it to non-inclusive ranges to be consistent, but I get that you’re trying to mimic another function :slight_smile:

I think that the example/template, however, is more complex than it needs to be. For instance, we don’t actually need the ClosureScope object. You can pass the integers directly into the function as comptime arguments:

fn foo(comptime begin: usize, comptime end: usize) ClosureType {
  return struct {
      fn closure(text: []const u8) []const u8 {
          return text[@min(begin, text.len)..@min(end + 1, text.len)];
      }
   }.closure;
}

I bring this up not to disparage your article (it’s well written and well intended). I think it’s introducing more steps than are actually necessary.

I also think you should point out that this is for comptime-known parameters. This won’t work for runtime closures (which are doable, but there’s more machinery involved).

4 Likes

Good point about the ranges. I was trying to keep things simple and not to distract from the pattern being presented. The article had already been updated in the Summary section about why it was presented in its verbose form and I supplied a similar simplified example similar to yours. The article also had been updated to remove the comptime restriction and the newly presented pattern works for compile or run time values. Thanks for the feedback, it was appreciated.

1 Like

The comptime restriction is still there in your latest version. It is still generating a new function for every call to ClosureFunc and all parameters must be comptime known.

So, technically, it’s still not a closure. It’s a function template/macro.

The current version as of today works with both compile and runtime values. Pull the example code from the article and run the tests. If there is still some additional use case not covered, then I would welcome a test that demonstrates the issue so I can understand the shortcoming. BTW, in the current version ClosureFunc has been renamed ClosureGenerator to better reflect its functionality and ClosureBind has been renamed ClosureBinder to better reflect its functionality.

Sorry, my previous comment was not quite correct. It’s not still comptime only.

But, it works by storing the bound variables in a struct scoped variable (also called static scope or global scope in other languages). This is not how closures are expected to store their state.

You can break the test by adding _ = TextInRange(0, 0); on line 96, overwriting the previously declared closure’s state. I’m guessing this is not the behaviour you’re expecting.

I think the confusion comes from struct scoped variable in the anonymous struct in ClosureBinder. If you change that to a struct member (this: ClosureScope = undefined, without the var and with a comma) you’ll see that your example can’t compile.

on the subject of closures, i helped implement Objective-C’s “blocks” in https://github.com/mitchellh/zig-objc (see also GitHub - ryleelyman/objz: Zig interface to libobjc )—the source code there might be a good guide for creating closures that can capture “arbitrary” values :slight_smile: