I have this struct which produces and stores ‘moves’ to a std.ArrayList.
But now I want to ‘extract’ this storage functionality to the outside.
Get rid of the internal arraylist and provide a store function on the outside.
But I just cannot get my syntax right. A little sketch down here.
const Producer = struct
{
fn init () {...}
fn producemoves(self)
{
produce and foreach move do self.storemove(context, move)
}
fn store_move(self, move)
{
// call outside function store_move(context, move)
}
}
const Receiver = struct
{
fn execute(self: *Receiver)
{
// create producer(Receiver??, store_move??)
// producer.producemoves();
}
fn store_move(self: *Receiver, move: Move)
{
// store here / or count / or do whatever we want with the move.
}
}
It must be some magic combination of comptime / anytype / generic.
I would be very thankful if someone could explain the secrets / syntax.
How to create a generic Producer with a ‘context’ which is in this case ‘Receiver’
and a pointer to its ‘store-move’ function.
I am surely gonna need it.
In one case it is a randomgame player.
In another case an engine which only has to count the top 10 moves or store all the moves.
it is an abstact ‘event’ onStore.
It’s still too early to implement it, since you believe that you are surely going to need it, but you don’t need it yet. The point of YAGNI is that you benefit from never implementing something until the very moment that you actually truly really do need it right now.
Some truth in that, but I mainly cannot get my head around it (generics / anytypes/interfacss).
I am building a scrabble engine (comparable to a chess engine).
This Engine uses a MoveGenerator who has to pass a “I have a new move produced” to the Engine.
Sometimes we just count the moves, sometimes we need them all, sometimes we need only one, sometimes we need a filter. That is why I need the flexibility of a custom function inside the Engine struct.
I don’t know yet if the structure is perfect but you have to see it in a “Engine Iterative Deepening AlphaBeta Search Pruning” context.
It currently lacks some interface flexibility but it works.
Today (experimenting) I divided things into:
MoveGenerator
Just producing moves and calls a parameterized function from MoveHandler.
contains a pointer to MoveHandler and a function from MoveHandler like store.
MoveHandler
Which contains a movelist has functions like: fn store(self, move) // store in movelist fn count(self, move) // just count, do not store fn any(self, move) // please dear MoveGenerator quit if found one move fn filter(self, move) // just take filtered moves
Engine
Which performs a tree search. the engine creates a MoveHandler and a MoveGenerator.
var handler = MoveHandler.init();
var gen = MoveGenerator.init(board, &handler, MoveHandler.count)
or
var gen = MoveGenerator.init(board, &handler, MoveHandler.filter)
(and still looking for more comptime stuff, but it is kind of working now with same speed as before)
My instinct is that you’re applying patterns from other languages, which won’t be a good fit for Zig. For instance, you can just have a pointer to a MoveHandler and call store with that pointer. It seems like you need some flexibility in which function of the MoveHandler is called: consider using an enum switch to decide that in a dispatch function.
As a general rule, you don’t need dispatch based on function pointers unless you have an open-world problem: that is, there’s code out of your control where you’d like to make it possible for that code to implement a type-compatible interface, which can be stored/created/passed around as a concrete type, that is, not specialized with anytype: using an anytype specialized at comptime can be a better choice if there isn’t a need for the interface to be a single type. See Allocator for the classic example of the open-world pattern.
From what I can tell, your code is closed-world, all the variations you need will appear in code which you control. It looks like you’re translating code which used an interface in another language, you did some reading on how Zig does interfaces, and you’re trying to apply that knowledge. My additional tip is that Zig does interfaces (in the sense you’re implementing) rarely, because it has other ways of structuring code which are more efficient and less fragile.
Very true! Still I need the storage handling on the outside of the movegenerator, that is: the generator does not know anything about what is being done with the moves.
If the MoveHandler needs to be doing only one thing at a time, then it can hold a state enum, and have a generic dispatch function which uses that information.
If you need to create the move conditions in a way which doesn’t affect the source, then you can have a MoveGenerator struct which holds a pointer to the MoveHandler and the action-specific state enum, then calls a similar dispatch function which passes in that state.
A nice thing about doing it this way is that the state enum is explicit data about what the generator is doing: it’s easy to log, use later if you discover that wait, it’s actually helpful for the generator to be able to check what’s happening. You can just look at it when debugging and know what’s going on, it’s plain old data.
Function pointers make all of that hard, and limit optimization opportunities in the process.
Possibly. That will produce a unique MoveHandler type for each state. That might or might not work, the consequence will be viral: everything which accepts a MoveHandler will need to accept it as an anytype, and that will generate a version of the function which takes it for each sort of MoveHandler.
That may be good, actually, in terms of generating optimal code for each pathway. The binary will be larger but depending on how much time is spent inside each of those function calls, you might come out on top. It gets challenging though if you need to store the MoveHandler in another struct, because you can’t do that if you have say, five distinct MoveHandlers.
In that case you might want the switch to happen at runtime. It’s still likely to be better to have a conditional leading to known function addresses, rather than an implicit conditional (as far as the branch predictor is concerned) based on a runtime function address. No guarantees on that front, unfortunately the performance of modern CPUs can be pretty hard to reason about in advance when it comes to this type of tradeoff. A lot of it depends on how much time it spends in the switch vs. in the switched-to function, and how predictable the switch (or the destination address) is to the branch predictor.
I got the impression that you want the MoveHandler to be passed in and used, and not stored by the struct which receives it, in which case using a comptime enum to eliminate the switch, at the cost of several specializations, that’s likely to be your best-performing option.
Yep. I once wrote an insanely fast movegenerator in Rust for chess.
In there the entire optional movegeneration was monomorphized (generate captues, checks, evasions etc.) inside the MovGen. But there the movelist was a relatively small fixed array (easily fitting in the stack) in which I incremented a pointer when storing moves.
In scrabble sometimes there are more than 100.000 possibilities. And a move is 16 bytes instead of 2.
I think I more or less catch the idea.
Willl upload the stuff and come back here…