Resurrecting this to see what people think of the generic interface convention in my tiny Zimpl library. It achieves a very limited version of “generic concepts on function declarations.”
The idea is to provide clarity and avoid relying on duck typing by taking a separate parameter that contains all the necessary “member functions” and type data. Part of the std.io.Reader
interface translated to this style is below as an example.
pub fn Reader(comptime Type: type) type {
return struct {
ReadError: type = error{},
read: fn (reader_ctx: Type, buffer: []u8) anyerror!usize,
};
}
pub inline fn read(
reader_ctx: anytype,
reader_impl: Reader(@TypeOf(reader_ctx)),
buffer: []u8,
) reader_impl.ReadError!usize {
return @errorCast(reader_impl.read(reader_ctx, buffer));
}
pub inline fn readAll(
reader_ctx: anytype,
reader_impl: Reader(@TypeOf(reader_ctx)),
buffer: []u8,
) reader_impl.ReadError!usize {
return readAtLeast(reader_ctx, reader_impl, buffer, buffer.len);
}
pub inline fn readAtLeast(
reader_ctx: anytype,
reader_impl: Reader(@TypeOf(reader_ctx)),
buffer: []u8,
len: usize,
) reader_impl.ReadError!usize {
assert(len <= buffer.len);
var index: usize = 0;
while (index < len) {
const amt = try read(reader_ctx, reader_impl, buffer[index..]);
if (amt == 0) break;
index += amt;
}
return index;
}
The issue now is that calling such functions is verbose and clunky, even in cases like Reader
where the interface only has two fields.
test {
var buffer: [19]u8 = undefined;
var file = try std.fs.cwd().openFile("my_file.txt", .{});
try io.readAll(
file,
.{
.read = std.fs.File.read,
.ReadError = std.fs.File.ReadError,
},
&buffer
);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}
My attempt at a solution is the Impl
function: Impl(Type, Reader)
is a struct with the same fields as Reader(Type)
but with the default value of each field set to be the declaration of Type
of the same name, if such a declaration exists[1].
Replacing Reader(@TypeOf(reader_ctx))
with Impl(@TypeOf(reader_ctx), Reader)
everywhere in the above example lets us default construct the reader_impl
parameter for std.fs.File
.
test {
var buffer: [19]u8 = undefined;
var file = try std.fs.cwd().openFile("my_file.txt", .{});
try io.readAll(file, .{}, &buffer);
try std.testing.expectEqualStrings("Hello, I am a file!", &buffer);
}
I’ve been enjoying this style because it provides type requirements for anytype
parameters in function signatures while remaining simple and feeling similar to other Zig patterns. It has very limited power and thus encourages simple uses of generics, which also feels in line with Zig.