Making my anytype interface more flexible

Maybe there is a more flexible way to do things than I describe down here?

In my chessprogram I use an anytype when generating moves.
fn gen_moves(pos: *const Position, storage: anytype) void {}
This one simply calls storage.store() orelse return for each produced move.

The ‘requirements’ for this anytype: it is a struct with two functions.
fn reset(self: *Self) void {}
fn store(self: *Self, m: Move) ?void {}
The store function returns null if the movegenerator should stop producing moves.

Currently I have 4 structs which can be injected into gen_moves depending on what I want to do:
In short:

pub const MoveStorage = struct {
    array: [224]Move, // store each move in the array.
}
pub const JustCount = struct {
    count: u8, // just count the moves.
}
pub const Any = struct {
    has_moves: bool,  // when 1 move found stop.
}
pub const Finder = struct {
    move_to_find: Move,
    found: bool, // when found then stop.
}

Now this works very well and fast.
But I would like to have a more constraint parameter than anytype,
thus knowing inside gen_moves what we are doing (strongly typed).

Would it be an idea - for example - to put in the type, something like this params?
storage_type: type, storage: *@TypeOf(storage_type)
(edit: this still will not reallly know what is put in…)

It also would be nice if the store functions could have additional parameters (flexible).

Or some static interface? (never done that).

Tagged union would be the most straight forward change.

I personally would have different functions.

Was thinking about that too. Never used them.

If it needs to be more flexible than an union, but still comptime known, I’ve also been doing stuff like the vtable-interface-approach-but-comptime. Note that you probably don’t need it to be comptime-known in most cases; unions or vtables are very likely to be optimized enough if it is more on the static side anyway, and you don’t need to compile every variant of comptime arguments. Nevertheless, if it fits your project:

struct CountStorage = struct {
  count: usize,
  storage: Storage(@This()) = .{}; // no actual data, just for the namespace

  // some counter specific function
  pub fn reset(self: *FooStorage) void {
    self.count = 0;
  }

  pub fn store(self: *FooStorage, m: Move) ?void {
    self.count += 1;
  }
};

pub fn Storage(Container: type) type {
  // could check here if Container actually fulfills the Storage interface
  // e.g. has `store` with the right function type
  return struct {
    const Self = @This();

    // just forwarding
    pub fn store(self: *Self, m: Move) ?void {
      const parent : Container = @fieldParentPtr("storage", self);
      parent.store();
    }

    // some derived function that works on all Storages
    pub fn storeSlice(self: *Self, ms: []Move) usize {
      for (0.., ms) |i, ms| self.store(m) orelse return i;
      return ms.len;
    }
  }
}

// some variants how you could call this
fn doSomething1(container: anytype, ms: []Move) void {
   var s : Storage(@TypeOf(container.*)) = &container.storage;
   // we now have a typed storage!
   _ = s.fnForAllStorages(ms);
}

// more type safe variant
fn doSomething2(Container: type, s: *Storage(Container), ms: []Move) void {
   _ = s.fnForAllStorages(ms);
}

It’s more implicit and less flexible, i.e. fixed on the names of storage and store. You could pass these as arguments to Storage, but then you’d need to pass these arguments to doSomething, too, which makes it a bit too verbose in the project where I used this pattern:

const StorageConfig = struct {
  container: type,
  field_name: []const u8,
  store_fun: *const fn( *anyopaque, Move) ?void
}; 
pub fn Storage(comptime config: StorageConfig) type { 
  return struct { … };
}

struct CountStorage = struct {
  count: usize,
  storage: Storage(StorageConfig) = .{};

  const config : StorageConfig = .{
    .container = CountStorage,
    .field_name = "storage",
    .store_fun = store
  };

  …
}

fn doSomething3(comptime storage_config: StorageConfig, s: *Storage(storage_config), ms: []Move) void {
   _ = s.fnForAllStorages(ms);
}

fn foo() void {
  count_storage : CountStorage = .{ .count = 0 };
  doSomething3(CountStorage.config, &count_storage.storage, &.{});
}