Closure in zig?

var g: u8 = 0;

pub fn main() u8 {
    var i: u8 = 1;
    _ = &i;
    const u: u8 = 3;
    return struct {
        fn f() u8 {
            // const ret = i; // compile fail 
            // const ret = u; // compile ok 
            // const ret = g; // compile ok
            return 0;
        }
    }.f();
}

usually this pattern was taken as ‘closure’ in zig, but the fn f fails to access local var i;

You could try something like this. Possibly someone with more experience might chime in with something better.

const std = @import("std");

fn foo(fun: anytype) void {
    fun.op();
}

pub fn main() void {
    var i: u8 = undefined;
    i = 1;

    const f = struct {
        i: *u8,

        fn op(self: @This()) void {
            self.i.* += 1;
        }
    }{ .i = &i };

    foo(f);

    // prints i = 2
    std.debug.print("i = {}\n", .{i});
}

Zig has no closures. A function can only “close” over comptime values which is why in the example above i causes a compile error.

To get something like this to work, instead of trying to close over runtime values, define a struct that holds all the values you would want to close over, create an instance of it, and pass that around.

If you need to “hide” what you’re closing over (ie you planned to use the function as a form of interface where different instances of it would close over different values and do different things) then you might want to take a look at Zig's @fieldParentPtr for dumbos like me - ryanliptak.com

5 Likes

many functions in our std provide a context parameter, like
std.sort.block(comptime T: type, items:T, context: anytype, comptime lessThanFn: fn(@TypeOf(context), lhs:T, rhs: T) bool ) void
std.sort.heap(comptime T: type, items:T, context: anytype, comptime lessThanFn: fn(@TypeOf(context), lhs:T, rhs: T) bool ) void
as a contrast, the context parameter is unnecessary in many other lang.
and usually there is no need for a context even in zig;

So do we really need the context parameter in these functions?

fn block(comptime T: type, items: []T, less_than_fn: fn (lhs: T, rhs: T) bool) void {
    std.sort.block(T, items, {}, struct {
        fn f (_: void, lhs_: T, rhs_:T ) bool {
            return less_than_fn(lhs_, rhs_);
        }
    }.f);
}

test "block without context"{
    var items = [_]i8 {2, 1, 3, 7};
    block(i8, &items, struct {
        fn f(lhs: i8, rhs: i8) bool {
            return lhs <= rhs;
        }
    }.f);
    try std.testing.expectEqualSlices(i8, &[_]i8{1,2,3,7}, &items);
}

test "block with context" {
    var items = [_]i8 {2, 1, 3, 7};

    const S = struct {
        var ctx: i8 = undefined;
        fn f (lhs: i8, rhs: i8) bool {
            return ctx * lhs <= ctx * rhs;
        }
    };

    S.ctx = -1;    
    block(i8, &items, S.f);
    try std.testing.expectEqualSlices(i8, &[_]i8{7, 3, 2, 1}, &items);
}

Yes, in languages that support closures you can have the context parameter become a hidden value that the laguge passes around without you even knowing (but in one form or another it’s still there though).

The fact is that in a language with manual memory management like Zig, closures are a pattern that can cause you to accidentally close over values that will become invalid by the time the closure is run.

For example, if Zig had support for closures, then returning a closure from a function (so a function that returns a function) would easily cause a segfault if you closed over any local variable (since IIRC in other languages closures by default capture pointers to values).

The best recommendation is to restructure your code in a way that doesn’t make you inject logic from above into deeper layers of computation, if you can avoid it. Using closures (regardless of whether the language does it automatically for you or not) might be the idiomatic choice in other languages but in Zig it rarely it.

8 Likes

:+1:
in language that support closure, there is no need for the context parameter.
in zig, there is no closure, but we can construct a ‘closure’ and setup the context manually as in test ‘block with context’ do, so isn’t there no need for the context parameter too?

‘block with context’ uses a global variable to store the state, so it does not need a context function parameter.

Obviously (hopefully) this is not thread safe.

2 Likes

To add to this, Zig could have an implementation of closures, but it doesn’t. That implementation would have to look like context objects, because Zig isn’t a managed language, so if you want some state to go with a function, you have to add that state in a struct of some sort. So a reified closure type would be syntactic sugar with hidden control flow: something would be creating and freeing a struct to hold the state. Zig doesn’t do things this way.

When you break the problem down into its elements: a function, and a struct which holds the state, it’s straightforward to design what you actually need. There are a few ways to do this, which is another argument against a built-in way to provide closures: the different approaches are actually different, and building it in to the language would mean privileging one of them, or worse, the compiler choosing between implementations for you based on heuristics (and now there’s really hidden control flow).

3 Likes

Just an exercise with closure-like objects.

File 1. closure.zig

const std = @import("std");
const Allocator = std.mem.Allocator;

pub fn Closure(comptime D: type) type {

    const FP = *const fn(data: *D) void;

    return struct {

        const Self = @This();

        data: *D,
        func: FP,

        pub fn create(data: *D, func: FP, a: Allocator) !Self {
            var d = try a.create(D);
            d.* = data.*;
            return .{.data = d, .func = func};
        }

        pub fn invoke(self: *Self) void {
            self.func(self.data);
        }

        pub fn destroy(self: *Self, a: Allocator) void {
            a.destroy(self.data);
        }
    };
}

File 2. main.zig

const std = @import("std");
const log = std.debug.print;
const Allocator = std.mem.Allocator;

const Closure = @import("closure.zig").Closure;
const Counter = Closure(u32);

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer log("leakage?.. {}\n", .{gpa.deinit()});
    const allocator = gpa.allocator();

    var counter: u32 = 10;

    var c1 = try Counter.create(&counter, &countUp, allocator);
    defer c1.destroy(allocator);

    var c2 = try Counter.create(&counter, &countDn, allocator);
    defer c2.destroy(allocator);

    c1.invoke();
    c2.invoke();

    c1.invoke();
    c2.invoke();

    c1.invoke();
    c2.invoke();

}

fn countUp(c: *u32) void {
    c.* += 1;
    log("up - {}\n", .{c.*});
}

fn countDn(c: *u32) void {
    c.* -= 1;
    log("dn - {}\n", .{c.*});
}

Output of the main

$ ./main 
up - 11
dn - 9
up - 12
dn - 8
up - 13
dn - 7
leakage?.. heap.general_purpose_allocator.Check.ok

Everything as expected.
Each instance (c1 & c2) of a “closure” works with it’s own copy of the counter, of course.

2 Likes

and good riddance.

I wonder how these ancient concepts, like

  • coroutines (under various names, “greenlets”, “fibers”, “goroutines”)
    ( Melvin Conway coined the term coroutine in 1958)

  • closures
    (The concept of closures was developed in the 1960s, according to WP)

  • and any other mathematically/computationally related stuff

could have a “new life” in every new prog-lang?

C with it’s concept of “hi-level-asm” beat them all, isn’t?
Zee is the next level of Cee, so just do not pollute Zig with garbage :slight_smile:

Sorry for the emotions :slight_smile:

1 Like

Here is another variant I managed to write after some thinking.

const std = @import("std");
const log = std.debug.print;

const ClosureData = u32;
const ClosureFPtr = *const fn (*Closure, u32) void;

const Closure = struct {
    fptr: ClosureFPtr,
    data: ClosureData,
    lvar: ClosureData,
};

fn makeClosure(arg: u32) Closure {
    const i: u32 = 3;
    return .{
        .fptr = & struct {
            fn func (c: *Closure, d: u32) void {
                log("x + y + z = {}\n", .{d + c.data + c.lvar});
            }
        }.func,
        .data = arg,
        .lvar = i,
    };
}

pub fn main() void {
    var c1 = makeClosure(7);
    c1.fptr(&c1, 5);

    var c2 = makeClosure(1);
    c2.fptr(&c2, 1);
}

Now it looks more similar to, say, JS/Python closures than my previous exercise. Closure function is inside closure “generator” and it captures both argument to this generator and also local variable of it. Looks quite hairy (compared to JS/Python), but I guess this can be helped, Zig is not JS/Python.

Output:

$ ./a 
x + y + z = 15
x + y + z = 5

Forgot to mention - no heap allocations now.

Seems like it’s not really a closure, in the sense that your struct is not closing over the local variables. It is capturing them by value. It’s more like a C++ lambda which doesn’t capture references.

1 Like

TBH, I have near zero experience in languages with intrinsic support of closures, so maybe I have wrong understanding of what “closure” is. If I got you right, a real closure is when all local vars of closure creating function get allocated on the heap and all instances of closures created deal with the same “local” vars of closure generator. Correct?

What you did is a closure, with the exception that it is not a first class function.
Closures captures the free variables --used locally but defined in the enclosing scope-- as values or references.
A closure allows the function to access those captured variables when the function is invoked outside their scope.

Ok, what about this

const std = @import("std");
const log = std.debug.print;

const ClosureData = u32;
const ClosureFPtr = *const fn (*Closure, ClosureData) void;

const Closure = struct {
    var svar: ?ClosureData = null;
    fptr: ClosureFPtr,
    data: ClosureData,
};

fn makeClosure(arg: ClosureData) Closure {
    const i: ClosureData = 3;
    const c: Closure = .{
        .fptr = & struct {
            fn func (c: *Closure, d: ClosureData) void {
                log("x + y + z = {}\n", .{d + c.data + Closure.svar.?});
                Closure.svar.? += 1;
            }
        }.func,
        .data = arg,
    };
    if (null == Closure.svar) Closure.svar = i;
    return c;
}

pub fn main() void {
    var c1 = makeClosure(7);
    c1.fptr(&c1, 5);

    var c2 = makeClosure(1);
    c2.fptr(&c2, 1);

    c1.fptr(&c1, 5);
}

Now all instances have some shared data (via var inside Closure) which are initialized once from local var of the makeClosure().

1 Like