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"
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);
}
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);
}
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.
- Describe the closure's signature type.
- Describe the closure's implementation.
- Describe the closure's generator.
- Export the closure's generator name.
Describe the closure's binding scope type
/// Type used to describe the closure's binding scope.
const ClosureBindingScope = struct { begin: usize, end: usize };
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;
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];
}
};
}
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 theClosureImplementation
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);
}
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;
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;
}
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";
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 });
}
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");
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)'
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) });
}
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) });
}
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) });
}
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:
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
- Why is the exported name
TextInRange
used in the presented example instead of something more appropriate?
The inspiration for
TextInRange
example comes from theSplitter
namespace in the PowerQuery-M language. TheSplitTextByRanges
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;
}
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;
}
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. |
Latest comments (9)
mmm you converted zig lang into crap js ..
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
I cannot beleve that there is a way to create closures in zig without memory allocation of some sort on each closure creation.
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.
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.
The definition for
ClosureBind
is wrong, since it usedcomptime this: ClosureScope
. The correct definition forClosureBind
should have omittedcomptime
, thus allowing free bound variables at runtme. I'll update the article. Thanks for the feedback.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 messageerror: 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.
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.After further review of the presented implementation and a few minor changes to
ClosureType
andClosureBind
the above test passes using runtime variables. I'll update the article. Thanks for clarifying Zig'scomptime
concept.In ClosureBind argument
this
is marked ascomptime
.But then later in ClosureFunc you're invoking ClosureBind with the value of ClosureScope that is not comptime. Why is this allowed?
The Zig Reference under the comptime section indicates:
The Zig Reference under the Local Variables section indicates:
Given those statements,
ClosureBind
declares thethis
parameter ascomptime
which implies that the value from the caller must be known atcomptime
.ClosureFunc
creates thescope
constant and initializes it. Since the initialization of thescope
constant was known atcomptime
, then thescope
constant is also known atcomptime
. Thus, satisfying theClosureBind
comptime
constraint.Hope that helps.