Best pattern for callback parametrization

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.

4 Likes

Instead of having a void context, what if you make the bundle of functions into the context and make it so the code requires that the context has these functions?

That way the context is a type that requires a set of functions, similar to for example the hash context that requires eql and hash as methods, I think that would make it equivalent to some of the contexts used in the standard library?

4 Likes

Ohhh, right, I didn’t realize that std.HashMap is using a similar pattern, I was only thinking about std.sort which doesn’t!

1 Like

There is also std.mem.sortContext which is (currently) implemented via std.sort.insertionContext:

context must have methods swap and lessThan

I think std.sort is a simpler interface, while std.sortContext allows you to not only define the comparison function but also how the swap function works (which is useful when the default swap function std.mem.swap doesn’t work for what you consider to be the “element” while sorting.

Here is an example where I use a custom swap function to sort rows of a matrix that is stored as a flat slice in memory:

I think the “context with multiple methods” pattern makes sense, when you have multiple methods that need to work together and I see the “provide a single callback function” more as the special case of that when you only need a single function.

3 Likes

The “direct” method can lead to some god-awful-looking code:

queue(struct {
   fn start() void {
       // …
   }
}.start, struct {
   fn finish() void {
       // …
   }
}.finish,
);

The zzz HTTP uses this convention a lot.