Zig NEWS

Andrew Houghton
Andrew Houghton

Posted on • Updated on

Closure Pattern in Zig

Caution: The presented implementation in this article has limitations.


From the Wikipedia Closure (computer programming) article.

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

Closure Pattern Abstract

This article discusses an implementation of the closure pattern in the Zig language and was inspired after reading Andrew Gossage's "Implementing Closures and Monads in Zig" article. Gossage's article discusses the challenges of implementing closures in the Zig language and presents an example of a Zig language implementation of the closure pattern. The presented Zig language code however, is not an implementation of the closure pattern. Gossage's article does point out that the example returns a structure instead of a function. The challenges implementing closures in the Zig language however, are more about understanding closure concepts, Zig language concepts and some Zig Zen.

JavaScript Closure Pattern

The general closure pattern uses a binding scope to maintain state for its implementation and that binding scope needs to be appropriately mapped onto the programming language concepts of the implementation. Consider the following JavaScript language implementation of the closure pattern.

function TextInRange(begin, end) {
    return function(text) {
        return text.substring(begin, end + 1);
    };
}
var closure = TextInRange(2, 5);
console.log(closure("0123456789")); // logs "2345"
console.log(closure("abcdefghij")); // logs "cdef"
console.log(TextInRange(2, 8)("0123456789")); //logs "2345678"
Enter fullscreen mode Exit fullscreen mode

This implementation of the JavaScript language closure pattern has state that is used in the anonymous closure function. The arguments to the TextInRange function constitutes the state used by the anonymous closure function. The TextInRange function also provides the lexical binding scope for the anonymous closure function. The anonymous closure function takes a string argument and returns the characters found in the range specified by the invocation arguments of the TextInRange function. The anonymous closure function can be invoked with different string arguments and it will always evaluate to the same range of characters in the string.

The JavaScript language allows functions to be declared inside of another function and those sub-functions have access to the lexical binding scope of the parent function. This simplifies the implementation of the JavaScript language closure pattern which allows it to be implemented in a few lines of code.

Restricting the lexical binding scope

What if the programming language that is being used to implement the closure pattern did not allow a sub-function to access the lexical binding scope of the parent function? How would we implement the closure pattern in the JavaScript language with this restriction? Let's reimplement the JavaScript language closure pattern with this restriction so the closure function does not directly use the lexical binding scope of the parent function.

function TextInRange(begin, end) {
    const scope = { begin: begin, end: end };
    function closure(text) {
        return text.substring(this.begin, this.end);
    };
    return closure.bind(scope);
}
Enter fullscreen mode Exit fullscreen mode

The closure binding scope is maintained by the code const scope = { begin: begin, end: end } in the parent function and the closure function uses the JavaScript language this keyword to retrieve the scope that is bound to the closure function. Initially the scope for the closure function is undefined until we bind it to the function and that is what the code closure.bind(scope) does.

This implementation of the JavaScript language closure pattern produces the same results as the prior implementation of the JavaScript language closure pattern.

Restricting functions within functions

What if the programming language that is being used to implement the closure pattern did not allow sub-functions to be declared within another function? How would we implement the closure pattern in the JavaScript language with this restriction? Let's reimplement the JavaScript language closure pattern with this restriction so that it does not declare the closure function within the parent function.

function closure(text) {
    return text.substring(this.begin, this.end);
}
function TextInRange(begin, end) {
    const scope = { begin: begin, end: end };
    return closure.bind(scope);
}
Enter fullscreen mode Exit fullscreen mode

This implementation of the JavaScript language closure pattern produces the same results as the other implementations of the JavaScript language closure pattern.

Zig Closure Pattern

What do these implementations of the JavaScript language closure pattern have to do with a Zig language implementation? The last implementation of the JavaScript language closure pattern is conceptually compatible with the Zig language by not declaring functions within a function and providing a binding scope mechanism for use by the closure implementation function.

A closure is comprised of an outer function called the closure's generator which is described by the ClosureGenerator function and an inner function called the closure's implementation which is described by the ClosureImplementation generic type.

To implement the closure pattern in the Zig language:

Describe the closure's binding scope type

/// Type used to describe the closure's binding scope.
const ClosureBindingScope = struct { begin: usize, end: usize };
Enter fullscreen mode Exit fullscreen mode

This declaration is lexically scoped as private in the Zig language source file. The binding scope environment contains the values in the closure's generator that are used by the closure's implementation. In the presented example code the closure's ClosureImplementation generic type will use the beginning and ending positions in the string to evaluate the range of characters that will be returned by the closure.

Describe the closure's signature type

/// Type used to describe the closure's signature.
const ClosureSignature = *const fn (text: []const u8) []const u8;
Enter fullscreen mode Exit fullscreen mode

This declaration is lexically scoped as private in the Zig language source file and describes the closure's signature type for the closure's inner function. The ClosureImplementation generic type's bind and invoke functions implement the closure's signature interface for different use cases. The ClosureSignature type in the presented example code describes an interface that is a pointer to a function which takes a text string and returns a text string.

Describe the closure's implementation

/// Type used for implementation and generation of the closure.
fn ClosureImplementation(comptime Tscope: type) type {
    return struct {
        scope: Tscope,
        const Self = @This();
        fn new(scope: Tscope) ClosureImplementation(Tscope) {
            return ClosureImplementation(Tscope){ .scope = scope };
        }
        fn bind(scope: Tscope) ClosureSignature {
            // This function has usage limitations.
            const Bind = struct {
                var this: ClosureImplementation(Tscope) = undefined;
                fn closure(text: []const u8) []const u8 {
                    return this.invoke(text);
                }
            };
            Bind.this = new(scope);
            return &Bind.closure;
        }
        fn invoke(self: Self, text: []const u8) []const u8 {
            const this = self.scope;
            const first = @min(this.begin, text.len);
            const last = @min(this.end + 1, text.len);
            return text[first..last];
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

This declaration is lexically scoped as private in the Zig language source file and describes the Zig language generic type that binds the the closure's binding scope environment and its implementation. This is equivalent to the declaration of the closure function in the last implementation of the JavaScript language closure pattern.

The JavaScript language hides the implementation details of how the this keyword works, but it is essentially doing something similar to allow any binding scope to be bound to a function by using the bind function on its function object.

The ClosureImplementation generic type in the presented example code implements the following:

  • The binding scope environment is saved in the scope structure field.
  • The new function creates a new instance of the ClosureImplementation generic type that can be used to invoke the closure function or return the closure function.
  • The bind function returns the closure function to be called. This function has limitations on its use.
  • The invoke function invokes the closure function.

Describe the closure's generator

/// Create the closure's binding scope and generate the closure.
fn ClosureGenerator(begin: usize, end: usize) ClosureSignature {
    // Implement any necessary functionality for division of labor scenarios.
    // Implement any necessary transformations for generating the closure's binding scope.
    const scope = ClosureBindingScope{ .begin = begin, .end = end };
    return ClosureImplementation(@TypeOf(scope)).bind(scope);
}
Enter fullscreen mode Exit fullscreen mode

This declaration is lexically scoped as private in the Zig language source file and describes the closure's generator. The ClosureGenerator function saves the values that will be used by the closure's ClosureImplementation generic type. A ClosureBindingScope type instance saves the beginning and ending position arguments in the scope constant. This Zig language code is equivalent to the JavaScript language code const scope = { begin: begin, end: end } in the last implementation of the JavaScript language closure pattern.

The ClosureGenerator function then uses the ClosureImplementation generic type to save the binding scope environment and return the closure's inner function which the caller will use to invoke it with different arguments. The Zig language code is equivalent to the JavaScript language code closure.bind(scope) in the last implementation of the JavaScript language closure pattern.

Export the closure's generator name

/// Export the closure's generator using an appropriate name.
pub const TextInRange = ClosureGenerator;
Enter fullscreen mode Exit fullscreen mode

This declaration is lexically scoped as public in the Zig language source file and exports the TextInRange name using the ClosureGenerator implementation.

Practical Closure Example

Now that the presented example code has implemented the TextInRange closure in the Zig language, let's demonstrate a practical example of its use in action. This practical example will take an input stream of lines that contain 8 positional fields and transform those positional fields into an output stream of comma delimited lines. The practical example code implementation is as follows.

  • Mock the input stream as an array of the text lines.
  • Create a closure for each positional field.
  • Invoke each field's closure to retrieve the field contents for the text line.
  • Mock the output steam by printing the CSV line to the console.
// Test practical closure example.
test {
    print("\n{s}\n", .{"[Practical closure example]"});

    // Mock positional input lines.
    const lines = [_][]const u8{
        //         1         2         3         4         5         6     tens position
        //123456789012345678901234567890123456789012345678901234567890123  ones position
        //122222222222333333333334444444444566666666666777777777778888888  field position
        "3900003233328000000750620000000000C0000004680000000001170  01000\n",
        "3900003233328000000750620000000000C0000001530000000000383  16000\n",
        "3900003233328000000750620000000000C0000315862800000063172  49056\n",
        "3900003233328000000750620000000000C0000001260000000000252  56448\n",
    };

    // Generate closures for each field's position in the line.
    const lineField1 = TextInRange(0, 1);
    const lineField2 = TextInRange(2, 12);
    const lineField3 = TextInRange(13, 23);
    const lineField4 = TextInRange(24, 33);
    const lineField5 = TextInRange(34, 34);
    const lineField6 = TextInRange(35, 45);
    const lineField7 = TextInRange(46, 56);
    const lineField8 = TextInRange(57, 63);

    // Process each positional input line.
    for (lines) |line| {

        // Retrieve each field in the positional line.
        const field1 = lineField1(line);
        const field2 = lineField2(line);
        const field3 = lineField3(line);
        const field4 = lineField4(line);
        const field5 = lineField5(line);
        const field6 = lineField6(line);
        const field7 = lineField7(line);
        const field8 = lineField8(line);

        // Generate argument list.
        const fields = .{ field1, field2, field3, field4, field5, field6, field7, field8 };

        // Convert positional line input to CSV.
        print("{s},{s},{s},{s},{s},{s},{s},{s}\n", fields);
    }

    return;
}
Enter fullscreen mode Exit fullscreen mode

The practical example code closure creates a division of labor which happens both outside and inside the for loop. While the practical example code presented is simplistic, the algorithm implemented in the closure generator could be time intensive while the invocation of the returned closure could quickly execute since it only has do the work necessary to transform its arguments. Contrast that with a generalized function that does the work of both and is invoked from inside the for loop for each text line.

Let's look at another similar use of a closure and this division of labor concept. Consider regular expressions. Regular expressions are usually transformed into an internal representation and that representation is executed against the target string. The compilation to the internal representation could be time intensive compared to the execution against the target string. A closure could be used to provide a division of labor where the closure generator compiles the regular expression and provides the internal representation as binding scope to closure to complete its task against the target string.

Hopefully, these examples provide some insight into the usefulness of closures and where you might use them in your projects. A template is provided with this article for the implementation of the Zig language closure pattern. You can use the template as a starting point for your own projects.

Closure Pattern Summary

The Zig language does not allow functions to be declared within a function nor does it allow anonymous functions to be directly created in version 0.12.0. Those two feature constitute a common pattern for implementing the closure pattern in other programming languages such as the JavaScript, PowerQuery-M, etc.

The Zig language implementation of the closure pattern is analogous to the last implementation of the JavaScript language closure pattern. The differences between these two implementations are the Zig language type wrangling and the binding scope mechanism.

The presented Zig language closure pattern lexically scopes the implementation as private details to the Zig language source file and only exports the name that will be used for calling the closure generator. The Zig language closure pattern makes use of simple Zig language coding concepts.

Two of the 5 implementation steps deal with Zig language type wrangling and 1 step deals with the closure generator name export. The entire Zig language closure pattern amounts to a handful lines of code, excluding the implementation details of the closure algorithm and any code that might be need to transform the arguments of the ClosureGenerator function into the binding scope needed by the closure's ClosureImplementation generic type.


Closure Pattern Limitations

The ClosureImplementation.bind function uses a static local variable to store the binding scope environment which causes limitations on using the bind function. After invoking the ClosureImplementation.bind function you are required to finish using the closure generated by the bind function before invoking the bind function again. Otherwise the static local variable will be overwritten with the new binding scope environment and the existing closure functions will start using the new binding scope environment. As the practical example code demonstrates, this limitation may or may not be an issue for the implementer, but it is a limitation of the presented pattern. The author is investigating alternate implementations to remove this restriction.

Should the implementer need to create multiple closures and invoke them at a later time, then they should implement the following coding strategy, since it is the best that can be done within the Zig language's current limitations:

// Create closures up front.
const scope1 = ClosureBindingScope{ .begin = 2, .end = 5 };
const scope2 = ClosureBindingScope{ .begin = 2, .end = 8 };

const closure1 = ClosureImplementation(@TypeOf(scope1)).new(scope1);
const closure2 = ClosureImplementation(@TypeOf(scope2)).new(scope2);

// Invoke at some later point in time.
const value1 = closure1.invoke("0123456789"); // value1 = "2345";
const value2 = closure2.invoke("0123456789"); // value2 = "2345678";
Enter fullscreen mode Exit fullscreen mode

The ClosureImplementation.bind function should be able to be implemented in the Zig language, simply as:

fn ClosureImplementation(comptime Tscope: type) type {
    return struct {
        scope: Tscope,
        const Self = @This();
        fn new(scope: Tscope) ClosureImplementation(Tscope) {
            return ClosureImplementation(Tscope){ .scope = scope };
        }
        fn bind(scope: Tscope) ClosureSignature {
            return &new(scope).invoke;
        }
        fn invoke(self: Self, text: []const u8) []const u8 {
            const this = self.scope;
            const first = @min(this.begin, text.len);
            const last = @min(this.end + 1, text.len);
            return text[first..last];
        }
    };
}

test {
    const scope = ClosureBindingScope{ .begin = 2, .end = 5 };
    const closure = ClosureImplementation(@TypeOf(scope)).bind(scope);
    const value = closure("0123456789");

    print("\nvalue = {s}\n", .{ value });
}
Enter fullscreen mode Exit fullscreen mode

When the closure is invoked as closure("0123456789") the Zig language compiler should have generated a call to the invoke function, automatically passed the self: Self argument, as the first argument, and should have passed the string "0123456789" as the second argument. The Zig language compiler already has the type information for the invoke function and knows that the invoke function should be be called with self: Self. Also, the Zig language compiler already does this code generation when explicitly calling the invoke function where it automatically passes the self: Self argument which is not specified by the caller, for example:

const value = ClosureImplementation(@TypeOf(scope)).new(scope).invoke("0123456789");
Enter fullscreen mode Exit fullscreen mode

However, the Zig language v0.12.0 compiler generates a compile-time error given the above implementation of the bind function:

error: no field named 'invoke' in struct 'closure-example.ClosureImplementation(closure-example.ClosureBindingScope)'
Enter fullscreen mode Exit fullscreen mode

Consider the following two analogous Zig language code examples which retrieve a function or a reference to a function and are syntactically consistent and work without issue:

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

fn foo(x: usize) usize { return x; }

test {
    const foo1 = foo;
    const foo2 = &foo;
    print("foo1 = {any}\t foo2 = {any}\n", .{ foo1(2), foo2(3) });
}
Enter fullscreen mode Exit fullscreen mode
const print = @import("std").debug.print;

fn bar(T: type) type {
    return struct {
        fn foo(n: T) T {
            return n;
        }
    };
}

test {
    const foo1 = bar(usize).foo;
    const foo2 = &bar(usize).foo;
    print("foo1 = {any}\t foo2 = {any}\n", .{ foo1(2), foo2(3) });
}
Enter fullscreen mode Exit fullscreen mode

Now consider the following Zig language code example which produces the compiler error:

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

fn baz(T: type) type {
    return struct {
        fn new() @This() {
            return @This(){};
        }
        fn foo(self: @This(), n: T) T {
            _ = self;
            return n;
        }
    };
}

test {
    const foo1 = baz(usize).new().foo;
    const foo2 = &baz(usize).new().foo;
    print("foo1 = {any}\t foo2 = {any}\n", .{ foo1(2), foo2(3) });
}
Enter fullscreen mode Exit fullscreen mode

The Zig language complier treats this case differently than the prior two code examples when using a reference to a function and it should not. Because of this difference, i.e., not being able to retrieve the reference to the foo function, i.e., &baz(usize).new().foo, is why the bind function was implemented with limitations. This last code example should be no different than the prior two code examples. The Zig language complier has all the type information that it needs to find the foo function in the AST and generate the correct code.

This difference in how the Zig language compiler treats these code examples is obvious when using the official Zig language VS Code extension. The official Zig language VS Code extension generates the following for the constants foo1 and foo2. Notice the type information, in gray, circled in red, which was generated from the Zig language compiler AST:

Zig language VS Code extension generated type information.

This discrepancy shows that the type information is being generated in the AST by the Zig language compiler, but yet the Zig language v0.12.0 compiler cannot find the invoke function? Given the type information is in the AST, then the Zig language compiler should be able to generate a function reference from &baz(usize).new().foo and automatically add self: Self to the invoke function's argument list just as it does when explicitly calling the invoke function. If the Zig language compiler treated the latter code example as the prior two examples, then true closures could be implemented without limitations.


Closure Pattern FAQ

  1. Why is the exported name TextInRange used in the presented example instead of something more appropriate?

The inspiration for TextInRange example comes from the Splitter namespace in the PowerQuery-M language. The SplitTextByRanges closure in PowerQuery-M takes a list of offsets and lengths, returns a closure function that takes a string argument and returns a list of strings at those offsets/lengths pairs.

To simplify the discussion in this article and to not bog the reader down with algorithmic implementation details, the presented example uses only a beginning and ending position. That decision also made the discussion about the similarities between the implementations of the JavaScript language closure pattern and the Zig language closure pattern, simpler as well.


Closure Pattern Example

closure-example.zig

zig test closure-example.zig

const std = @import("std");
const builtin = std.builtin;
const debug = std.debug;
const mem = std.mem;
const testing = std.testing;

const assert = debug.assert;
const eql = mem.eql;
const expect = testing.expect;
const print = debug.print;

/// Type used to describe the closure's binding scope.
const ClosureBindingScope = struct { begin: usize, end: usize };

/// Type used to describe the closure's signature.
const ClosureSignature = *const fn (text: []const u8) []const u8;

/// Type used for implementation and generation of the closure.
fn ClosureImplementation(comptime Tscope: type) type {
    return struct {
        scope: Tscope,
        const Self = @This();
        fn new(scope: Tscope) ClosureImplementation(Tscope) {
            return ClosureImplementation(Tscope){ .scope = scope };
        }
        fn bind(scope: Tscope) ClosureSignature {
            const Bind = struct {
                var this: ClosureImplementation(Tscope) = undefined;
                fn closure(text: []const u8) []const u8 {
                    return this.invoke(text);
                }
            };
            Bind.this = new(scope);
            return &Bind.closure;
        }
        fn invoke(self: Self, text: []const u8) []const u8 {
            const this = self.scope;
            const first = @min(this.begin, text.len);
            const last = @min(this.end + 1, text.len);
            return text[first..last];
        }
    };
}

/// Create the closure's binding scope and generate the closure.
fn ClosureGenerator(begin: usize, end: usize) ClosureSignature {
    // Implement any necessary functionality for division of labor scenarios.
    // Implement any necessary transformations for generating the closure's binding scope.
    const scope = ClosureBindingScope{ .begin = begin, .end = end };
    return ClosureImplementation(@TypeOf(scope)).bind(scope);
}

/// Export the closure's generator using an appropriate name.
pub const TextInRange = ClosureGenerator;

// Test practical closure example.
test {
    print("\n{s}\n", .{"[Practical closure example]"});

    // Mock positional input lines.
    const lines = [_][]const u8{
        //         1         2         3         4         5         6     tens position
        //123456789012345678901234567890123456789012345678901234567890123  ones position
        //122222222222333333333334444444444566666666666777777777778888888  field position
        "3900003233328000000750620000000000C0000004680000000001170  01000\n",
        "3900003233328000000750620000000000C0000001530000000000383  16000\n",
        "3900003233328000000750620000000000C0000315862800000063172  49056\n",
        "3900003233328000000750620000000000C0000001260000000000252  56448\n",
    };

    // Generate closures for each field's position in the line.
    const lineField1 = TextInRange(0, 1);
    const lineField2 = TextInRange(2, 12);
    const lineField3 = TextInRange(13, 23);
    const lineField4 = TextInRange(24, 33);
    const lineField5 = TextInRange(34, 34);
    const lineField6 = TextInRange(35, 45);
    const lineField7 = TextInRange(46, 56);
    const lineField8 = TextInRange(57, 63);

    // Process each positional input line.
    for (lines) |line| {

        // Retrieve each field in the positional line.
        const field1 = lineField1(line);
        const field2 = lineField2(line);
        const field3 = lineField3(line);
        const field4 = lineField4(line);
        const field5 = lineField5(line);
        const field6 = lineField6(line);
        const field7 = lineField7(line);
        const field8 = lineField8(line);

        // Generate argument list.
        const fields = .{ field1, field2, field3, field4, field5, field6, field7, field8 };

        // Convert positional line input to CSV.
        print("{s},{s},{s},{s},{s},{s},{s},{s}\n", fields);
    }

    return;
}

// Test single closure generation using constants followed by invocation.
test {
    print("\n{s}\n", .{"[Single closure generation using constants followed by invocation]"});

    // Define constants used for testing.

    const text = "0123456789";

    const begin1: usize = 2;
    const end1: usize = 5;

    const begin2: usize = 2;
    const end2: usize = 8;

    // Generate column header.

    const head = .{ "text", "begin", "end", "actual", "expect" };
    print("{s}\t\t{s}\t{s}\t{s}\t\t{s}\n", head);

    // Test closure generation immediately followed by invocation.

    const TextInRange1 = ClosureGenerator(begin1, end1);

    const actual1 = TextInRange1(text);
    const expect1 = "2345";

    const args1 = .{ text, begin1, end1, actual1, expect1 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args1);

    try expect(eql(u8, actual1, expect1));

    // Test closure generation immediately followed by invocation.

    const TextInRange2 = ClosureGenerator(begin2, end2);

    const acutal2 = TextInRange2(text);
    const expect2 = "2345678";

    const args2 = .{ text, begin2, end2, acutal2, expect2 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args2);

    try expect(eql(u8, acutal2, expect2));

    return;
}

// Test single closure generation using variables followed by invocation.
test {
    print("\n{s}\n", .{"[Single closure generation using variables followed by invocation]"});

    // Define constants used for testing.

    const text = "0123456789";

    var begin1: usize = undefined;
    begin1 = 2;

    var end1: usize = undefined;
    end1 = 5;

    var begin2: usize = undefined;
    begin2 = 2;

    var end2: usize = undefined;
    end2 = 8;

    // Generate column header.

    const head = .{ "text", "begin", "end", "actual", "expect" };
    print("{s}\t\t{s}\t{s}\t{s}\t\t{s}\n", head);

    // Test closure generation immediately followed by invocation.

    const TextInRange1 = ClosureGenerator(begin1, end1);

    const actual1 = TextInRange1(text);
    const expect1 = "2345";

    const args1 = .{ text, begin1, end1, actual1, expect1 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args1);

    try expect(eql(u8, actual1, expect1));

    // Test closure generation immediately followed by invocation.

    const TextInRange2 = ClosureGenerator(begin2, end2);

    const acutal2 = TextInRange2(text);
    const expect2 = "2345678";

    const args2 = .{ text, begin2, end2, acutal2, expect2 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args2);

    try expect(eql(u8, acutal2, expect2));

    return;
}

// Test multiple closure generation using constants followed by explicit invocation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using constants followed by explicit invocation]"});

    // Define constants used for testing.

    const text = "0123456789";

    const begin1: usize = 2;
    const end1: usize = 5;
    const scope1 = ClosureBindingScope{ .begin = begin1, .end = end1 };

    const begin2: usize = 2;
    const end2: usize = 8;
    const scope2 = ClosureBindingScope{ .begin = begin2, .end = end2 };

    // Generate column header.

    const head = .{ "text", "begin", "end", "actual", "expect" };
    print("{s}\t\t{s}\t{s}\t{s}\t\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureImplementation(@TypeOf(scope1)).new(scope1);
    const TextInRange2 = ClosureImplementation(@TypeOf(scope2)).new(scope2);

    // Test first closure invocation.

    const actual1 = TextInRange1.invoke(text);
    const expect1 = "2345";

    const args1 = .{ text, begin1, end1, actual1, expect1 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args1);

    try expect(eql(u8, actual1, expect1));

    // Test second closure invocation.

    const actual2 = TextInRange2.invoke(text);
    const expect2 = "2345678";

    const args2 = .{ text, begin2, end2, actual2, expect2 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args2);

    try expect(eql(u8, actual2, expect2));

    return;
}

// Test multiple closure generation using variables followed by explicit invocation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using variables followed by explicit invocation]"});

    // Define constants used for testing.

    const text = "0123456789";

    var begin1: usize = undefined;
    begin1 = 2;

    var end1: usize = undefined;
    end1 = 5;

    var scope1: ClosureBindingScope = undefined;
    scope1 = ClosureBindingScope{ .begin = begin1, .end = end1 };

    var begin2: usize = undefined;
    begin2 = 2;

    var end2: usize = undefined;
    end2 = 8;

    var scope2: ClosureBindingScope = undefined;
    scope2 = ClosureBindingScope{ .begin = begin2, .end = end2 };

    // Generate column header.

    const head = .{ "text", "begin", "end", "actual", "expect" };
    print("{s}\t\t{s}\t{s}\t{s}\t\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureImplementation(@TypeOf(scope1)).new(scope1);
    const TextInRange2 = ClosureImplementation(@TypeOf(scope2)).new(scope2);

    // Test first closure invocation.

    const actual1 = TextInRange1.invoke(text);
    const expect1 = "2345";

    const args1 = .{ text, begin1, end1, actual1, expect1 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args1);

    try expect(eql(u8, actual1, expect1));

    // Test second closure invocation.

    const actual2 = TextInRange2.invoke(text);
    const expect2 = "2345678";

    const args2 = .{ text, begin2, end2, actual2, expect2 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args2);

    try expect(eql(u8, actual2, expect2));

    return;
}

// Test multiple closure generation using constants followed by implicit invocation.
// Test is expected to FAIL due to using local static variable to get around Zig compiler limitation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using constants followed by implicit invocation]"});

    // Define constants used for testing.

    const text = "0123456789";

    const begin1: usize = 2;
    const end1: usize = 5;

    const begin2: usize = 2;
    const end2: usize = 8;

    // Generate column header.

    const head = .{ "text", "begin", "end", "actual", "expect" };
    print("{s}\t\t{s}\t{s}\t{s}\t\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureGenerator(begin1, end1);
    const TextInRange2 = ClosureGenerator(begin2, end2);

    // Test first closure invocation.

    const actual1 = TextInRange1(text);
    const expect1 = "2345";

    const args1 = .{ text, begin1, end1, actual1, expect1 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args1);

    try expect(eql(u8, actual1, expect1));

    // Test second closure invocation.

    const actual2 = TextInRange2(text);
    const expect2 = "2345678";

    const args2 = .{ text, begin2, end2, actual2, expect2 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args2);

    try expect(eql(u8, actual2, expect2));

    return;
}

// Test multiple closure generation using variables followed by implicit invocation.
// Test is expected to FAIL due to using local static variable to get around Zig compiler limitation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using variables followed by implicit invocation]"});

    // Define constants used for testing.

    const text = "0123456789";

    var begin1: usize = undefined;
    begin1 = 2;

    var end1: usize = undefined;
    end1 = 5;

    var begin2: usize = undefined;
    begin2 = 2;

    var end2: usize = undefined;
    end2 = 8;

    // Generate column header.

    const head = .{ "text", "begin", "end", "actual", "expect" };
    print("{s}\t\t{s}\t{s}\t{s}\t\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureGenerator(begin1, end1);
    const TextInRange2 = ClosureGenerator(begin2, end2);

    // Test first closure invocation.

    const actual1 = TextInRange1(text);
    const expect1 = "2345";

    const args1 = .{ text, begin1, end1, actual1, expect1 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args1);

    try expect(eql(u8, actual1, expect1));

    // Test second closure invocation.

    const actual2 = TextInRange2(text);
    const expect2 = "2345678";

    const args2 = .{ text, begin2, end2, actual2, expect2 };
    print("{s}\t{d}\t{d}\t{s}\t\t{s}\n", args2);

    try expect(eql(u8, actual2, expect2));

    return;
}
Enter fullscreen mode Exit fullscreen mode

Closure Pattern Template

closure-template.zig

zig test closure-template.zig

const std = @import("std");
const builtin = std.builtin;
const debug = std.debug;
const mem = std.mem;
const testing = std.testing;

const assert = debug.assert;
const eql = mem.eql;
const expect = testing.expect;
const print = debug.print;

/// Type used to describe the closure's binding scope.
const ClosureBindingScope = struct {
    // TODO add structure fields and their types to maintain the closure's binding scope.
    value: bool,
};

/// Type used to describe the closure's signature.
const ClosureSignature = *const fn () bool; // TODO change to describe the closure's signature.

/// Type used for implementation and generation of the closure.
fn ClosureImplementation(comptime Tscope: type) type {
    return struct {
        scope: Tscope,
        const Self = @This();
        fn new(scope: Tscope) ClosureImplementation(Tscope) {
            return ClosureImplementation(Tscope){ .scope = scope };
        }
        // The ClosureImplementation.bind function uses a static local variable to store the binding
        // scope environment which causes limitations on using the `bind` function. After invoking the
        // ClosureImplementation.bind function you are **required** to finish using the closure
        // generated by the `bind` function before invoking the `bind` function again. Otherwise the
        // static local variable will be overwritten with the new binding scope environment and the
        // existing closure functions will start using the new binding scope environment. As the
        // practical example code demonstrates, this limitation may or may not be an issue for the
        // implementer, but it is a limitation of the presented pattern. The author is investigating
        // alternate implementations to remove this restriction.
        //
        // Should the implementer need to create multiple closures and invoke them at a later time,
        // then they should implement the following coding strategy, since it is the best that can be done
        // within the Zig language's current limitations:
        //
        //     // Create closures up front.
        //     const scope1 = ClosureBindingScope{ .begin = 2, .end = 5 };
        //     const scope2 = ClosureBindingScope{ .begin = 2, .end = 8 };
        //
        //     const closure1 = ClosureImplementation(@TypeOf(scope1)).new(scope1);
        //     const closure2 = ClosureImplementation(@TypeOf(scope2)).new(scope2);
        //
        //     // Invoke at some later point in time.
        //     const value1 = closure1.invoke("0123456789"); // value1 = "2345";
        //     const value2 = closure2.invoke("0123456789"); // value2 = "2345678";
        //
        fn bind(scope: Tscope) ClosureSignature {
            const Bind = struct {
                var this: ClosureImplementation(Tscope) = undefined;
                fn closure() bool { // TODO change signature used by closure.
                    return this.invoke(); // TODO change to use arguments for closure.
                }
            };
            Bind.this = new(scope);
            return &Bind.closure;
        }
        fn invoke(self: Self) bool { // TODO change signature used by closure.
            return self.scope.value; // TODO change to return an appropriate value.
        }
    };
}

/// Create the closure's binding scope and generate the closure.
fn ClosureGenerator(value: bool) ClosureSignature { // TODO change to accept any necessary arguments.
    // TODO implement any necessary functionality for division of labor scenarios.
    // TODO implement any necessary transformations for generating the closure's binding scope.
    const scope = ClosureBindingScope{
        // TODO initialize fields to match ClosureBindingScope.
        .value = value,
    };
    return ClosureImplementation(@TypeOf(scope)).bind(scope);
}

/// Export the closure's generator using an appropriate name.
pub const ChangeThisName = ClosureGenerator; // TODO change the ChangeThisName as appropriate.

// Test single closure generation using constants followed by invocation.
test {
    print("\n{s}\n", .{"[Single closure generation using constants followed by invocation]"});

    // Define constants used for testing.

    const value1 = false;
    const value2 = true;

    // Generate column header.

    const head = .{ "value", "actual", "expect" };
    print("{s}\t{s}\t{s}\n", head);

    // Test closure generation immediately followed by invocation.

    const TextInRange1 = ClosureGenerator(value1);

    const actual1 = TextInRange1();
    const expect1 = false;

    const args1 = .{ value1, actual1, expect1 };
    print("{any}\t{any}\t{any}\n", args1);

    try expect(actual1 == expect1);

    // Test closure generation immediately followed by invocation.

    const TextInRange2 = ClosureGenerator(value2);

    const actual2 = TextInRange2();
    const expect2 = true;

    const args2 = .{ value2, actual2, expect2 };
    print("{any}\t{any}\t{any}\n", args2);

    try expect(actual2 == expect2);

    return;
}

// Test single closure generation using variables followed by invocation.
test {
    print("\n{s}\n", .{"[Single closure generation using variables followed by invocation]"});

    // Define constants used for testing.

    var value1: bool = undefined;
    value1 = false;

    var value2: bool = undefined;
    value2 = true;

    // Generate column header.

    const head = .{ "value", "actual", "expect" };
    print("{s}\t{s}\t{s}\n", head);

    // Test closure generation immediately followed by invocation.

    const TextInRange1 = ClosureGenerator(value1);

    const actual1 = TextInRange1();
    const expect1 = false;

    const args1 = .{ value1, actual1, expect1 };
    print("{any}\t{any}\t{any}\n", args1);

    try expect(actual1 == expect1);

    // Test closure generation immediately followed by invocation.

    const TextInRange2 = ClosureGenerator(value2);

    const actual2 = TextInRange2();
    const expect2 = true;

    const args2 = .{ value2, actual2, expect2 };
    print("{any}\t{any}\t{any}\n", args2);

    try expect(actual2 == expect2);

    return;
}

// Test multiple closure generation using constants followed by explicit invocation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using constants followed by explicit invocation]"});

    // Define constants used for testing.

    const value1 = false;
    const scope1 = ClosureBindingScope{ .value = value1 };

    const value2 = true;
    const scope2 = ClosureBindingScope{ .value = value2 };

    // Generate column header.

    const head = .{ "value", "actual", "expect" };
    print("{s}\t{s}\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureImplementation(@TypeOf(scope1)).new(scope1);
    const TextInRange2 = ClosureImplementation(@TypeOf(scope2)).new(scope2);

    // Test first closure invocation.

    const actual1 = TextInRange1.invoke();
    const expect1 = false;

    const args1 = .{ value1, actual1, expect1 };
    print("{any}\t{any}\t{any}\n", args1);

    try expect(actual1 == expect1);

    // Test second closure invocation.

    const actual2 = TextInRange2.invoke();
    const expect2 = true;

    const args2 = .{ value2, actual2, expect2 };
    print("{any}\t{any}\t{any}\n", args2);

    try expect(actual2 == expect2);

    return;
}

// Test multiple closure generation using variables followed by explicit invocation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using variables followed by explicit invocation]"});

    // Define constants used for testing.
    var value1: bool = undefined;
    value1 = false;

    var scope1: ClosureBindingScope = undefined;
    scope1 = ClosureBindingScope{ .value = value1 };

    var value2: bool = undefined;
    value2 = true;

    var scope2: ClosureBindingScope = undefined;
    scope2 = ClosureBindingScope{ .value = value2 };

    // Generate column header.

    const head = .{ "value", "actual", "expect" };
    print("{s}\t{s}\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureImplementation(@TypeOf(scope1)).new(scope1);
    const TextInRange2 = ClosureImplementation(@TypeOf(scope2)).new(scope2);

    // Test first closure invocation.

    const actual1 = TextInRange1.invoke();
    const expect1 = false;

    const args1 = .{ value1, actual1, expect1 };
    print("{any}\t{any}\t{any}\n", args1);

    try expect(actual1 == expect1);

    // Test second closure invocation.

    const actual2 = TextInRange2.invoke();
    const expect2 = true;

    const args2 = .{ value2, actual2, expect2 };
    print("{any}\t{any}\t{any}\n", args2);

    try expect(actual2 == expect2);

    return;
}

// Test multiple closure generation using constants followed by implicit invocation.
// Test is expected to FAIL due to using local static variable to get around Zig compiler limitation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using constants followed by implicit invocation]"});

    // Define constants used for testing.

    const value1 = false;
    const value2 = true;

    // Generate column header.

    const head = .{ "value", "actual", "expect" };
    print("{s}\t{s}\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureGenerator(value1);
    const TextInRange2 = ClosureGenerator(value2);

    // Test first closure invocation.

    const actual1 = TextInRange1();
    const expect1 = false;

    const args1 = .{ value1, actual1, expect1 };
    print("{any}\t{any}\t{any}\n", args1);

    try expect(actual1 == expect1);

    // Test second closure invocation.

    const actual2 = TextInRange2();
    const expect2 = true;

    const args2 = .{ value2, actual2, expect2 };
    print("{any}\t{any}\t{any}\n", args2);

    try expect(actual2 == expect2);

    return;
}

// Test multiple closure generation using variables followed by implicit invocation.
// Test is expected to FAIL due to using local static variable to get around Zig compiler limitation.
test {
    print("\n{s}\n", .{"[Multiple closure generation using variables followed by implicit invocation]"});

    // Define constants used for testing.

    var value1: bool = undefined;
    value1 = false;

    var value2: bool = undefined;
    value2 = true;

    // Generate column header.

    const head = .{ "value", "actual", "expect" };
    print("{s}\t{s}\t{s}\n", head);

    // Test multiple closure generations.

    const TextInRange1 = ClosureGenerator(value1);
    const TextInRange2 = ClosureGenerator(value2);

    // Test first closure invocation.

    const actual1 = TextInRange1();
    const expect1 = false;

    const args1 = .{ value1, actual1, expect1 };
    print("{any}\t{any}\t{any}\n", args1);

    try expect(actual1 == expect1);

    // Test second closure invocation.

    const actual2 = TextInRange2();
    const expect2 = true;

    const args2 = .{ value2, actual2, expect2 };
    print("{any}\t{any}\t{any}\n", args2);

    try expect(actual2 == expect2);

    return;
}
Enter fullscreen mode Exit fullscreen mode

Article History

Change Date Change Description
2024‑05‑10 Initial article published.
2024‑05‑13 Removed comptime on ClosureBind. See comment by Neurocyte@zig.new and the author's comment by houghtonap@zig.new. Minor wordsmithing.
2024‑05‑15 Minor change to closure pattern to allow the implementation to work with compile and run time values. See comment by Neurocyte@zig.new and the author's comment by houghtonap@zig.new. Added discussion of a simplified closure pattern. Minor wordsmithing.
2024‑05‑16 Changed ClosureBind to ClosureBinder and ClosureFunc to ClosureGenerator to make names more reflective of their functionality. Added ClosureSimplified to the closure example source code and added a mechanism to switch between the ClosureGenerator and ClosureSimplified implementations for running tests. Updated the closure implementation per comments by AndrewCodeDev@ziggit.def. Minor wordsmithing.
2024‑05‑19 Added errata about invoking the ClosureGenerator function before using complete using the generated closure function.
2024‑05‑23 Changed names to be more descriptive. Renamed Closure Pattern Errata section to Closure Pattern Limitation section and expanded content. Updated example and template with additional test conditions. Reworked content under Zig Closure Pattern section to be more descriptive of the example being presented. Minor wordsmithing.

Top comments (8)

Collapse
 
neurocyte profile image
Neurocyte • Edited

These are not closures.

Your closure environment consists entirely of comptime known constant values and not variables. That makes them plain old functions, not closures. This is a function generator pattern, not a closure pattern.

The usual definition of a closure (that you started your article with) is a function with bound free variables which implies some sort of additional state. Usually implemented as an additional environment pointer (often hidden by language syntax) which may vary at runtime.

Collapse
 
houghtonap profile image
Andrew Houghton

The definition for ClosureBind is wrong, since it used comptime this: ClosureScope. The correct definition for ClosureBind should have omitted comptime, thus allowing free bound variables at runtme. I'll update the article. Thanks for the feedback.

Collapse
 
neurocyte profile image
Neurocyte

Removing comptime from ClosureScope doesn't change anything. ClosureFunc returns a type and is therefore a comptime function and anything you pass to it has to be comptime known and is therefore implicitly also comptime. ClosureFunc is a comptime function generator that binds the function to some static values. It does not generate closures.

You can verify this in your example by passing a variable to TextInRange instead of a literal. Will get the message error: unable to resolve comptime value.

You can't implement closures without an additional state parameter at the closure call site. This is why closures in static languages are always some sort of object.

Thread Thread
 
houghtonap profile image
Andrew Houghton

In response to your comment I added the following test which confirms your assertion that the presented implementation works only with compile time static values and my misunderstanding of Zig's comptime concept.

test "[Comptime Only Issue]" {

    // Test reuse invocation of the closure.
    var from: u32 = undefined;
    var to: u32 = undefined;

    from = 2;
    to = 5;

    const closure = TextInRange(from, to);

    const text = closure("0123456789");
    print("text = {s}\n", .{text});
}
Enter fullscreen mode Exit fullscreen mode

After further review of the presented implementation and a few minor changes to ClosureType and ClosureBind the above test passes using runtime variables. I'll update the article. Thanks for clarifying Zig's comptime concept.

Collapse
 
kfird214 profile image
kfir • Edited

Somthing looks wrong to me.

Im no sure what does using var inside a struct means but I think it is like a static variable for the struct.

So the folowing line is an obvious bug

Bind.this = scope;
Enter fullscreen mode Exit fullscreen mode
test "closure test" {
    const closure23 = ClosureGenerator(2, 3);
    const closure13 = ClosureGenerator(1, 3); // this will override the static Bind.this variable and closure23 will not be the same.

    const res_ell = closure13("Hello");
    try std.testing.expectEqualSlices(u8, "ell", res_ell);

    const res_ll = closure23("Hello!");
    try std.testing.expectEqualSlices(u8, "ll", res_ll);
// ~
// ============ expected this output: =============  len: 2 (0x2)
// 6C 6C                                             ll
// ============= instead found this: ==============  len: 3 (0x3)
// 65 6C 6C                                          ell

    const res_orl = closure13("world!");
    try std.testing.expectEqualSlices(u8, "orl", res_orl);

    const res_rl = closure23("world!");
    try std.testing.expectEqualSlices(u8, "rl", res_rl);
}
Enter fullscreen mode Exit fullscreen mode

I cannot beleve that there is a way to create closures in zig without memory allocation of some sort on each closure creation.

Collapse
 
houghtonap profile image
Andrew Houghton

You are correct. This was also pointed out @neurocyte yesterday. I'm in the process of adding errata to the document and investigating alternate implementations. Thank you for the tests and the feedback.

Collapse
 
mkulak profile image
Misha • Edited

fn ClosureBind(comptime this: ClosureScope) ClosureType {

In ClosureBind argument this is marked as comptime.
But then later in ClosureFunc you're invoking ClosureBind with the value of ClosureScope that is not comptime. Why is this allowed?

fn ClosureFunc(begin: usize, end: usize) ClosureType {
const scope = ClosureScope{ .begin = begin, .end = end };
return ClosureBind(scope);
}

Collapse
 
houghtonap profile image
Andrew Houghton

The Zig Reference under the comptime section indicates:

A comptime parameter means that:

  • At the callsite, the value must be known at compile-time, or it is a compile error.
  • In the function definition, the value is known at compile-time.

The Zig Reference under the Local Variables section indicates:

When a local variable is const, it means that after initialization, the variable's value will not change. If the initialization value of a const variable is comptime-known, then the variable is also comptime-known.

Given those statements, ClosureBind declares the this parameter as comptime which implies that the value from the caller must be known at comptime. ClosureFunc creates the scope constant and initializes it. Since the initialization of the scope constant was known at comptime, then the scope constant is also known at comptime. Thus, satisfying the ClosureBind comptime constraint.

Hope that helps.