I think an important question here is whether the set of possible “callbacks” is closed or open, meaning: “are all the possible kinds of callbacks known at compile time and they only differ by runtime data?”. If that is the case I would ditch the callbacks and use a tagged union instead:
const std = @import("std");
const Compute = union(enum) {
found: u8,
not_found: u8,
sum_at_least: struct {
target: usize,
start: usize = 0,
},
fn compute(self: *@This(), r: u8) bool {
switch (self.*) {
.found => |f| return f == r,
.not_found => |n| return n != r,
.sum_at_least => |*u| {
u.start += r;
return u.start >= u.target;
},
}
}
};
fn countUntil(data: []const u8, c: *Compute) usize {
var index: usize = 0;
while (index < data.len) {
const r = data[index];
if (c.compute(r)) {
return index;
}
index += 1;
}
return data.len;
}
pub fn main() !void {
const seed: u64 = @intCast(std.time.nanoTimestamp());
std.debug.print("seed: {}\n", .{seed});
var gen = std.rand.DefaultPrng.init(seed);
const rng = gen.random();
const data = "AAAA, world!";
var c: Compute = switch (rng.uintLessThan(u8, 3)) {
0 => .{ .found = '!' },
1 => .{ .not_found = 'A' },
2 => .{ .sum_at_least = .{ .target = 800 } },
else => unreachable,
};
std.debug.print("compute: {}\n", .{c});
std.debug.print("{}\n", .{countUntil(data, &c)});
}
If the set is open and you need to choose at runtime, you can define an interface that uses type erasure like std.mem.Allocator
instead of the tagged union.
If the choice can be made at comptime you could use a comptime enum (possibly associated with a runtime payload/tagged union) like here: Tagged union with comptime tag - #2 by Sze
I think the other answers are also valid and useful, I just prefer union if it is enough to do the job. I enjoy that it can be used to basically limit the scope of what needs to be considered, in my example you simply see it takes a *Compute
you look at compute and can see all the cases, which also makes it easier to evaluate whether countUntil
works correctly for all cases. With open solutions you don’t get that. So I guess my 2 cents are, don’t use too generic solutions, staying specific has its benefits.