I’ve been thinking about different ways that this library could be structured to be more accessible, and I came up with a stripped-down variant of polystate
that’s much simpler, but still covers a lot polystate
’s functionality. I wanted to hear your thoughts on it, as I think you might be able to simplify polystate
in a similar way.
The main benefits of my approach:
- The representation of a state is more concrete. A state is just a struct with no fields, and the state’s transition logic is in the struct’s declarations.
- Composition is as simple as passing states into a function that returns another state.
- State names can be trivially refactored with ZLS, as a state’s name never needs to be declared in more than one place.
The main conventions I follow in this example:
- Each state declares the states it can transition to.
- Each state declares its name.
- Each state declares a transition function, which returns either the next transition function or an exit signal.
const std = @import("std");
pub fn main() !void {
const StartingState = Example.AOrB(Example.State1, Example.State2);
var gst: GlobalState = .init;
sw: switch (StartingState.transition(&gst)) {
.Current => |function| {
continue :sw function(&gst);
},
.Exit => {},
}
}
pub const GlobalState = struct {
counter: u64,
prng: std.Random.DefaultPrng,
pub const init: GlobalState = .{
.counter = 0,
.prng = .init(123),
};
};
pub const Example = struct {
pub const Exit = struct {
pub const name = getName(Example, Exit);
pub fn transition(_: *GlobalState) TransitionResult {
return .{ .Exit = {} };
}
};
pub const State1 = struct {
pub const transitions = .{
Exit,
AOrB(State1, State2),
};
pub const name = getName(Example, State1);
pub fn transition(gst: *GlobalState) TransitionResult {
return genericTransition(State1, gst);
}
pub fn transitionInt(gst: *GlobalState) usize {
gst.counter += 1;
if (gst.counter >= 10) {
return intFromTransition(State1, Exit);
}
return intFromTransition(State1, AOrB(State1, State2));
}
};
pub const State2 = struct {
pub const transitions = .{
Exit,
AOrB(State1, State2),
};
pub const name = getName(Example, State2);
pub fn transition(gst: *GlobalState) TransitionResult {
return genericTransition(State2, gst);
}
pub fn transitionInt(gst: *GlobalState) usize {
gst.counter += 1;
if (gst.counter >= 10) {
return intFromTransition(State2, Exit);
}
return intFromTransition(State2, AOrB(State1, State2));
}
};
pub fn AOrB(comptime A: type, comptime B: type) type {
return struct {
pub const transitions = .{
A,
B,
AOrB(A, B),
};
pub const name = getComposedName(Example, AOrB, .{ A, B });
const Self = @This();
pub fn transition(gst: *GlobalState) TransitionResult {
return genericTransition(Self, gst);
}
pub fn transitionInt(gst: *GlobalState) usize {
const random = gst.prng.random();
if (random.intRangeLessThan(usize, 0, 3) != 0) {
return intFromTransition(Self, AOrB(A, B));
}
if (random.boolean()) {
return intFromTransition(Self, A);
}
return intFromTransition(Self, B);
}
};
}
};
// Utilities:
pub fn getName(comptime Container: type, comptime decl: anytype) []const u8 {
comptime {
for (std.meta.declarations(Container)) |possible_decl| {
const name = possible_decl.name;
const decl_value = @field(Container, name);
if (@TypeOf(decl_value) == @TypeOf(decl) and decl_value == decl) {
return name;
}
} else unreachable;
}
}
pub fn getComposedName(comptime Container: type, comptime decl: anytype, comptime args: anytype) []const u8 {
comptime {
var composed_name: []const u8 = "";
composed_name = composed_name ++ getName(Container, decl) ++ "(";
for (&args, 0..) |arg, i| {
if (i > 0) {
composed_name = composed_name ++ ", ";
}
composed_name = composed_name ++ arg.name;
}
composed_name = composed_name ++ ")";
return composed_name;
}
}
pub fn genericTransition(comptime State: type, gst: *GlobalState) TransitionResult {
switch (State.transitionInt(gst)) {
inline 0...State.transitions.len - 1 => |int| {
std.debug.print("{s} -> {s}\n", .{ State.name, TransitionFromInt(State, int).name });
return .{ .Current = TransitionFromInt(State, int).transition };
},
else => unreachable,
}
}
pub fn intFromTransition(comptime State: type, comptime TransitionState: type) usize {
return comptime std.mem.indexOfScalar(type, &State.transitions, TransitionState) orelse unreachable;
}
pub fn TransitionFromInt(comptime State: type, comptime int: usize) type {
return State.transitions[int];
}
pub const TransitionResult = union(enum) {
Exit: void,
Current: *const fn (gst: *GlobalState) TransitionResult,
};