Let’s say you are writing a thing, and the thing in question needs to be parametrized by a couple of callbacks. I see two ways to achieve this: direct way, and “smart” way. The direct way would be something like:
pub fn KWayMergeIteratorType(
comptime Context: type,
comptime Key: type,
comptime Value: type,
comptime stream_peek: fn (
context: *Context,
stream_index: u32,
) error{ Empty, Drained }!Key,
comptime stream_pop: fn (context: *Context, stream_index: u32) Value,
comptime stream_precedence: fn (context: *const Context, a: u32, b: u32) bool,
) type {
...
}
That is, just pass a bunch of comptime arguments of fn type.
The problem with that is that Zig doesn’t have function expressions, so the call site will generally be somewhat ugly.
The “smart” way is to do something akin to:
pub fn KWayMergeIteratorType(
comptime Context: type,
comptime Key: type,
comptime Value: type,
comptime StreamFunctions: type,
) type {
const stream_peek: fn (context: *Context, stream_index: u32) error{ Empty, Drained }!Key =
StreamFunctions.stream_peek;
const stream_pop: fn (context: *Context, stream_index: u32) Value =
StreamFunctions.stream_pop;
const stream_precedence: fn (context: *const Context, a: u32, b: u32) bool =
StreamFunctions.stream_precedence;
...
}
Instead of accepting a bunch of individual functions, we require a type, whose declarations are the functions. This is neat, because the callsite could look like
const MyKWayMerge = KWayMergeIteratorType(
void,
u64,
u128,
struct {
fn stream_peek(context: *void, stream_index: u32) error{ Empty, Drained }!u64 { ... }
fn stream_pop(context: *void, stream_index: u32) u128 { ... }
fn stream_precedence(context: *const void, a: u32, b: u32) bool { ... }
}
)
That is, while Zig doesn’t have literals for functions, it has container literals.
An alternative spin on the idea would be to write a declStruct(T: type) function that works like this:
const x: struct {
foo: fn() u32,
} = declStruct(struct {
fn foo() u32 { return 92; }
})
going from the decl world to the value world.
Has anyone done this in practice in larger projects? Are there any non-obvious reasons why this is great/terrible idea?
One non-obvious reason why is this great — you are forced to give the same name to all instances of callback functions, which improves greppability.